Repository: MuntashirAkon/AppManager Branch: master Commit: ba82bc9ffbd9 Files: 1869 Total size: 17.4 MB Directory structure: gitextract_vctfzydw/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── config.yml │ │ ├── documentation.yml │ │ ├── feature_request.yml │ │ └── help-wanted.yml │ └── workflows/ │ ├── codeql.yml │ ├── lint.yml │ └── tests.yml ├── .gitignore ├── .gitmodules ├── .run/ │ ├── Documentation.run.xml │ ├── app.run.xml │ ├── app_details.run.xml │ ├── fm.run.xml │ ├── lint.run.xml │ ├── settings.run.xml │ └── test.run.xml ├── BUILDING.rst ├── CONTRIBUTING.rst ├── COPYING ├── LICENSES/ │ ├── Apache-2.0 │ ├── BSD-2-Clause │ ├── BSD-3-Clause │ ├── CC-BY-SA-4.0 │ ├── GPL-2.0 │ ├── GPL-3.0 │ ├── ISC │ ├── MIT │ └── WTFPL ├── PRIVACY_POLICY.rst ├── README.md ├── app/ │ ├── .gitignore │ ├── build.gradle │ ├── lint.xml │ ├── proguard-rules.pro │ ├── schemas/ │ │ └── io.github.muntashirakon.AppManager.db.AppsDb/ │ │ ├── 1.json │ │ ├── 2.json │ │ ├── 3.json │ │ ├── 4.json │ │ ├── 5.json │ │ ├── 6.json │ │ └── 7.json │ └── src/ │ ├── debug/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── io/ │ │ └── github/ │ │ └── muntashirakon/ │ │ └── AppManager/ │ │ └── debug/ │ │ └── R.java │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── aidl/ │ │ │ └── io/ │ │ │ └── github/ │ │ │ └── muntashirakon/ │ │ │ └── AppManager/ │ │ │ ├── IAMService.aidl │ │ │ ├── IRemoteProcess.aidl │ │ │ ├── IRemoteShell.aidl │ │ │ ├── IShellResult.aidl │ │ │ └── ipc/ │ │ │ └── ps/ │ │ │ ├── ProcessEntry.aidl │ │ │ └── ProcessUsers.aidl │ │ ├── annotations/ │ │ │ └── android/ │ │ │ └── content/ │ │ │ └── pm/ │ │ │ └── annotations.xml │ │ ├── assets/ │ │ │ ├── blanks/ │ │ │ │ ├── blank.docx │ │ │ │ ├── blank.odp │ │ │ │ ├── blank.ods │ │ │ │ ├── blank.odt │ │ │ │ ├── blank.pptx │ │ │ │ ├── blank.txt │ │ │ │ └── blank.xlsx │ │ │ ├── debloat.json │ │ │ ├── editor_themes/ │ │ │ │ ├── dark.tmTheme.json │ │ │ │ └── light.tmTheme │ │ │ ├── languages/ │ │ │ │ ├── java/ │ │ │ │ │ ├── language-configuration.json │ │ │ │ │ └── tmLanguage.json │ │ │ │ ├── json/ │ │ │ │ │ ├── language-configuration.json │ │ │ │ │ └── tmLanguage.json │ │ │ │ ├── kotlin/ │ │ │ │ │ ├── language-configuration.json │ │ │ │ │ └── tmLanguage.json │ │ │ │ ├── properties/ │ │ │ │ │ ├── language-configuration.json │ │ │ │ │ └── tmLanguage.json │ │ │ │ ├── sh/ │ │ │ │ │ ├── language-configuration.json │ │ │ │ │ └── tmLanguage.json │ │ │ │ ├── smali/ │ │ │ │ │ ├── language-configuration.json │ │ │ │ │ └── tmLanguage.json │ │ │ │ └── xml/ │ │ │ │ ├── language-configuration.json │ │ │ │ └── tmLanguage.json │ │ │ ├── run_server.sh │ │ │ └── suggestions.json │ │ ├── cpp/ │ │ │ ├── AhoCorasick.cpp │ │ │ ├── AhoCorasick.h │ │ │ ├── CMakeLists.txt │ │ │ ├── io_github_muntashirakon_AppManager_utils_CpuUtils.cpp │ │ │ ├── io_github_muntashirakon_AppManager_utils_CpuUtils.h │ │ │ ├── io_github_muntashirakon_algo_AhoCorasick.cpp │ │ │ ├── io_github_muntashirakon_algo_AhoCorasick.h │ │ │ ├── io_github_muntashirakon_compat_system_OsCompat.cpp │ │ │ └── io_github_muntashirakon_compat_system_OsCompat.h │ │ ├── java/ │ │ │ ├── androidx/ │ │ │ │ ├── appcompat/ │ │ │ │ │ └── app/ │ │ │ │ │ └── PublicTwilightManager.java │ │ │ │ └── documentfile/ │ │ │ │ └── provider/ │ │ │ │ ├── DocumentFileUtils.java │ │ │ │ ├── MediaDocumentFile.java │ │ │ │ └── VirtualDocumentFile.java │ │ │ ├── aosp/ │ │ │ │ └── libcore/ │ │ │ │ └── util/ │ │ │ │ ├── EmptyArray.java │ │ │ │ └── HexEncoding.java │ │ │ ├── io/ │ │ │ │ └── github/ │ │ │ │ └── muntashirakon/ │ │ │ │ ├── AppManager/ │ │ │ │ │ ├── AppManager.java │ │ │ │ │ ├── BaseActivity.java │ │ │ │ │ ├── DummyActivity.java │ │ │ │ │ ├── PerProcessActivity.java │ │ │ │ │ ├── StaticDataset.java │ │ │ │ │ ├── accessibility/ │ │ │ │ │ │ ├── AccessibilityMultiplexer.java │ │ │ │ │ │ ├── BaseAccessibilityService.java │ │ │ │ │ │ ├── NoRootAccessibilityService.java │ │ │ │ │ │ └── activity/ │ │ │ │ │ │ ├── LeadingActivityTrackerActivity.java │ │ │ │ │ │ └── TrackerWindow.java │ │ │ │ │ ├── adb/ │ │ │ │ │ │ ├── AdbConnectionManager.java │ │ │ │ │ │ ├── AdbPairingService.java │ │ │ │ │ │ └── AdbUtils.java │ │ │ │ │ ├── apk/ │ │ │ │ │ │ ├── ApkFile.java │ │ │ │ │ │ ├── ApkSource.java │ │ │ │ │ │ ├── ApkUtils.java │ │ │ │ │ │ ├── ApplicationInfoApkSource.java │ │ │ │ │ │ ├── CachedApkSource.java │ │ │ │ │ │ ├── UriApkSource.java │ │ │ │ │ │ ├── behavior/ │ │ │ │ │ │ │ ├── FreezeUnfreeze.java │ │ │ │ │ │ │ ├── FreezeUnfreezeActivity.java │ │ │ │ │ │ │ ├── FreezeUnfreezeService.java │ │ │ │ │ │ │ └── FreezeUnfreezeShortcutInfo.java │ │ │ │ │ │ ├── dexopt/ │ │ │ │ │ │ │ ├── DexOptDialog.java │ │ │ │ │ │ │ ├── DexOptOptions.java │ │ │ │ │ │ │ └── DexOptimizer.java │ │ │ │ │ │ ├── installer/ │ │ │ │ │ │ │ ├── ApkQueueItem.java │ │ │ │ │ │ │ ├── InstallerDialogFragment.java │ │ │ │ │ │ │ ├── InstallerDialogHelper.java │ │ │ │ │ │ │ ├── InstallerOptions.java │ │ │ │ │ │ │ ├── InstallerOptionsFragment.java │ │ │ │ │ │ │ ├── PackageInstallerActivity.java │ │ │ │ │ │ │ ├── PackageInstallerBroadcastReceiver.java │ │ │ │ │ │ │ ├── PackageInstallerCompat.java │ │ │ │ │ │ │ ├── PackageInstallerService.java │ │ │ │ │ │ │ ├── PackageInstallerViewModel.java │ │ │ │ │ │ │ └── SupportedAppStores.java │ │ │ │ │ │ ├── list/ │ │ │ │ │ │ │ ├── AppListItem.java │ │ │ │ │ │ │ └── ListExporter.java │ │ │ │ │ │ ├── parser/ │ │ │ │ │ │ │ ├── AndroidBinXmlDecoder.java │ │ │ │ │ │ │ ├── AndroidBinXmlEncoder.java │ │ │ │ │ │ │ ├── ManifestComponent.java │ │ │ │ │ │ │ ├── ManifestIntentFilter.java │ │ │ │ │ │ │ └── ManifestParser.java │ │ │ │ │ │ ├── signing/ │ │ │ │ │ │ │ ├── SigSchemes.java │ │ │ │ │ │ │ ├── Signer.java │ │ │ │ │ │ │ ├── SignerInfo.java │ │ │ │ │ │ │ └── ZipAlign.java │ │ │ │ │ │ ├── splitapk/ │ │ │ │ │ │ │ ├── ApksMetadata.java │ │ │ │ │ │ │ ├── SplitApkChooser.java │ │ │ │ │ │ │ └── SplitApkExporter.java │ │ │ │ │ │ └── whatsnew/ │ │ │ │ │ │ ├── ApkWhatsNewFinder.java │ │ │ │ │ │ ├── WhatsNewDialogFragment.java │ │ │ │ │ │ ├── WhatsNewDialogViewModel.java │ │ │ │ │ │ ├── WhatsNewFragment.java │ │ │ │ │ │ └── WhatsNewRecyclerAdapter.java │ │ │ │ │ ├── app/ │ │ │ │ │ │ └── AndroidFragment.java │ │ │ │ │ ├── backup/ │ │ │ │ │ │ ├── BackupCryptSetupHelper.java │ │ │ │ │ │ ├── BackupDataDirectoryInfo.java │ │ │ │ │ │ ├── BackupException.java │ │ │ │ │ │ ├── BackupFlags.java │ │ │ │ │ │ ├── BackupItems.java │ │ │ │ │ │ ├── BackupManager.java │ │ │ │ │ │ ├── BackupOp.java │ │ │ │ │ │ ├── BackupUtils.java │ │ │ │ │ │ ├── CryptoUtils.java │ │ │ │ │ │ ├── MetadataManager.java │ │ │ │ │ │ ├── RestoreOp.java │ │ │ │ │ │ ├── VerifyOp.java │ │ │ │ │ │ ├── adb/ │ │ │ │ │ │ │ ├── AndroidBackupCreator.java │ │ │ │ │ │ │ ├── AndroidBackupExtractor.java │ │ │ │ │ │ │ ├── AndroidBackupHeader.java │ │ │ │ │ │ │ ├── BackupCategories.java │ │ │ │ │ │ │ └── Constants.java │ │ │ │ │ │ ├── convert/ │ │ │ │ │ │ │ ├── ConvertUtils.java │ │ │ │ │ │ │ ├── Converter.java │ │ │ │ │ │ │ ├── ImportType.java │ │ │ │ │ │ │ ├── OABConverter.java │ │ │ │ │ │ │ ├── SBConverter.java │ │ │ │ │ │ │ └── TBConverter.java │ │ │ │ │ │ ├── dialog/ │ │ │ │ │ │ │ ├── BackupFragment.java │ │ │ │ │ │ │ ├── BackupInfo.java │ │ │ │ │ │ │ ├── BackupInfoState.java │ │ │ │ │ │ │ ├── BackupRestoreDialogFragment.java │ │ │ │ │ │ │ ├── BackupRestoreDialogViewModel.java │ │ │ │ │ │ │ ├── FlagsAdapter.java │ │ │ │ │ │ │ ├── RestoreMultipleFragment.java │ │ │ │ │ │ │ └── RestoreSingleFragment.java │ │ │ │ │ │ └── struct/ │ │ │ │ │ │ ├── BackupMetadataV2.java │ │ │ │ │ │ ├── BackupMetadataV5.java │ │ │ │ │ │ ├── BackupOpOptions.java │ │ │ │ │ │ ├── DeleteOpOptions.java │ │ │ │ │ │ └── RestoreOpOptions.java │ │ │ │ │ ├── batchops/ │ │ │ │ │ │ ├── BatchOpsLogger.java │ │ │ │ │ │ ├── BatchOpsManager.java │ │ │ │ │ │ ├── BatchOpsResultsActivity.java │ │ │ │ │ │ ├── BatchOpsService.java │ │ │ │ │ │ ├── BatchQueueItem.java │ │ │ │ │ │ └── struct/ │ │ │ │ │ │ ├── BatchAppOpsOptions.java │ │ │ │ │ │ ├── BatchBackupImportOptions.java │ │ │ │ │ │ ├── BatchBackupOptions.java │ │ │ │ │ │ ├── BatchComponentOptions.java │ │ │ │ │ │ ├── BatchDexOptOptions.java │ │ │ │ │ │ ├── BatchFreezeOptions.java │ │ │ │ │ │ ├── BatchNetPolicyOptions.java │ │ │ │ │ │ ├── BatchPermissionOptions.java │ │ │ │ │ │ └── IBatchOpOptions.java │ │ │ │ │ ├── changelog/ │ │ │ │ │ │ ├── Changelog.java │ │ │ │ │ │ ├── ChangelogHeader.java │ │ │ │ │ │ ├── ChangelogItem.java │ │ │ │ │ │ ├── ChangelogParser.java │ │ │ │ │ │ └── ChangelogRecyclerAdapter.java │ │ │ │ │ ├── compat/ │ │ │ │ │ │ ├── ActivityManagerCompat.java │ │ │ │ │ │ ├── AppOpsManagerCompat.java │ │ │ │ │ │ ├── ApplicationInfoCompat.java │ │ │ │ │ │ ├── BackupCompat.java │ │ │ │ │ │ ├── BinderCompat.java │ │ │ │ │ │ ├── BiometricAuthenticatorsCompat.java │ │ │ │ │ │ ├── ClearDataObserver.java │ │ │ │ │ │ ├── ConnectivityManagerCompat.java │ │ │ │ │ │ ├── DeviceIdleManagerCompat.java │ │ │ │ │ │ ├── DomainVerificationManagerCompat.java │ │ │ │ │ │ ├── InputManagerCompat.java │ │ │ │ │ │ ├── InstallSourceInfoCompat.java │ │ │ │ │ │ ├── IntegerCompat.java │ │ │ │ │ │ ├── ManifestCompat.java │ │ │ │ │ │ ├── NetworkPolicyManagerCompat.java │ │ │ │ │ │ ├── NetworkStatsCompat.java │ │ │ │ │ │ ├── NetworkStatsManagerCompat.java │ │ │ │ │ │ ├── OverlayManagerCompact.java │ │ │ │ │ │ ├── PackageInfoCompat2.java │ │ │ │ │ │ ├── PackageManagerCompat.java │ │ │ │ │ │ ├── PermissionCompat.java │ │ │ │ │ │ ├── ProcessCompat.java │ │ │ │ │ │ ├── SensorServiceCompat.java │ │ │ │ │ │ ├── StorageManagerCompat.java │ │ │ │ │ │ ├── SubscriptionManagerCompat.java │ │ │ │ │ │ ├── ThumbnailUtilsCompat.java │ │ │ │ │ │ ├── UriCompat.java │ │ │ │ │ │ ├── UsageStatsManagerCompat.java │ │ │ │ │ │ └── VirtualDeviceManagerCompat.java │ │ │ │ │ ├── crypto/ │ │ │ │ │ │ ├── AESCrypto.java │ │ │ │ │ │ ├── Crypto.java │ │ │ │ │ │ ├── CryptoException.java │ │ │ │ │ │ ├── DummyCrypto.java │ │ │ │ │ │ ├── ECCCrypto.java │ │ │ │ │ │ ├── OpenPGPCrypto.java │ │ │ │ │ │ ├── OpenPGPCryptoActivity.java │ │ │ │ │ │ ├── RSACrypto.java │ │ │ │ │ │ ├── RandomChar.java │ │ │ │ │ │ ├── auth/ │ │ │ │ │ │ │ ├── AuthFeatureDemultiplexer.java │ │ │ │ │ │ │ ├── AuthManager.java │ │ │ │ │ │ │ └── AuthManagerActivity.java │ │ │ │ │ │ └── ks/ │ │ │ │ │ │ ├── AesEncryptedData.java │ │ │ │ │ │ ├── CompatUtil.java │ │ │ │ │ │ ├── KeyPair.java │ │ │ │ │ │ ├── KeyStoreActivity.java │ │ │ │ │ │ ├── KeyStoreManager.java │ │ │ │ │ │ ├── KeyStoreUtils.java │ │ │ │ │ │ ├── SecretKeyAndVersion.java │ │ │ │ │ │ └── SecretKeyCompat.java │ │ │ │ │ ├── db/ │ │ │ │ │ │ ├── AppsDb.java │ │ │ │ │ │ ├── dao/ │ │ │ │ │ │ │ ├── AppDao.java │ │ │ │ │ │ │ ├── BackupDao.java │ │ │ │ │ │ │ ├── FmFavoriteDao.java │ │ │ │ │ │ │ ├── FreezeTypeDao.java │ │ │ │ │ │ │ ├── LogFilterDao.java │ │ │ │ │ │ │ └── OpHistoryDao.java │ │ │ │ │ │ ├── entity/ │ │ │ │ │ │ │ ├── App.java │ │ │ │ │ │ │ ├── Backup.java │ │ │ │ │ │ │ ├── FmFavorite.java │ │ │ │ │ │ │ ├── FreezeType.java │ │ │ │ │ │ │ ├── LogFilter.java │ │ │ │ │ │ │ └── OpHistory.java │ │ │ │ │ │ └── utils/ │ │ │ │ │ │ └── AppDb.java │ │ │ │ │ ├── debloat/ │ │ │ │ │ │ ├── BloatwareDetailsDialog.java │ │ │ │ │ │ ├── DebloatObject.java │ │ │ │ │ │ ├── DebloaterActivity.java │ │ │ │ │ │ ├── DebloaterListOptions.java │ │ │ │ │ │ ├── DebloaterRecyclerViewAdapter.java │ │ │ │ │ │ ├── DebloaterViewModel.java │ │ │ │ │ │ └── SuggestionObject.java │ │ │ │ │ ├── details/ │ │ │ │ │ │ ├── ActivityLauncherShortcutActivity.java │ │ │ │ │ │ ├── AppDetailsActivity.java │ │ │ │ │ │ ├── AppDetailsComponentsFragment.java │ │ │ │ │ │ ├── AppDetailsFragment.java │ │ │ │ │ │ ├── AppDetailsOtherFragment.java │ │ │ │ │ │ ├── AppDetailsOverlaysFragment.java │ │ │ │ │ │ ├── AppDetailsPermissionsFragment.java │ │ │ │ │ │ ├── AppDetailsViewModel.java │ │ │ │ │ │ ├── IconPickerDialogFragment.java │ │ │ │ │ │ ├── PackageItemShortcutInfo.java │ │ │ │ │ │ ├── info/ │ │ │ │ │ │ │ ├── ActionItem.java │ │ │ │ │ │ │ ├── AppInfoFragment.java │ │ │ │ │ │ │ ├── AppInfoRecyclerAdapter.java │ │ │ │ │ │ │ ├── AppInfoViewModel.java │ │ │ │ │ │ │ ├── ListItem.java │ │ │ │ │ │ │ └── TagItem.java │ │ │ │ │ │ ├── manifest/ │ │ │ │ │ │ │ ├── ManifestViewerActivity.java │ │ │ │ │ │ │ └── ManifestViewerViewModel.java │ │ │ │ │ │ └── struct/ │ │ │ │ │ │ ├── AppDetailsActivityItem.java │ │ │ │ │ │ ├── AppDetailsAppOpItem.java │ │ │ │ │ │ ├── AppDetailsComponentItem.java │ │ │ │ │ │ ├── AppDetailsDefinedPermissionItem.java │ │ │ │ │ │ ├── AppDetailsFeatureItem.java │ │ │ │ │ │ ├── AppDetailsItem.java │ │ │ │ │ │ ├── AppDetailsLibraryItem.java │ │ │ │ │ │ ├── AppDetailsOverlayItem.java │ │ │ │ │ │ ├── AppDetailsPermissionItem.java │ │ │ │ │ │ └── AppDetailsServiceItem.java │ │ │ │ │ ├── dex/ │ │ │ │ │ │ ├── DexClasses.java │ │ │ │ │ │ └── DexUtils.java │ │ │ │ │ ├── editor/ │ │ │ │ │ │ ├── CodeEditorActivity.java │ │ │ │ │ │ ├── CodeEditorFragment.java │ │ │ │ │ │ ├── CodeEditorViewModel.java │ │ │ │ │ │ ├── CodeEditorWidget.java │ │ │ │ │ │ ├── EditorThemes.java │ │ │ │ │ │ └── Languages.java │ │ │ │ │ ├── filters/ │ │ │ │ │ │ ├── AbsExpressionEvaluator.java │ │ │ │ │ │ ├── EditFilterOptionFragment.java │ │ │ │ │ │ ├── EditFiltersDialogFragment.java │ │ │ │ │ │ ├── FilterItem.java │ │ │ │ │ │ ├── FilterableAppInfo.java │ │ │ │ │ │ ├── FilteringUtils.java │ │ │ │ │ │ ├── FinderActivity.java │ │ │ │ │ │ ├── FinderAdapter.java │ │ │ │ │ │ ├── FinderFilterAdapter.java │ │ │ │ │ │ ├── FinderViewModel.java │ │ │ │ │ │ ├── IFilterableAppInfo.java │ │ │ │ │ │ └── options/ │ │ │ │ │ │ ├── ApkSizeOption.java │ │ │ │ │ │ ├── AppLabelOption.java │ │ │ │ │ │ ├── AppTypeOption.java │ │ │ │ │ │ ├── BackupOption.java │ │ │ │ │ │ ├── BloatwareOption.java │ │ │ │ │ │ ├── CacheSizeOption.java │ │ │ │ │ │ ├── CompileSdkOption.java │ │ │ │ │ │ ├── ComponentsOption.java │ │ │ │ │ │ ├── DataSizeOption.java │ │ │ │ │ │ ├── DataUsageOption.java │ │ │ │ │ │ ├── FilterOption.java │ │ │ │ │ │ ├── FilterOptions.java │ │ │ │ │ │ ├── FreezeOption.java │ │ │ │ │ │ ├── InstalledOption.java │ │ │ │ │ │ ├── InstallerOption.java │ │ │ │ │ │ ├── LastUpdateOption.java │ │ │ │ │ │ ├── MinSdkOption.java │ │ │ │ │ │ ├── PackageNameOption.java │ │ │ │ │ │ ├── PermissionsOption.java │ │ │ │ │ │ ├── RunningAppsOption.java │ │ │ │ │ │ ├── ScreenTimeOption.java │ │ │ │ │ │ ├── SignatureOption.java │ │ │ │ │ │ ├── TargetSdkOption.java │ │ │ │ │ │ ├── TimesOpenedOption.java │ │ │ │ │ │ ├── TotalSizeOption.java │ │ │ │ │ │ ├── TrackersOption.java │ │ │ │ │ │ ├── UidOption.java │ │ │ │ │ │ └── VersionNameOption.java │ │ │ │ │ ├── fm/ │ │ │ │ │ │ ├── ContentType2.java │ │ │ │ │ │ ├── FmActivity.java │ │ │ │ │ │ ├── FmAdapter.java │ │ │ │ │ │ ├── FmDrawerItem.java │ │ │ │ │ │ ├── FmFavoritesManager.java │ │ │ │ │ │ ├── FmFragment.java │ │ │ │ │ │ ├── FmItem.java │ │ │ │ │ │ ├── FmListOptions.java │ │ │ │ │ │ ├── FmPathListAdapter.java │ │ │ │ │ │ ├── FmProvider.java │ │ │ │ │ │ ├── FmShortcutInfo.java │ │ │ │ │ │ ├── FmTasks.java │ │ │ │ │ │ ├── FmUtils.java │ │ │ │ │ │ ├── FmViewModel.java │ │ │ │ │ │ ├── FolderShortInfo.java │ │ │ │ │ │ ├── OpenWithActivity.java │ │ │ │ │ │ ├── SharableItems.java │ │ │ │ │ │ ├── dialogs/ │ │ │ │ │ │ │ ├── ChangeFileModeDialogFragment.java │ │ │ │ │ │ │ ├── ChecksumsDialogFragment.java │ │ │ │ │ │ │ ├── FilePropertiesDialogFragment.java │ │ │ │ │ │ │ ├── NewFileDialogFragment.java │ │ │ │ │ │ │ ├── NewFolderDialogFragment.java │ │ │ │ │ │ │ ├── NewSymbolicLinkDialogFragment.java │ │ │ │ │ │ │ ├── OpenWithDialogFragment.java │ │ │ │ │ │ │ └── RenameDialogFragment.java │ │ │ │ │ │ └── icons/ │ │ │ │ │ │ ├── EpubCoverGenerator.java │ │ │ │ │ │ ├── FmIconFetcher.java │ │ │ │ │ │ ├── FmIcons.java │ │ │ │ │ │ └── J2meIconExtractor.java │ │ │ │ │ ├── history/ │ │ │ │ │ │ ├── IJsonSerializer.java │ │ │ │ │ │ ├── JsonDeserializer.java │ │ │ │ │ │ └── ops/ │ │ │ │ │ │ ├── OpHistoryActivity.java │ │ │ │ │ │ ├── OpHistoryItem.java │ │ │ │ │ │ └── OpHistoryManager.java │ │ │ │ │ ├── intercept/ │ │ │ │ │ │ ├── ActivityInterceptor.java │ │ │ │ │ │ ├── AddIntentExtraFragment.java │ │ │ │ │ │ ├── HistoryEditText.java │ │ │ │ │ │ ├── IntentCompat.java │ │ │ │ │ │ └── InterceptorShortcutInfo.java │ │ │ │ │ ├── ipc/ │ │ │ │ │ │ ├── AMService.java │ │ │ │ │ │ ├── BinderHolder.java │ │ │ │ │ │ ├── Container.java │ │ │ │ │ │ ├── FileSystemService.java │ │ │ │ │ │ ├── HiddenAPIs.java │ │ │ │ │ │ ├── LocalServices.java │ │ │ │ │ │ ├── ProxyBinder.java │ │ │ │ │ │ ├── RemoteProcess.java │ │ │ │ │ │ ├── RemoteProcessImpl.java │ │ │ │ │ │ ├── RemoteShellImpl.java │ │ │ │ │ │ ├── RootService.java │ │ │ │ │ │ ├── RootServiceManager.java │ │ │ │ │ │ ├── RootServiceServer.java │ │ │ │ │ │ ├── SerialExecutorService.java │ │ │ │ │ │ ├── ServiceConnectionWrapper.java │ │ │ │ │ │ ├── ServiceNotFoundException.java │ │ │ │ │ │ ├── package.html │ │ │ │ │ │ └── ps/ │ │ │ │ │ │ ├── ProcessEntry.java │ │ │ │ │ │ ├── ProcessUsers.java │ │ │ │ │ │ └── Ps.java │ │ │ │ │ ├── logcat/ │ │ │ │ │ │ ├── AbsLogViewerFragment.java │ │ │ │ │ │ ├── CrazyLoggerService.java │ │ │ │ │ │ ├── LiveLogViewerFragment.java │ │ │ │ │ │ ├── LogFilterAdapter.java │ │ │ │ │ │ ├── LogViewerActivity.java │ │ │ │ │ │ ├── LogViewerRecyclerAdapter.java │ │ │ │ │ │ ├── LogViewerViewModel.java │ │ │ │ │ │ ├── LogcatRecordingService.java │ │ │ │ │ │ ├── RecordLogDialogActivity.java │ │ │ │ │ │ ├── RecordLogDialogFragment.java │ │ │ │ │ │ ├── RecordingWidgetProvider.java │ │ │ │ │ │ ├── SavedLogViewerFragment.java │ │ │ │ │ │ ├── helper/ │ │ │ │ │ │ │ ├── BuildHelper.java │ │ │ │ │ │ │ ├── LogcatHelper.java │ │ │ │ │ │ │ ├── PreferenceHelper.java │ │ │ │ │ │ │ ├── SaveLogHelper.java │ │ │ │ │ │ │ ├── ServiceHelper.java │ │ │ │ │ │ │ └── WidgetHelper.java │ │ │ │ │ │ ├── reader/ │ │ │ │ │ │ │ ├── AbsLogcatReader.java │ │ │ │ │ │ │ ├── LogcatReader.java │ │ │ │ │ │ │ ├── LogcatReaderLoader.java │ │ │ │ │ │ │ ├── MultipleLogcatReader.java │ │ │ │ │ │ │ ├── ScrubberUtils.java │ │ │ │ │ │ │ └── SingleLogcatReader.java │ │ │ │ │ │ └── struct/ │ │ │ │ │ │ ├── LogLine.java │ │ │ │ │ │ ├── SavedLog.java │ │ │ │ │ │ ├── SearchCriteria.java │ │ │ │ │ │ └── SendLogDetails.java │ │ │ │ │ ├── magisk/ │ │ │ │ │ │ ├── MagiskDenyList.java │ │ │ │ │ │ ├── MagiskHide.java │ │ │ │ │ │ ├── MagiskProcess.java │ │ │ │ │ │ └── MagiskUtils.java │ │ │ │ │ ├── main/ │ │ │ │ │ │ ├── ApplicationItem.java │ │ │ │ │ │ ├── MainActivity.java │ │ │ │ │ │ ├── MainBatchOpsHandler.java │ │ │ │ │ │ ├── MainListOptions.java │ │ │ │ │ │ ├── MainRecyclerAdapter.java │ │ │ │ │ │ ├── MainViewModel.java │ │ │ │ │ │ └── SplashActivity.java │ │ │ │ │ ├── misc/ │ │ │ │ │ │ ├── AMExceptionHandler.java │ │ │ │ │ │ ├── AdvancedSearchView.java │ │ │ │ │ │ ├── DeviceInfo.java │ │ │ │ │ │ ├── DeviceInfo2.java │ │ │ │ │ │ ├── HelpActivity.java │ │ │ │ │ │ ├── LabsActivity.java │ │ │ │ │ │ ├── ListOptions.java │ │ │ │ │ │ ├── NoOps.java │ │ │ │ │ │ ├── OidMap.java │ │ │ │ │ │ ├── OsEnvironment.java │ │ │ │ │ │ ├── ScreenLockChecker.java │ │ │ │ │ │ ├── SystemProperties.java │ │ │ │ │ │ ├── VMRuntime.java │ │ │ │ │ │ ├── XposedModuleInfo.java │ │ │ │ │ │ └── gles/ │ │ │ │ │ │ ├── EglCore.java │ │ │ │ │ │ ├── EglSurfaceBase.java │ │ │ │ │ │ ├── GlUtil.java │ │ │ │ │ │ ├── OffscreenSurface.java │ │ │ │ │ │ └── package.html │ │ │ │ │ ├── miui/ │ │ │ │ │ │ └── MiuiVersionInfo.java │ │ │ │ │ ├── oneclickops/ │ │ │ │ │ │ ├── AppOpCount.java │ │ │ │ │ │ ├── BackupTasksDialogFragment.java │ │ │ │ │ │ ├── ClearCacheAppWidget.java │ │ │ │ │ │ ├── ItemCount.java │ │ │ │ │ │ ├── OneClickOpsActivity.java │ │ │ │ │ │ ├── OneClickOpsViewModel.java │ │ │ │ │ │ └── RestoreTasksDialogFragment.java │ │ │ │ │ ├── permission/ │ │ │ │ │ │ ├── DevelopmentPermission.java │ │ │ │ │ │ ├── PermUtils.java │ │ │ │ │ │ ├── Permission.java │ │ │ │ │ │ ├── PermissionException.java │ │ │ │ │ │ ├── ReadOnlyPermission.java │ │ │ │ │ │ └── RuntimePermission.java │ │ │ │ │ ├── profiles/ │ │ │ │ │ │ ├── AddToProfileDialogFragment.java │ │ │ │ │ │ ├── AppsBaseProfileActivity.java │ │ │ │ │ │ ├── AppsFilterProfileActivity.java │ │ │ │ │ │ ├── AppsFragment.java │ │ │ │ │ │ ├── AppsProfileActivity.java │ │ │ │ │ │ ├── AppsProfileViewModel.java │ │ │ │ │ │ ├── ConfFragment.java │ │ │ │ │ │ ├── ConfPreferences.java │ │ │ │ │ │ ├── LogViewerFragment.java │ │ │ │ │ │ ├── NewProfileDialogFragment.java │ │ │ │ │ │ ├── ProfileApplierActivity.java │ │ │ │ │ │ ├── ProfileApplierService.java │ │ │ │ │ │ ├── ProfileLogger.java │ │ │ │ │ │ ├── ProfileManager.java │ │ │ │ │ │ ├── ProfileQueueItem.java │ │ │ │ │ │ ├── ProfileShortcutInfo.java │ │ │ │ │ │ ├── ProfilesActivity.java │ │ │ │ │ │ ├── ProfilesViewModel.java │ │ │ │ │ │ └── struct/ │ │ │ │ │ │ ├── AppsBaseProfile.java │ │ │ │ │ │ ├── AppsFilterProfile.java │ │ │ │ │ │ ├── AppsProfile.java │ │ │ │ │ │ ├── BaseProfile.java │ │ │ │ │ │ └── ProfileApplierResult.java │ │ │ │ │ ├── progress/ │ │ │ │ │ │ ├── NotificationProgressHandler.java │ │ │ │ │ │ ├── ProgressHandler.java │ │ │ │ │ │ └── QueuedProgressHandler.java │ │ │ │ │ ├── rules/ │ │ │ │ │ │ ├── PseudoRules.java │ │ │ │ │ │ ├── RuleType.java │ │ │ │ │ │ ├── RulesExporter.java │ │ │ │ │ │ ├── RulesImporter.java │ │ │ │ │ │ ├── RulesStorageManager.java │ │ │ │ │ │ ├── RulesTypeSelectionDialogFragment.java │ │ │ │ │ │ ├── compontents/ │ │ │ │ │ │ │ ├── ComponentUtils.java │ │ │ │ │ │ │ ├── ComponentsBlocker.java │ │ │ │ │ │ │ └── ExternalComponentsImporter.java │ │ │ │ │ │ └── struct/ │ │ │ │ │ │ ├── AppOpRule.java │ │ │ │ │ │ ├── BatteryOptimizationRule.java │ │ │ │ │ │ ├── ComponentRule.java │ │ │ │ │ │ ├── FreezeRule.java │ │ │ │ │ │ ├── MagiskDenyListRule.java │ │ │ │ │ │ ├── MagiskHideRule.java │ │ │ │ │ │ ├── NetPolicyRule.java │ │ │ │ │ │ ├── NotificationListenerRule.java │ │ │ │ │ │ ├── PermissionRule.java │ │ │ │ │ │ ├── RuleEntry.java │ │ │ │ │ │ ├── SsaidRule.java │ │ │ │ │ │ └── UriGrantRule.java │ │ │ │ │ ├── runner/ │ │ │ │ │ │ ├── NormalShell.java │ │ │ │ │ │ ├── PrivilegedShell.java │ │ │ │ │ │ ├── Runner.java │ │ │ │ │ │ └── RunnerUtils.java │ │ │ │ │ ├── runningapps/ │ │ │ │ │ │ ├── AppProcessItem.java │ │ │ │ │ │ ├── ProcessItem.java │ │ │ │ │ │ ├── ProcessParser.java │ │ │ │ │ │ ├── RunningAppDetails.java │ │ │ │ │ │ ├── RunningAppsActivity.java │ │ │ │ │ │ ├── RunningAppsAdapter.java │ │ │ │ │ │ └── RunningAppsViewModel.java │ │ │ │ │ ├── scanner/ │ │ │ │ │ │ ├── ClassListingFragment.java │ │ │ │ │ │ ├── LibraryInfoDialog.java │ │ │ │ │ │ ├── NativeLibraries.java │ │ │ │ │ │ ├── Pithus.java │ │ │ │ │ │ ├── ScannerActivity.java │ │ │ │ │ │ ├── ScannerFragment.java │ │ │ │ │ │ ├── ScannerViewModel.java │ │ │ │ │ │ ├── SignatureInfo.java │ │ │ │ │ │ ├── TrackerInfoDialog.java │ │ │ │ │ │ ├── VirusTotalDialog.java │ │ │ │ │ │ └── vt/ │ │ │ │ │ │ ├── VirusTotal.java │ │ │ │ │ │ ├── VtAvEngineResult.java │ │ │ │ │ │ ├── VtAvEngineStats.java │ │ │ │ │ │ ├── VtError.java │ │ │ │ │ │ └── VtFileReport.java │ │ │ │ │ ├── self/ │ │ │ │ │ │ ├── BootReceiver.java │ │ │ │ │ │ ├── Migration.java │ │ │ │ │ │ ├── MigrationTask.java │ │ │ │ │ │ ├── Migrations.java │ │ │ │ │ │ ├── SelfPermissions.java │ │ │ │ │ │ ├── SelfUriManager.java │ │ │ │ │ │ ├── filecache/ │ │ │ │ │ │ │ ├── FileCache.java │ │ │ │ │ │ │ └── InternalCacheCleanerService.java │ │ │ │ │ │ ├── imagecache/ │ │ │ │ │ │ │ ├── ImageFileCache.java │ │ │ │ │ │ │ └── ImageLoader.java │ │ │ │ │ │ ├── life/ │ │ │ │ │ │ │ ├── BuildExpiryChecker.java │ │ │ │ │ │ │ └── FundingCampaignChecker.java │ │ │ │ │ │ └── pref/ │ │ │ │ │ │ └── TipsPrefs.java │ │ │ │ │ ├── servermanager/ │ │ │ │ │ │ ├── AssetsUtils.java │ │ │ │ │ │ ├── LocalServer.java │ │ │ │ │ │ ├── LocalServerManager.java │ │ │ │ │ │ ├── ServerConfig.java │ │ │ │ │ │ ├── ServerStatusChangeReceiver.java │ │ │ │ │ │ └── WifiWaitService.java │ │ │ │ │ ├── session/ │ │ │ │ │ │ └── SessionMonitoringService.java │ │ │ │ │ ├── settings/ │ │ │ │ │ │ ├── AboutDeviceFragment.java │ │ │ │ │ │ ├── AboutPreferences.java │ │ │ │ │ │ ├── AdvancedPreferences.java │ │ │ │ │ │ ├── ApkSigningPreferences.java │ │ │ │ │ │ ├── AppearancePreferences.java │ │ │ │ │ │ ├── BackupRestorePreferences.java │ │ │ │ │ │ ├── ChangeLanguageFragment.java │ │ │ │ │ │ ├── FeatureController.java │ │ │ │ │ │ ├── FileManagerPreferences.java │ │ │ │ │ │ ├── ImportExportRulesPreferences.java │ │ │ │ │ │ ├── InstallerPreferences.java │ │ │ │ │ │ ├── LogViewerPreferences.java │ │ │ │ │ │ ├── MainPreferences.java │ │ │ │ │ │ ├── MainPreferencesViewModel.java │ │ │ │ │ │ ├── ModeOfOpsPreference.java │ │ │ │ │ │ ├── Ops.java │ │ │ │ │ │ ├── PreferenceFragment.java │ │ │ │ │ │ ├── Prefs.java │ │ │ │ │ │ ├── PrivacyPreferences.java │ │ │ │ │ │ ├── RulesPreferences.java │ │ │ │ │ │ ├── SecurityAndOpsViewModel.java │ │ │ │ │ │ ├── SettingsActivity.java │ │ │ │ │ │ ├── SettingsDataStore.java │ │ │ │ │ │ ├── TroubleshootingPreferences.java │ │ │ │ │ │ ├── VirusTotalPreferences.java │ │ │ │ │ │ └── crypto/ │ │ │ │ │ │ ├── AESCryptoSelectionDialogFragment.java │ │ │ │ │ │ ├── ECCCryptoSelectionDialogFragment.java │ │ │ │ │ │ ├── ImportExportKeyStoreDialogFragment.java │ │ │ │ │ │ ├── KeyPairGeneratorDialogFragment.java │ │ │ │ │ │ ├── KeyPairImporterDialogFragment.java │ │ │ │ │ │ ├── OpenPgpKeySelectionDialogFragment.java │ │ │ │ │ │ └── RSACryptoSelectionDialogFragment.java │ │ │ │ │ ├── sharedpref/ │ │ │ │ │ │ ├── EditPrefItemFragment.java │ │ │ │ │ │ ├── SharedPrefsActivity.java │ │ │ │ │ │ ├── SharedPrefsUtil.java │ │ │ │ │ │ └── SharedPrefsViewModel.java │ │ │ │ │ ├── shortcut/ │ │ │ │ │ │ ├── CreateShortcutDialogFragment.java │ │ │ │ │ │ └── ShortcutInfo.java │ │ │ │ │ ├── ssaid/ │ │ │ │ │ │ ├── ChangeSsaidDialog.java │ │ │ │ │ │ ├── SettingsState.java │ │ │ │ │ │ ├── SettingsStateV26.java │ │ │ │ │ │ └── SsaidSettings.java │ │ │ │ │ ├── sysconfig/ │ │ │ │ │ │ ├── SysConfigActivity.java │ │ │ │ │ │ ├── SysConfigInfo.java │ │ │ │ │ │ ├── SysConfigType.java │ │ │ │ │ │ ├── SysConfigViewModel.java │ │ │ │ │ │ ├── SysConfigWrapper.java │ │ │ │ │ │ └── SystemConfig.java │ │ │ │ │ ├── terminal/ │ │ │ │ │ │ └── TermActivity.java │ │ │ │ │ ├── types/ │ │ │ │ │ │ ├── ForegroundService.java │ │ │ │ │ │ ├── PackageChangeReceiver.java │ │ │ │ │ │ ├── PackageSizeInfo.java │ │ │ │ │ │ └── UserPackagePair.java │ │ │ │ │ ├── uri/ │ │ │ │ │ │ ├── GrantUriUtils.java │ │ │ │ │ │ └── UriManager.java │ │ │ │ │ ├── usage/ │ │ │ │ │ │ ├── AppUsageActivity.java │ │ │ │ │ │ ├── AppUsageAdapter.java │ │ │ │ │ │ ├── AppUsageDetailsDialog.java │ │ │ │ │ │ ├── AppUsageStatsManager.java │ │ │ │ │ │ ├── AppUsageViewModel.java │ │ │ │ │ │ ├── BarChartView.java │ │ │ │ │ │ ├── DataUsageAppWidget.java │ │ │ │ │ │ ├── IntervalType.java │ │ │ │ │ │ ├── PackageUsageInfo.java │ │ │ │ │ │ ├── ScreenTimeAppWidget.java │ │ │ │ │ │ ├── SortOrder.java │ │ │ │ │ │ ├── TimeInterval.java │ │ │ │ │ │ ├── UsageDataProcessor.java │ │ │ │ │ │ └── UsageUtils.java │ │ │ │ │ ├── users/ │ │ │ │ │ │ ├── Groups.java │ │ │ │ │ │ ├── Owners.java │ │ │ │ │ │ ├── UserInfo.java │ │ │ │ │ │ └── Users.java │ │ │ │ │ ├── utils/ │ │ │ │ │ │ ├── AlphanumComparator.java │ │ │ │ │ │ ├── AppPref.java │ │ │ │ │ │ ├── ArrayUtils.java │ │ │ │ │ │ ├── BetterActivityResult.java │ │ │ │ │ │ ├── BinderShellExecutor.java │ │ │ │ │ │ ├── BitmapRandomizer.java │ │ │ │ │ │ ├── BroadcastUtils.java │ │ │ │ │ │ ├── ClipboardUtils.java │ │ │ │ │ │ ├── ContextUtils.java │ │ │ │ │ │ ├── CpuUtils.java │ │ │ │ │ │ ├── DateUtils.java │ │ │ │ │ │ ├── DigestUtils.java │ │ │ │ │ │ ├── ExUtils.java │ │ │ │ │ │ ├── FileUtils.java │ │ │ │ │ │ ├── FreezeUtils.java │ │ │ │ │ │ ├── HuaweiUtils.java │ │ │ │ │ │ ├── IntegerUtils.java │ │ │ │ │ │ ├── IntentUtils.java │ │ │ │ │ │ ├── JSONUtils.java │ │ │ │ │ │ ├── KeyStoreUtils.java │ │ │ │ │ │ ├── LangUtils.java │ │ │ │ │ │ ├── ListItemCreator.java │ │ │ │ │ │ ├── MiuiUtils.java │ │ │ │ │ │ ├── MotorolaUtils.java │ │ │ │ │ │ ├── MultithreadedExecutor.java │ │ │ │ │ │ ├── NonNullUtils.java │ │ │ │ │ │ ├── NotificationUtils.java │ │ │ │ │ │ ├── PackageUtils.java │ │ │ │ │ │ ├── ParcelFileDescriptorUtil.java │ │ │ │ │ │ ├── ResourceUtil.java │ │ │ │ │ │ ├── RestartUtils.java │ │ │ │ │ │ ├── SAFUtils.java │ │ │ │ │ │ ├── StoragePermission.java │ │ │ │ │ │ ├── StorageUtils.java │ │ │ │ │ │ ├── TarUtils.java │ │ │ │ │ │ ├── TextUtilsCompat.java │ │ │ │ │ │ ├── ThreadUtils.java │ │ │ │ │ │ ├── UIUtils.java │ │ │ │ │ │ ├── Utils.java │ │ │ │ │ │ └── appearance/ │ │ │ │ │ │ ├── AppearanceUtils.java │ │ │ │ │ │ ├── ColorCodes.java │ │ │ │ │ │ └── TypefaceUtil.java │ │ │ │ │ └── viewer/ │ │ │ │ │ ├── ExplorerActivity.java │ │ │ │ │ └── audio/ │ │ │ │ │ ├── AudioMetadata.java │ │ │ │ │ ├── AudioPlayerActivity.java │ │ │ │ │ ├── AudioPlayerDialogFragment.java │ │ │ │ │ ├── AudioPlayerViewModel.java │ │ │ │ │ └── RepeatMode.java │ │ │ │ ├── algo/ │ │ │ │ │ └── AhoCorasick.java │ │ │ │ ├── csv/ │ │ │ │ │ └── CsvWriter.java │ │ │ │ ├── io/ │ │ │ │ │ ├── DirectoryUtils.java │ │ │ │ │ ├── LocalFileOverlay.java │ │ │ │ │ ├── PathAttributesImpl.java │ │ │ │ │ ├── PathContentInfoImpl.java │ │ │ │ │ ├── PathImpl.java │ │ │ │ │ ├── Paths.java │ │ │ │ │ ├── ReadOnlyLocalFile.java │ │ │ │ │ └── fs/ │ │ │ │ │ ├── ApkFileSystem.java │ │ │ │ │ ├── DexFileSystem.java │ │ │ │ │ ├── VirtualFileSystem.java │ │ │ │ │ └── ZipFileSystem.java │ │ │ │ ├── proc/ │ │ │ │ │ ├── ProcFdInfoList.java │ │ │ │ │ ├── ProcFs.java │ │ │ │ │ ├── ProcMappedFiles.java │ │ │ │ │ ├── ProcMemStat.java │ │ │ │ │ ├── ProcMemoryInfo.java │ │ │ │ │ ├── ProcStat.java │ │ │ │ │ ├── ProcStatus.java │ │ │ │ │ └── ProcUidNetStat.java │ │ │ │ └── svg/ │ │ │ │ ├── ParserHelper.java │ │ │ │ ├── SVG.java │ │ │ │ ├── SVGParseException.java │ │ │ │ └── SVGParser.java │ │ │ └── org/ │ │ │ └── apache/ │ │ │ └── commons/ │ │ │ └── compress/ │ │ │ ├── archivers/ │ │ │ │ ├── ArchiveEntry.java │ │ │ │ ├── ArchiveInputStream.java │ │ │ │ ├── ArchiveOutputStream.java │ │ │ │ ├── EntryStreamOffsets.java │ │ │ │ ├── tar/ │ │ │ │ │ ├── TarArchiveEntry.java │ │ │ │ │ ├── TarArchiveInputStream.java │ │ │ │ │ ├── TarArchiveOutputStream.java │ │ │ │ │ ├── TarArchiveSparseEntry.java │ │ │ │ │ ├── TarArchiveSparseZeroInputStream.java │ │ │ │ │ ├── TarArchiveStructSparse.java │ │ │ │ │ ├── TarConstants.java │ │ │ │ │ └── TarUtils.java │ │ │ │ └── zip/ │ │ │ │ ├── CharsetAccessor.java │ │ │ │ ├── NioZipEncoding.java │ │ │ │ ├── ZipEncoding.java │ │ │ │ └── ZipEncodingHelper.java │ │ │ ├── compressors/ │ │ │ │ ├── CompressorInputStream.java │ │ │ │ ├── CompressorOutputStream.java │ │ │ │ ├── FileNameUtil.java │ │ │ │ ├── bzip2/ │ │ │ │ │ ├── BZip2CompressorInputStream.java │ │ │ │ │ ├── BZip2CompressorOutputStream.java │ │ │ │ │ ├── BZip2Constants.java │ │ │ │ │ ├── BZip2Utils.java │ │ │ │ │ ├── BlockSort.java │ │ │ │ │ ├── CRC.java │ │ │ │ │ └── Rand.java │ │ │ │ └── gzip/ │ │ │ │ ├── GzipCompressorInputStream.java │ │ │ │ ├── GzipCompressorOutputStream.java │ │ │ │ ├── GzipParameters.java │ │ │ │ └── GzipUtils.java │ │ │ └── utils/ │ │ │ ├── ArchiveUtils.java │ │ │ ├── BitInputStream.java │ │ │ ├── BoundedInputStream.java │ │ │ ├── ByteUtils.java │ │ │ ├── CharsetNames.java │ │ │ ├── CloseShieldFilterInputStream.java │ │ │ ├── CountingInputStream.java │ │ │ ├── CountingOutputStream.java │ │ │ ├── FixedLengthBlockOutputStream.java │ │ │ ├── IOUtils.java │ │ │ └── InputStreamStatistics.java │ │ └── res/ │ │ ├── animator/ │ │ │ ├── enter_from_left.xml │ │ │ ├── enter_from_right.xml │ │ │ ├── exit_from_left.xml │ │ │ └── exit_from_right.xml │ │ ├── drawable/ │ │ │ ├── app_widget_background.xml │ │ │ ├── badge_background.xml │ │ │ ├── circle_background.xml │ │ │ ├── circle_with_padding.xml │ │ │ ├── dashed_border.xml │ │ │ ├── ic_add.xml │ │ │ ├── ic_android.xml │ │ │ ├── ic_archive.xml │ │ │ ├── ic_arrow_outward.xml │ │ │ ├── ic_audio_file.xml │ │ │ ├── ic_autorenew.xml │ │ │ ├── ic_backup_restore.xml │ │ │ ├── ic_battery_plus.xml │ │ │ ├── ic_block.xml │ │ │ ├── ic_book.xml │ │ │ ├── ic_brush.xml │ │ │ ├── ic_calendar_month.xml │ │ │ ├── ic_cctv_off.xml │ │ │ ├── ic_charity.xml │ │ │ ├── ic_check_circle.xml │ │ │ ├── ic_clear_cache.xml │ │ │ ├── ic_clear_data.xml │ │ │ ├── ic_code.xml │ │ │ ├── ic_contact_page.xml │ │ │ ├── ic_content_copy.xml │ │ │ ├── ic_content_cut.xml │ │ │ ├── ic_content_duplicate.xml │ │ │ ├── ic_content_paste.xml │ │ │ ├── ic_content_save.xml │ │ │ ├── ic_cursor_default_click.xml │ │ │ ├── ic_data_usage.xml │ │ │ ├── ic_database.xml │ │ │ ├── ic_drag.xml │ │ │ ├── ic_edit.xml │ │ │ ├── ic_email.xml │ │ │ ├── ic_exodusprivacy.xml │ │ │ ├── ic_expand_less.xml │ │ │ ├── ic_expand_more.xml │ │ │ ├── ic_eye.xml │ │ │ ├── ic_fast_forward.xml │ │ │ ├── ic_fast_rewind.xml │ │ │ ├── ic_file.xml │ │ │ ├── ic_file_copy.xml │ │ │ ├── ic_file_document.xml │ │ │ ├── ic_file_document_multiple.xml │ │ │ ├── ic_file_export.xml │ │ │ ├── ic_file_import.xml │ │ │ ├── ic_file_plus.xml │ │ │ ├── ic_file_replace.xml │ │ │ ├── ic_file_undo.xml │ │ │ ├── ic_filter_list.xml │ │ │ ├── ic_filter_menu.xml │ │ │ ├── ic_find_and_replace.xml │ │ │ ├── ic_find_and_replace_all.xml │ │ │ ├── ic_flag.xml │ │ │ ├── ic_fold_vertical.xml │ │ │ ├── ic_folder.xml │ │ │ ├── ic_font_download.xml │ │ │ ├── ic_form_textbox.xml │ │ │ ├── ic_frost_aurorastore.xml │ │ │ ├── ic_frost_fdroid.xml │ │ │ ├── ic_frost_termux.xml │ │ │ ├── ic_get_app.xml │ │ │ ├── ic_hammer_wrench.xml │ │ │ ├── ic_history.xml │ │ │ ├── ic_image.xml │ │ │ ├── ic_information_circle.xml │ │ │ ├── ic_launcher_fm_foreground.xml │ │ │ ├── ic_launcher_foreground.xml │ │ │ ├── ic_link.xml │ │ │ ├── ic_liquid_spot_off.xml │ │ │ ├── ic_list_status.xml │ │ │ ├── ic_lock.xml │ │ │ ├── ic_menu.xml │ │ │ ├── ic_next.xml │ │ │ ├── ic_open_in_new.xml │ │ │ ├── ic_package.xml │ │ │ ├── ic_palette_swatch.xml │ │ │ ├── ic_pause.xml │ │ │ ├── ic_pdf_file.xml │ │ │ ├── ic_phone_android.xml │ │ │ ├── ic_play_arrow.xml │ │ │ ├── ic_power_settings.xml │ │ │ ├── ic_presentation.xml │ │ │ ├── ic_previous.xml │ │ │ ├── ic_pulse.xml │ │ │ ├── ic_record_rec.xml │ │ │ ├── ic_redo.xml │ │ │ ├── ic_refresh.xml │ │ │ ├── ic_repeat.xml │ │ │ ├── ic_repeat_off.xml │ │ │ ├── ic_repeat_one.xml │ │ │ ├── ic_replay.xml │ │ │ ├── ic_restore.xml │ │ │ ├── ic_run_fast.xml │ │ │ ├── ic_security.xml │ │ │ ├── ic_security_network.xml │ │ │ ├── ic_settings.xml │ │ │ ├── ic_share.xml │ │ │ ├── ic_shield_key.xml │ │ │ ├── ic_shortcut_record.xml │ │ │ ├── ic_snowflake.xml │ │ │ ├── ic_snowflake_off.xml │ │ │ ├── ic_sort.xml │ │ │ ├── ic_splash_logo.xml │ │ │ ├── ic_star.xml │ │ │ ├── ic_star_outline.xml │ │ │ ├── ic_stop.xml │ │ │ ├── ic_sync.xml │ │ │ ├── ic_tab.xml │ │ │ ├── ic_table.xml │ │ │ ├── ic_touch_app.xml │ │ │ ├── ic_transit_connection.xml │ │ │ ├── ic_translate.xml │ │ │ ├── ic_trash_can.xml │ │ │ ├── ic_tune.xml │ │ │ ├── ic_undo.xml │ │ │ ├── ic_unlock.xml │ │ │ ├── ic_video_file.xml │ │ │ ├── ic_view_agenda.xml │ │ │ ├── ic_view_list.xml │ │ │ ├── ic_vt.xml │ │ │ ├── ic_wifi.xml │ │ │ ├── ic_wrap_text.xml │ │ │ ├── ic_zip_disk.xml │ │ │ └── ripple_background_24.xml │ │ ├── drawable-anydpi-v24/ │ │ │ └── ic_default_notification.xml │ │ ├── layout/ │ │ │ ├── activity_app_details.xml │ │ │ ├── activity_app_usage.xml │ │ │ ├── activity_apps_profile.xml │ │ │ ├── activity_audio_player.xml │ │ │ ├── activity_auth_management.xml │ │ │ ├── activity_authentication.xml │ │ │ ├── activity_batch_ops_results.xml │ │ │ ├── activity_code_editor.xml │ │ │ ├── activity_debloater.xml │ │ │ ├── activity_finder.xml │ │ │ ├── activity_fm.xml │ │ │ ├── activity_help.xml │ │ │ ├── activity_interceptor.xml │ │ │ ├── activity_labs.xml │ │ │ ├── activity_logcat.xml │ │ │ ├── activity_main.xml │ │ │ ├── activity_one_click_ops.xml │ │ │ ├── activity_op_history.xml │ │ │ ├── activity_profiles.xml │ │ │ ├── activity_running_apps.xml │ │ │ ├── activity_settings.xml │ │ │ ├── activity_settings_dual_pane.xml │ │ │ ├── activity_shared_prefs.xml │ │ │ ├── activity_sys_config.xml │ │ │ ├── activity_term.xml │ │ │ ├── app_widget_clear_cache.xml │ │ │ ├── app_widget_data_usage_small.xml │ │ │ ├── app_widget_refresh_button.xml │ │ │ ├── app_widget_screen_time.xml │ │ │ ├── app_widget_screen_time_large.xml │ │ │ ├── app_widget_screen_time_small.xml │ │ │ ├── appbar.xml │ │ │ ├── dialog_app_usage_details.xml │ │ │ ├── dialog_audio_player.xml │ │ │ ├── dialog_backup_restore.xml │ │ │ ├── dialog_backup_tasks.xml │ │ │ ├── dialog_bloatware_details.xml │ │ │ ├── dialog_certificate_generator.xml │ │ │ ├── dialog_change_file_mode.xml │ │ │ ├── dialog_checksums.xml │ │ │ ├── dialog_create_shortcut.xml │ │ │ ├── dialog_debloater_list_options.xml │ │ │ ├── dialog_dexopt.xml │ │ │ ├── dialog_disclaimer.xml │ │ │ ├── dialog_edit_filter_item.xml │ │ │ ├── dialog_edit_filter_option.xml │ │ │ ├── dialog_edit_pref_item.xml │ │ │ ├── dialog_file_properties.xml │ │ │ ├── dialog_icon_picker.xml │ │ │ ├── dialog_installer.xml │ │ │ ├── dialog_installer_options.xml │ │ │ ├── dialog_key_pair_importer.xml │ │ │ ├── dialog_keystore_password.xml │ │ │ ├── dialog_list_options.xml │ │ │ ├── dialog_new_file.xml │ │ │ ├── dialog_new_symlink.xml │ │ │ ├── dialog_open_with.xml │ │ │ ├── dialog_profile_backup_restore.xml │ │ │ ├── dialog_progress.xml │ │ │ ├── dialog_progress2.xml │ │ │ ├── dialog_progress_circular.xml │ │ │ ├── dialog_rename.xml │ │ │ ├── dialog_restore_tasks.xml │ │ │ ├── dialog_running_app_details.xml │ │ │ ├── dialog_searchable_multi_choice.xml │ │ │ ├── dialog_searchby.xml │ │ │ ├── dialog_send_log.xml │ │ │ ├── dialog_set_apk_format.xml │ │ │ ├── dialog_ssaid_info.xml │ │ │ ├── dialog_whats_new.xml │ │ │ ├── fragment_class_lister.xml │ │ │ ├── fragment_code_editor.xml │ │ │ ├── fragment_container.xml │ │ │ ├── fragment_dialog_backup.xml │ │ │ ├── fragment_dialog_restore_multiple.xml │ │ │ ├── fragment_dialog_restore_single.xml │ │ │ ├── fragment_fm.xml │ │ │ ├── fragment_log_viewer.xml │ │ │ ├── fragment_logcat.xml │ │ │ ├── fragment_mode_of_ops.xml │ │ │ ├── fragment_scanner.xml │ │ │ ├── header_running_apps_memory_info.xml │ │ │ ├── item_app_details_appop.xml │ │ │ ├── item_app_details_overlay.xml │ │ │ ├── item_app_details_perm.xml │ │ │ ├── item_app_details_primary.xml │ │ │ ├── item_app_details_secondary.xml │ │ │ ├── item_app_details_signature.xml │ │ │ ├── item_app_details_tertiary.xml │ │ │ ├── item_app_info_action.xml │ │ │ ├── item_app_usage.xml │ │ │ ├── item_app_usage_header.xml │ │ │ ├── item_bloatware_details.xml │ │ │ ├── item_changelog_header.xml │ │ │ ├── item_changelog_item.xml │ │ │ ├── item_checkbox.xml │ │ │ ├── item_chip.xml │ │ │ ├── item_debloater.xml │ │ │ ├── item_finder.xml │ │ │ ├── item_fm.xml │ │ │ ├── item_fm_drawer.xml │ │ │ ├── item_icon_title_subtitle.xml │ │ │ ├── item_logcat.xml │ │ │ ├── item_main.xml │ │ │ ├── item_op_history.xml │ │ │ ├── item_path.xml │ │ │ ├── item_right_standalone_action.xml │ │ │ ├── item_right_summary.xml │ │ │ ├── item_running_app.xml │ │ │ ├── item_shared_lib.xml │ │ │ ├── item_switch.xml │ │ │ ├── item_sys_config.xml │ │ │ ├── item_text_input_layout_monospace.xml │ │ │ ├── item_text_view.xml │ │ │ ├── item_title_action.xml │ │ │ ├── item_whats_new.xml │ │ │ ├── pager_app_details.xml │ │ │ ├── pager_app_info.xml │ │ │ ├── widget_recording.xml │ │ │ └── window_activity_tracker.xml │ │ ├── menu/ │ │ │ ├── activity_activity_interceptor_actions.xml │ │ │ ├── activity_app_explorer_selection_actions.xml │ │ │ ├── activity_app_usage_actions.xml │ │ │ ├── activity_batch_ops_results_actions.xml │ │ │ ├── activity_code_editor_actions.xml │ │ │ ├── activity_debloater_actions.xml │ │ │ ├── activity_debloater_selection_actions.xml │ │ │ ├── activity_fm_actions.xml │ │ │ ├── activity_help_actions.xml │ │ │ ├── activity_main_actions.xml │ │ │ ├── activity_main_selection_actions.xml │ │ │ ├── activity_profile_navigation.xml │ │ │ ├── activity_profiles_actions.xml │ │ │ ├── activity_profiles_popup_actions.xml │ │ │ ├── activity_running_apps_actions.xml │ │ │ ├── activity_running_apps_popup_actions.xml │ │ │ ├── activity_scanner.xml │ │ │ ├── activity_shared_prefs_actions.xml │ │ │ ├── fragment_app_details_app_ops_actions.xml │ │ │ ├── fragment_app_details_components_actions.xml │ │ │ ├── fragment_app_details_components_selection_actions.xml │ │ │ ├── fragment_app_details_overlay_actions.xml │ │ │ ├── fragment_app_details_permissions_actions.xml │ │ │ ├── fragment_app_details_refresh_actions.xml │ │ │ ├── fragment_app_info_actions.xml │ │ │ ├── fragment_class_lister_actions.xml │ │ │ ├── fragment_fm_item_actions.xml │ │ │ ├── fragment_fm_selection_actions.xml │ │ │ ├── fragment_fm_speed_dial.xml │ │ │ ├── fragment_live_log_viewer_actions.xml │ │ │ ├── fragment_logcat_selection_actions.xml │ │ │ ├── fragment_profile_apps_actions.xml │ │ │ ├── fragment_saved_log_viewer_actions.xml │ │ │ └── view_advanced_search_type_selections.xml │ │ ├── mipmap-anydpi-v26/ │ │ │ ├── ic_banner.xml │ │ │ ├── ic_launcher.xml │ │ │ ├── ic_launcher_fm.xml │ │ │ ├── ic_launcher_fm_round.xml │ │ │ └── ic_launcher_round.xml │ │ ├── raw/ │ │ │ └── changelog.xml │ │ ├── values/ │ │ │ ├── arrays.xml │ │ │ ├── attrs.xml │ │ │ ├── colors.xml │ │ │ ├── dimens.xml │ │ │ ├── disclaimer.xml │ │ │ ├── ic_banner_background.xml │ │ │ ├── ic_launcher_background.xml │ │ │ ├── ic_launcher_fm_background.xml │ │ │ ├── libs.xml │ │ │ ├── native_libs.xml │ │ │ ├── strings.xml │ │ │ ├── styles.xml │ │ │ ├── trackers.xml │ │ │ └── word_list.xml │ │ ├── values-ar/ │ │ │ ├── disclaimer.xml │ │ │ └── strings.xml │ │ ├── values-ar-rSA/ │ │ │ ├── disclaimer.xml │ │ │ └── strings.xml │ │ ├── values-az/ │ │ │ ├── disclaimer.xml │ │ │ └── strings.xml │ │ ├── values-ban/ │ │ │ ├── disclaimer.xml │ │ │ └── strings.xml │ │ ├── values-be/ │ │ │ ├── disclaimer.xml │ │ │ └── strings.xml │ │ ├── values-bn-rBD/ │ │ │ ├── disclaimer.xml │ │ │ └── strings.xml │ │ ├── values-cs/ │ │ │ └── disclaimer.xml │ │ ├── values-cs-rCZ/ │ │ │ └── strings.xml │ │ ├── values-da/ │ │ │ ├── disclaimer.xml │ │ │ └── strings.xml │ │ ├── values-de/ │ │ │ ├── disclaimer.xml │ │ │ └── strings.xml │ │ ├── values-el/ │ │ │ ├── disclaimer.xml │ │ │ └── strings.xml │ │ ├── values-eo/ │ │ │ ├── disclaimer.xml │ │ │ └── strings.xml │ │ ├── values-es/ │ │ │ └── disclaimer.xml │ │ ├── values-es-rES/ │ │ │ └── strings.xml │ │ ├── values-fa/ │ │ │ ├── disclaimer.xml │ │ │ └── strings.xml │ │ ├── values-fr/ │ │ │ ├── disclaimer.xml │ │ │ └── strings.xml │ │ ├── values-hi/ │ │ │ ├── disclaimer.xml │ │ │ └── strings.xml │ │ ├── values-hu/ │ │ │ ├── disclaimer.xml │ │ │ └── strings.xml │ │ ├── values-in-rID/ │ │ │ ├── disclaimer.xml │ │ │ └── strings.xml │ │ ├── values-it-rIT/ │ │ │ ├── disclaimer.xml │ │ │ └── strings.xml │ │ ├── values-iw/ │ │ │ ├── disclaimer.xml │ │ │ └── strings.xml │ │ ├── values-ja/ │ │ │ ├── disclaimer.xml │ │ │ └── strings.xml │ │ ├── values-ko/ │ │ │ ├── disclaimer.xml │ │ │ └── strings.xml │ │ ├── values-lv/ │ │ │ ├── disclaimer.xml │ │ │ └── strings.xml │ │ ├── values-nb-rNO/ │ │ │ ├── disclaimer.xml │ │ │ └── strings.xml │ │ ├── values-night-v31/ │ │ │ └── styles.xml │ │ ├── values-nl/ │ │ │ ├── disclaimer.xml │ │ │ └── strings.xml │ │ ├── values-pl/ │ │ │ ├── disclaimer.xml │ │ │ └── strings.xml │ │ ├── values-pt/ │ │ │ ├── disclaimer.xml │ │ │ └── strings.xml │ │ ├── values-pt-rBR/ │ │ │ ├── disclaimer.xml │ │ │ └── strings.xml │ │ ├── values-ro/ │ │ │ ├── disclaimer.xml │ │ │ └── strings.xml │ │ ├── values-ru-rRU/ │ │ │ ├── disclaimer.xml │ │ │ └── strings.xml │ │ ├── values-si/ │ │ │ ├── disclaimer.xml │ │ │ └── strings.xml │ │ ├── values-sq/ │ │ │ ├── disclaimer.xml │ │ │ └── strings.xml │ │ ├── values-sv/ │ │ │ ├── disclaimer.xml │ │ │ └── strings.xml │ │ ├── values-tr-rTR/ │ │ │ ├── disclaimer.xml │ │ │ └── strings.xml │ │ ├── values-uk/ │ │ │ └── disclaimer.xml │ │ ├── values-uk-rUA/ │ │ │ └── strings.xml │ │ ├── values-v31/ │ │ │ └── styles.xml │ │ ├── values-vi/ │ │ │ ├── disclaimer.xml │ │ │ └── strings.xml │ │ ├── values-zh-rCN/ │ │ │ ├── disclaimer.xml │ │ │ └── strings.xml │ │ ├── values-zh-rTW/ │ │ │ ├── disclaimer.xml │ │ │ └── strings.xml │ │ └── xml/ │ │ ├── accessibility_service_config.xml │ │ ├── app_widget_info_clear_cache.xml │ │ ├── app_widget_info_data_usage.xml │ │ ├── app_widget_info_screen_time.xml │ │ ├── backup_rules.xml │ │ ├── full_backup_rules.xml │ │ ├── locales_config.xml │ │ ├── network_security_config.xml │ │ ├── preferences_about.xml │ │ ├── preferences_advanced.xml │ │ ├── preferences_appearance.xml │ │ ├── preferences_backup_restore.xml │ │ ├── preferences_file_manager.xml │ │ ├── preferences_installer.xml │ │ ├── preferences_log_viewer.xml │ │ ├── preferences_main.xml │ │ ├── preferences_privacy.xml │ │ ├── preferences_profile_config.xml │ │ ├── preferences_rules.xml │ │ ├── preferences_rules_import_export.xml │ │ ├── preferences_signature.xml │ │ ├── preferences_troubleshooting.xml │ │ ├── preferences_virus_total.xml │ │ └── recording_widget_info.xml │ └── test/ │ ├── java/ │ │ ├── android/ │ │ │ ├── os/ │ │ │ │ └── UserHandleHidden.java │ │ │ └── system/ │ │ │ ├── OsHidden.java │ │ │ └── StructPasswd.java │ │ ├── androidx/ │ │ │ └── documentfile/ │ │ │ └── provider/ │ │ │ ├── DexDocumentFileTest.java │ │ │ └── ZipDocumentFileTest.java │ │ ├── io/ │ │ │ └── github/ │ │ │ └── muntashirakon/ │ │ │ ├── AppManager/ │ │ │ │ ├── apk/ │ │ │ │ │ ├── dexopt/ │ │ │ │ │ │ └── DexOptOptionsTest.java │ │ │ │ │ └── parser/ │ │ │ │ │ ├── AndroidBinXmlDecoderTest.java │ │ │ │ │ ├── AndroidBinXmlEncoderTest.java │ │ │ │ │ └── ManifestParserTest.java │ │ │ │ ├── backup/ │ │ │ │ │ ├── BackupDataDirectoryInfoTest.java │ │ │ │ │ ├── BackupManagerTest.java │ │ │ │ │ ├── BackupUtilsTest.java │ │ │ │ │ ├── adb/ │ │ │ │ │ │ ├── AndroidBackupCreatorTest.java │ │ │ │ │ │ └── AndroidBackupExtractorTest.java │ │ │ │ │ └── convert/ │ │ │ │ │ ├── OABConverterTest.java │ │ │ │ │ ├── SBConverterTest.java │ │ │ │ │ └── TBConverterTest.java │ │ │ │ ├── batchops/ │ │ │ │ │ ├── BatchQueueItemTest.java │ │ │ │ │ └── struct/ │ │ │ │ │ ├── BatchAppOpsOptionsTest.java │ │ │ │ │ ├── BatchBackupImportOptionsTest.java │ │ │ │ │ └── BatchDexOptOptionsTest.java │ │ │ │ ├── compat/ │ │ │ │ │ └── ActivityManagerCompatTest.java │ │ │ │ ├── filters/ │ │ │ │ │ └── FilterItemTest.java │ │ │ │ ├── fm/ │ │ │ │ │ └── FmProviderTest.java │ │ │ │ ├── rules/ │ │ │ │ │ ├── PseudoRulesTest.java │ │ │ │ │ ├── compontents/ │ │ │ │ │ │ └── ComponentUtilsTest.java │ │ │ │ │ └── struct/ │ │ │ │ │ └── RuleEntryTest.java │ │ │ │ ├── runningapps/ │ │ │ │ │ └── ProcessParserTest.java │ │ │ │ ├── scanner/ │ │ │ │ │ └── vt/ │ │ │ │ │ └── VirusTotalTest.java │ │ │ │ ├── ssaid/ │ │ │ │ │ └── SsaidSettingsTest.java │ │ │ │ └── utils/ │ │ │ │ ├── BitmapRandomizerTest.java │ │ │ │ ├── RoboUtils.java │ │ │ │ └── TarUtilsTest.java │ │ │ ├── csv/ │ │ │ │ └── CsvWriterTest.java │ │ │ ├── io/ │ │ │ │ ├── PathTest.java │ │ │ │ ├── PathsTest.java │ │ │ │ ├── ShadowLocalFileOverlay.java │ │ │ │ ├── SplitInputStreamTest.java │ │ │ │ ├── SplitOutputStreamTest.java │ │ │ │ └── fs/ │ │ │ │ └── ZipFileSystemTest.java │ │ │ └── test/ │ │ │ └── shadows/ │ │ │ ├── OsConstantsValues.java │ │ │ ├── ShadowBackupDataDirectoryInfo.java │ │ │ ├── ShadowDeviceIdleManagerCompat.java │ │ │ ├── ShadowOsConstants.java │ │ │ ├── ShadowOwners.java │ │ │ ├── ShadowPackageInstallerCompat.java │ │ │ └── ShadowPackageManagerCompat.java │ │ └── org/ │ │ └── apache/ │ │ └── commons/ │ │ └── compress/ │ │ ├── archivers/ │ │ │ └── tar/ │ │ │ ├── TarArchiveInputStreamTest.java │ │ │ └── TarArchiveOutputStreamTest.java │ │ └── compressors/ │ │ ├── bzip2/ │ │ │ ├── BZip2CompressorInputStreamTest.java │ │ │ └── BZip2CompressorOutputStreamTest.java │ │ └── gzip/ │ │ ├── GzipCompressorInputStreamTest.java │ │ └── GzipCompressorOutputStreamTest.java │ └── resources/ │ ├── AppManager_v2.5.22.apks │ ├── AppManager_v2.5.22.apks.0 │ ├── AppManager_v2.5.22.apks.1 │ ├── AppManager_v2.5.22.apks.2 │ ├── AppManager_v2.5.22.apks.3 │ ├── AppManager_v2.5.22.apks.4 │ ├── AppManager_v2.5.22.apks.5 │ ├── AppManager_v2.5.22.apks.6 │ ├── AppManager_v2.5.22.apks.7 │ ├── AppManager_v2.5.22.apks.tar.0 │ ├── AppManager_v2.5.22.apks.tar.1 │ ├── AppManager_v2.5.22.apks.tar.bz2 │ ├── AppManager_v2.5.22.apks.tar.bz2.0 │ ├── AppManager_v2.5.22.apks.tar.bz2.1 │ ├── AppManager_v2.5.22.apks.tar.gz.0 │ ├── AppManager_v2.5.22.apks.tar.gz.1 │ ├── SwiftBackup/ │ │ ├── ademar.textlauncher.app │ │ ├── ademar.textlauncher.xml │ │ ├── com.google.android.samples.dynamicfeatures.ondemand.app │ │ ├── com.google.android.samples.dynamicfeatures.ondemand.splits │ │ ├── com.google.android.samples.dynamicfeatures.ondemand.xml │ │ ├── com.test.app.app │ │ ├── com.test.app.exp │ │ ├── com.test.app.xml │ │ ├── dnsfilter.android.app │ │ ├── dnsfilter.android.extdat │ │ ├── dnsfilter.android.xml │ │ ├── org.billthefarmer.editor.app │ │ └── org.billthefarmer.editor.xml │ ├── TitaniumBackup/ │ │ ├── ademar.textlauncher-20210530-111646.properties │ │ ├── ademar.textlauncher-7016ac07c52a556afd2eed992b976255.apk.bz2 │ │ ├── ca.cmetcalfe.locationshare-20210529-164219.properties │ │ ├── ca.cmetcalfe.locationshare-20210529-164219.tar.bz2 │ │ ├── dnsfilter.android-20210529-164214.properties │ │ ├── dnsfilter.android-20210529-164214.tar.bz2 │ │ ├── dnsfilter.android-6df217d8c3415e7664cb4942b2145ec1.apk.bz2 │ │ ├── org.billthefarmer.editor-20210529-164210.properties │ │ ├── org.billthefarmer.editor-20210529-164210.tar.bz2 │ │ └── org.billthefarmer.editor-71ecafd32cda1d2975f8aa3cbacdc540.apk.bz2 │ ├── backups/ │ │ ├── adb/ │ │ │ ├── all_data.ab │ │ │ ├── data0.tar.gz.0 │ │ │ ├── data1.tar.gz.0 │ │ │ ├── data2.tar.gz.0 │ │ │ ├── key_value.ab │ │ │ └── source.tar.gz.0 │ │ └── v4/ │ │ └── AppManager/ │ │ └── dnsfilter.android/ │ │ ├── 0/ │ │ │ ├── checksums.txt │ │ │ ├── data0.tar.gz.0 │ │ │ ├── data1.tar.gz.0 │ │ │ ├── meta_v2.am.json │ │ │ ├── misc.am.tsv │ │ │ └── source.tar.gz.0 │ │ └── 0_test/ │ │ ├── checksums.txt │ │ ├── data0.tar.gz.0 │ │ ├── data1.tar.gz.0 │ │ ├── meta_v2.am.json │ │ ├── misc.am.tsv │ │ └── source.tar.gz.0 │ ├── dumpsys_app_processes.txt │ ├── dumpsys_services.txt │ ├── ifw/ │ │ └── sample.package.xml │ ├── oandbackups/ │ │ ├── ademar.textlauncher/ │ │ │ ├── ademar.textlauncher.log │ │ │ └── classes.dex │ │ ├── ca.cmetcalfe.locationshare/ │ │ │ └── ca.cmetcalfe.locationshare.log │ │ ├── dnsfilter.android/ │ │ │ └── dnsfilter.android.log │ │ └── org.billthefarmer.editor/ │ │ └── org.billthefarmer.editor.log │ ├── plain.txt │ ├── prefixed/ │ │ ├── prefixed_exclude.txt │ │ └── prefixed_include.txt │ ├── proc/ │ │ ├── 11/ │ │ │ ├── attr/ │ │ │ │ ├── current │ │ │ │ ├── exec │ │ │ │ ├── fscreate │ │ │ │ ├── keycreate │ │ │ │ ├── prev │ │ │ │ └── sockcreate │ │ │ ├── cgroup │ │ │ ├── cmdline │ │ │ ├── comm │ │ │ ├── coredump_filter │ │ │ ├── cpuset │ │ │ ├── limits │ │ │ ├── oom_adj │ │ │ ├── oom_score │ │ │ ├── oom_score_adj │ │ │ ├── sched │ │ │ ├── sched_group_id │ │ │ ├── sched_init_task_load │ │ │ ├── sched_wake_up_idle │ │ │ ├── schedstat │ │ │ ├── stat │ │ │ ├── statm │ │ │ ├── status │ │ │ ├── timerslack_ns │ │ │ └── wchan │ │ ├── 1101/ │ │ │ ├── attr/ │ │ │ │ ├── current │ │ │ │ ├── exec │ │ │ │ ├── fscreate │ │ │ │ ├── keycreate │ │ │ │ ├── prev │ │ │ │ └── sockcreate │ │ │ ├── cgroup │ │ │ ├── cmdline │ │ │ ├── comm │ │ │ ├── coredump_filter │ │ │ ├── cpuset │ │ │ ├── limits │ │ │ ├── oom_adj │ │ │ ├── oom_score │ │ │ ├── oom_score_adj │ │ │ ├── sched │ │ │ ├── sched_group_id │ │ │ ├── sched_init_task_load │ │ │ ├── sched_wake_up_idle │ │ │ ├── schedstat │ │ │ ├── stat │ │ │ ├── statm │ │ │ ├── status │ │ │ ├── timerslack_ns │ │ │ └── wchan │ │ ├── 1129/ │ │ │ ├── attr/ │ │ │ │ ├── current │ │ │ │ ├── exec │ │ │ │ ├── fscreate │ │ │ │ ├── keycreate │ │ │ │ ├── prev │ │ │ │ └── sockcreate │ │ │ ├── cgroup │ │ │ ├── cmdline │ │ │ ├── comm │ │ │ ├── coredump_filter │ │ │ ├── cpuset │ │ │ ├── limits │ │ │ ├── oom_adj │ │ │ ├── oom_score │ │ │ ├── oom_score_adj │ │ │ ├── sched │ │ │ ├── sched_group_id │ │ │ ├── sched_init_task_load │ │ │ ├── sched_wake_up_idle │ │ │ ├── schedstat │ │ │ ├── stat │ │ │ ├── statm │ │ │ ├── status │ │ │ ├── timerslack_ns │ │ │ └── wchan │ │ ├── 11547/ │ │ │ ├── attr/ │ │ │ │ ├── current │ │ │ │ ├── exec │ │ │ │ ├── fscreate │ │ │ │ ├── keycreate │ │ │ │ ├── prev │ │ │ │ └── sockcreate │ │ │ ├── cgroup │ │ │ ├── cmdline │ │ │ ├── comm │ │ │ ├── coredump_filter │ │ │ ├── cpuset │ │ │ ├── limits │ │ │ ├── oom_adj │ │ │ ├── oom_score │ │ │ ├── oom_score_adj │ │ │ ├── sched │ │ │ ├── sched_group_id │ │ │ ├── sched_init_task_load │ │ │ ├── sched_wake_up_idle │ │ │ ├── schedstat │ │ │ ├── stat │ │ │ ├── statm │ │ │ ├── status │ │ │ ├── timerslack_ns │ │ │ └── wchan │ │ ├── 123/ │ │ │ ├── attr/ │ │ │ │ ├── current │ │ │ │ ├── exec │ │ │ │ ├── fscreate │ │ │ │ ├── keycreate │ │ │ │ ├── prev │ │ │ │ └── sockcreate │ │ │ ├── cgroup │ │ │ ├── cmdline │ │ │ ├── comm │ │ │ ├── coredump_filter │ │ │ ├── cpuset │ │ │ ├── limits │ │ │ ├── oom_adj │ │ │ ├── oom_score │ │ │ ├── oom_score_adj │ │ │ ├── sched │ │ │ ├── sched_group_id │ │ │ ├── sched_init_task_load │ │ │ ├── sched_wake_up_idle │ │ │ ├── schedstat │ │ │ ├── stat │ │ │ ├── statm │ │ │ ├── status │ │ │ ├── timerslack_ns │ │ │ └── wchan │ │ └── uptime │ ├── raw/ │ │ ├── exclude.txt │ │ └── include.txt │ ├── robolectric.properties │ └── xml/ │ ├── HMS_Core_Android_Manifest.bin.xml │ ├── HMS_Core_Android_Manifest.man.xml │ ├── settings_ssaid.xml │ ├── test_layout.bin.xml │ └── test_layout.plain.xml ├── build.gradle ├── docs/ │ ├── .gitignore │ ├── build.gradle │ ├── mdbook/ │ │ ├── .gitignore │ │ ├── book.toml │ │ └── build.py │ ├── raw/ │ │ ├── .gitignore │ │ ├── changelog_old.md │ │ ├── css/ │ │ │ └── custom.css │ │ ├── de/ │ │ │ ├── index.html │ │ │ └── strings.xml │ │ ├── en/ │ │ │ ├── README.md │ │ │ ├── appendices/ │ │ │ │ ├── app-ops.tex │ │ │ │ ├── changelogs.tex │ │ │ │ ├── main.tex │ │ │ │ └── specifications.tex │ │ │ ├── custom.css │ │ │ ├── doctool.sh │ │ │ ├── faq/ │ │ │ │ ├── aot.tex │ │ │ │ ├── app-components.tex │ │ │ │ ├── main.tex │ │ │ │ └── misc.tex │ │ │ ├── guide/ │ │ │ │ ├── aot.tex │ │ │ │ ├── automation.tex │ │ │ │ ├── backup-restore.tex │ │ │ │ ├── main.tex │ │ │ │ ├── net-policy.tex │ │ │ │ └── wireless_debugging.tex │ │ │ ├── images/ │ │ │ │ ├── appops.drawio │ │ │ │ ├── main_page_entry_info_labeled.drawio │ │ │ │ └── main_page_entry_info_numbered.drawio │ │ │ ├── index.html │ │ │ ├── intro/ │ │ │ │ └── main.tex │ │ │ ├── keywords.tex │ │ │ ├── lua/ │ │ │ │ ├── alert_fix.lua │ │ │ │ ├── header_with_hyperlinks.lua │ │ │ │ ├── img_to_object.lua │ │ │ │ └── toc_generator.lua │ │ │ ├── main.tex │ │ │ ├── main_vanilla.tex │ │ │ ├── pages/ │ │ │ │ ├── app-details-page.tex │ │ │ │ ├── interceptor-page.tex │ │ │ │ ├── main-page.tex │ │ │ │ ├── main.tex │ │ │ │ ├── one-click-ops-page.tex │ │ │ │ ├── profile-page.tex │ │ │ │ ├── profiles-page.tex │ │ │ │ ├── scanner-page.tex │ │ │ │ ├── settings-page.tex │ │ │ │ └── shared-prefs-editor-page.tex │ │ │ ├── strings.xml │ │ │ └── titlepage.tex │ │ ├── es/ │ │ │ ├── index.html │ │ │ └── strings.xml │ │ ├── fr/ │ │ │ ├── index.html │ │ │ └── strings.xml │ │ ├── index.html │ │ ├── ja/ │ │ │ ├── index.html │ │ │ └── strings.xml │ │ ├── ko/ │ │ │ ├── index.html │ │ │ └── strings.xml │ │ ├── pt/ │ │ │ ├── index.html │ │ │ └── strings.xml │ │ ├── pt-rBR/ │ │ │ ├── index.html │ │ │ └── strings.xml │ │ ├── ru/ │ │ │ ├── index.html │ │ │ └── strings.xml │ │ ├── vi/ │ │ │ ├── index.html │ │ │ └── strings.xml │ │ ├── zh-rCN/ │ │ │ ├── index.html │ │ │ └── strings.xml │ │ └── zh-rTW/ │ │ ├── index.html │ │ └── strings.xml │ └── src/ │ └── main/ │ └── AndroidManifest.xml ├── fastlane/ │ └── metadata/ │ └── android/ │ ├── en-US/ │ │ ├── changelogs/ │ │ │ ├── 437.txt │ │ │ ├── 440.txt │ │ │ ├── 441.txt │ │ │ ├── 442.txt │ │ │ ├── 443.txt │ │ │ ├── 444.txt │ │ │ └── 445.txt │ │ ├── full_description.txt │ │ ├── short_description.txt │ │ └── title.txt │ └── ja/ │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── hiddenapi/ │ ├── .gitignore │ ├── build.gradle │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ ├── android/ │ │ ├── annotation/ │ │ │ ├── AppIdInt.java │ │ │ └── UserIdInt.java │ │ ├── app/ │ │ │ ├── ActivityManagerNative.java │ │ │ ├── ActivityThread.java │ │ │ ├── AppOpsManagerHidden.java │ │ │ ├── ContentProviderHolder.java │ │ │ ├── ContextImpl.java │ │ │ ├── GrantedUriPermission.java │ │ │ ├── IActivityManager.java │ │ │ ├── IApplicationThread.java │ │ │ ├── INotificationManager.java │ │ │ ├── IServiceConnection.java │ │ │ ├── IUriGrantsManager.java │ │ │ ├── ProfilerInfo.java │ │ │ ├── backup/ │ │ │ │ └── IBackupManager.java │ │ │ └── usage/ │ │ │ ├── IStorageStatsManager.java │ │ │ └── IUsageStatsManager.java │ │ ├── content/ │ │ │ ├── ComponentCallbacks2.java │ │ │ ├── Context.java │ │ │ ├── IContentProvider.java │ │ │ ├── IIntentReceiver.java │ │ │ ├── IIntentSender.java │ │ │ ├── IntentHidden.java │ │ │ ├── om/ │ │ │ │ ├── IOverlayManager.java │ │ │ │ └── OverlayInfoHidden.java │ │ │ └── pm/ │ │ │ ├── ApplicationInfoHidden.java │ │ │ ├── IOnPermissionsChangeListener.java │ │ │ ├── IPackageDataObserver.java │ │ │ ├── IPackageDeleteObserver.java │ │ │ ├── IPackageDeleteObserver2.java │ │ │ ├── IPackageInstallObserver2.java │ │ │ ├── IPackageInstaller.java │ │ │ ├── IPackageInstallerCallback.java │ │ │ ├── IPackageInstallerSession.java │ │ │ ├── IPackageManager.java │ │ │ ├── IPackageManagerN.java │ │ │ ├── IPackageMoveObserver.java │ │ │ ├── IPackageStatsObserver.java │ │ │ ├── KeySet.java │ │ │ ├── PackageCleanItem.java │ │ │ ├── PackageInfoHidden.java │ │ │ ├── PackageInstallerHidden.java │ │ │ ├── ParceledListSlice.java │ │ │ ├── SuspendDialogInfo.java │ │ │ ├── UserInfo.java │ │ │ ├── VerifierDeviceIdentity.java │ │ │ ├── permission/ │ │ │ │ └── SplitPermissionInfoParcelable.java │ │ │ └── verify/ │ │ │ └── domain/ │ │ │ └── IDomainVerificationManager.java │ │ ├── hardware/ │ │ │ └── input/ │ │ │ ├── IInputManager.java │ │ │ └── InputManagerHidden.java │ │ ├── miui/ │ │ │ └── AppOpsUtils.java │ │ ├── net/ │ │ │ ├── ConnectivityManagerHidden.java │ │ │ ├── IConnectivityManager.java │ │ │ ├── INetworkPolicyListener.java │ │ │ ├── INetworkPolicyManager.java │ │ │ ├── INetworkStatsService.java │ │ │ ├── INetworkStatsSession.java │ │ │ ├── NetworkPolicyManager.java │ │ │ ├── NetworkStats.java │ │ │ └── NetworkTemplate.java │ │ ├── os/ │ │ │ ├── IBinderHidden.java │ │ │ ├── IDeviceIdleController.java │ │ │ ├── IUserManager.java │ │ │ ├── ResultReceiver.java │ │ │ ├── SELinux.java │ │ │ ├── ServiceManager.java │ │ │ ├── ServiceSpecificException.java │ │ │ ├── ShellCallback.java │ │ │ ├── SystemProperties.java │ │ │ ├── UserHandleHidden.java │ │ │ └── storage/ │ │ │ ├── IMountService.java │ │ │ ├── IStorageManager.java │ │ │ ├── StorageManagerHidden.java │ │ │ └── StorageVolumeHidden.java │ │ ├── permission/ │ │ │ ├── IOnPermissionsChangeListener.java │ │ │ └── IPermissionManager.java │ │ ├── provider/ │ │ │ └── SettingsHidden.java │ │ ├── system/ │ │ │ ├── OsHidden.java │ │ │ └── StructPasswd.java │ │ └── util/ │ │ ├── TypedXmlPullParser.java │ │ ├── TypedXmlSerializer.java │ │ └── XmlHidden.java │ ├── com/ │ │ └── android/ │ │ ├── internal/ │ │ │ ├── app/ │ │ │ │ ├── IAppOpsActiveCallback.java │ │ │ │ ├── IAppOpsCallback.java │ │ │ │ ├── IAppOpsNotedCallback.java │ │ │ │ └── IAppOpsService.java │ │ │ ├── os/ │ │ │ │ └── PowerProfile.java │ │ │ └── telephony/ │ │ │ ├── IPhoneSubInfo.java │ │ │ └── ISub.java │ │ └── org/ │ │ └── conscrypt/ │ │ └── Conscrypt.java │ └── misc/ │ └── utils/ │ └── HiddenUtil.java ├── libcore/ │ ├── .gitignore │ ├── compat/ │ │ ├── .gitignore │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ └── java/ │ │ │ ├── io/ │ │ │ │ └── github/ │ │ │ │ └── muntashirakon/ │ │ │ │ └── compat/ │ │ │ │ ├── HexDump.java │ │ │ │ ├── ObjectsCompat.java │ │ │ │ ├── io/ │ │ │ │ │ ├── FastDataInput.java │ │ │ │ │ └── FastDataOutput.java │ │ │ │ ├── os/ │ │ │ │ │ └── ParcelCompat2.java │ │ │ │ ├── system/ │ │ │ │ │ ├── OsCompat.java │ │ │ │ │ ├── StructGroup.java │ │ │ │ │ └── StructTimespec.java │ │ │ │ └── xml/ │ │ │ │ ├── BinaryXmlPullParser.java │ │ │ │ ├── BinaryXmlSerializer.java │ │ │ │ ├── FastXmlSerializer.java │ │ │ │ ├── TypedXmlPullParser.java │ │ │ │ ├── TypedXmlSerializer.java │ │ │ │ ├── Xml.java │ │ │ │ ├── XmlPullParserWrapper.java │ │ │ │ ├── XmlSerializerWrapper.java │ │ │ │ └── XmlUtils.java │ │ │ └── org/ │ │ │ └── slf4j/ │ │ │ ├── Logger.java │ │ │ ├── LoggerFactory.java │ │ │ └── LoggerImpl.java │ │ └── test/ │ │ ├── java/ │ │ │ └── io/ │ │ │ └── github/ │ │ │ └── muntashirakon/ │ │ │ └── compat/ │ │ │ └── xml/ │ │ │ └── XmlTest.java │ │ └── resources/ │ │ ├── robolectric.properties │ │ ├── settings_ssaid.abx.xml │ │ ├── settings_ssaid.plain.xml │ │ ├── urigrants.abx.xml │ │ └── urigrants.plain.xml │ ├── io/ │ │ ├── .gitignore │ │ ├── build.gradle │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ ├── aidl/ │ │ │ ├── aosp/ │ │ │ │ └── android/ │ │ │ │ └── content/ │ │ │ │ └── pm/ │ │ │ │ ├── ParceledListSlice.aidl │ │ │ │ └── StringParceledListSlice.aidl │ │ │ └── io/ │ │ │ └── github/ │ │ │ └── muntashirakon/ │ │ │ └── io/ │ │ │ ├── IFileSystemService.aidl │ │ │ ├── IOResult.aidl │ │ │ └── UidGidPair.aidl │ │ └── java/ │ │ ├── androidx/ │ │ │ └── documentfile/ │ │ │ └── provider/ │ │ │ └── ExtendedRawDocumentFile.java │ │ ├── aosp/ │ │ │ └── android/ │ │ │ └── content/ │ │ │ └── pm/ │ │ │ ├── BaseParceledListSlice.java │ │ │ ├── ParcelUtils.java │ │ │ ├── ParceledListSlice.java │ │ │ └── StringParceledListSlice.java │ │ └── io/ │ │ └── github/ │ │ └── muntashirakon/ │ │ └── io/ │ │ ├── AtomicExtendedFile.java │ │ ├── CharSequenceInputStream.java │ │ ├── ExtendedFile.java │ │ ├── FileContainer.java │ │ ├── FileImpl.java │ │ ├── FileSystemManager.java │ │ ├── FileSystemService.java │ │ ├── FileUtils.java │ │ ├── IOResult.java │ │ ├── IoUtils.java │ │ ├── LocalFile.java │ │ ├── NIOFactory.java │ │ ├── OpenFile.java │ │ ├── Path.java │ │ ├── PathAttributes.java │ │ ├── PathContentInfo.java │ │ ├── PathReader.java │ │ ├── PathWriter.java │ │ ├── RemoteFile.java │ │ ├── RemoteFileChannel.java │ │ ├── SplitInputStream.java │ │ ├── SplitOutputStream.java │ │ └── UidGidPair.java │ └── ui/ │ ├── .gitignore │ ├── build.gradle │ ├── consumer-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── io/ │ │ └── github/ │ │ └── muntashirakon/ │ │ ├── adapters/ │ │ │ ├── AnyFilterArrayAdapter.java │ │ │ ├── NoFilterArrayAdapter.java │ │ │ └── SelectedArrayAdapter.java │ │ ├── dialog/ │ │ │ ├── AlertDialogBuilder.java │ │ │ ├── BottomSheetAlertDialogFragment.java │ │ │ ├── BottomSheetBehavior.java │ │ │ ├── BottomSheetDialog.java │ │ │ ├── CapsuleBottomSheetDialogFragment.java │ │ │ ├── DialogTitleBuilder.java │ │ │ ├── FullScreenDialogTitleBuilder.java │ │ │ ├── ScrollableDialogBuilder.java │ │ │ ├── SearchableFlagsDialogBuilder.java │ │ │ ├── SearchableItemsDialogBuilder.java │ │ │ ├── SearchableMultiChoiceDialogBuilder.java │ │ │ ├── SearchableSingleChoiceDialogBuilder.java │ │ │ ├── TextInputDialogBuilder.java │ │ │ └── TextInputDropdownDialogBuilder.java │ │ ├── lifecycle/ │ │ │ ├── SingleLiveEvent.java │ │ │ └── SoftInputLifeCycleObserver.java │ │ ├── multiselection/ │ │ │ ├── MultiSelectionActionsMenu.java │ │ │ ├── MultiSelectionActionsMenuPresenter.java │ │ │ ├── MultiSelectionActionsView.java │ │ │ └── ReflowMenuItemView.java │ │ ├── preference/ │ │ │ ├── DefaultAlertPreference.java │ │ │ ├── HyperlinkPreference.java │ │ │ ├── InfoAlertPreference.java │ │ │ ├── PrimaryButtonPreference.java │ │ │ ├── TopSwitchPreference.java │ │ │ └── WarningAlertPreference.java │ │ ├── text/ │ │ │ └── style/ │ │ │ └── ListSpan.java │ │ ├── util/ │ │ │ ├── AccessibilityUtils.java │ │ │ ├── AdapterUtils.java │ │ │ ├── LocalizedString.java │ │ │ ├── MotionUtils.java │ │ │ ├── ParcelUtils.java │ │ │ └── UiUtils.java │ │ ├── view/ │ │ │ ├── AutoCompleteTextViewCompat.java │ │ │ ├── AutoFitGridLayoutManager.java │ │ │ ├── ProgressIndicatorCompat.java │ │ │ └── TextInputLayoutCompat.java │ │ └── widget/ │ │ ├── AlwaysFocusedCheckedTextView.java │ │ ├── AppBarLayout.java │ │ ├── CheckBox.java │ │ ├── FloatingActionButtonGroup.java │ │ ├── FlowLayout.java │ │ ├── HyperlinkTextView.java │ │ ├── MaterialAlertView.java │ │ ├── MaterialAutoCompleteTextView.java │ │ ├── MaterialSpinner.java │ │ ├── MaxHeightScrollView.java │ │ ├── MultiSelectionView.java │ │ ├── NestedScrollView.java │ │ ├── NestedScrollableHost.java │ │ ├── RadioGroupGridLayout.java │ │ ├── RecyclerView.java │ │ ├── RoundedFirstAndLastChildViewGroup.java │ │ ├── SearchView.java │ │ ├── SwipeRefreshLayout.java │ │ └── TextInputTextView.java │ └── res/ │ ├── anim/ │ │ ├── bottom_sheet_slide_down.xml │ │ ├── bottom_sheet_slide_up.xml │ │ ├── fullscreen_dialog_enter.xml │ │ └── fullscreen_dialog_exit.xml │ ├── color/ │ │ ├── bottom_sheet_drag_handle_color.xml │ │ ├── bottom_sheet_drag_handle_color_activated.xml │ │ └── tab_item_background_color.xml │ ├── drawable/ │ │ ├── bottom_sheet_drag_handle.xml │ │ ├── bottom_sheet_drag_handle_activated.xml │ │ ├── ic_caution.xml │ │ ├── ic_clear.xml │ │ ├── ic_information.xml │ │ ├── ic_keyboard_backspace.xml │ │ ├── ic_more_horiz.xml │ │ ├── ic_more_vert.xml │ │ ├── ic_search.xml │ │ ├── ic_spinner_caret.xml │ │ ├── item_highlight.xml │ │ ├── item_semi_transparent.xml │ │ ├── item_transparent.xml │ │ ├── mtrl_switch_thumb_checked_medium.xml │ │ ├── mtrl_switch_thumb_checked_pressed_medium.xml │ │ ├── mtrl_switch_thumb_checked_unchecked_medium.xml │ │ ├── mtrl_switch_thumb_medium.xml │ │ ├── mtrl_switch_thumb_pressed_checked_medium.xml │ │ ├── mtrl_switch_thumb_pressed_medium.xml │ │ ├── mtrl_switch_thumb_pressed_unchecked_medium.xml │ │ ├── mtrl_switch_thumb_unchecked_checked_medium.xml │ │ ├── mtrl_switch_thumb_unchecked_medium.xml │ │ ├── mtrl_switch_thumb_unchecked_pressed_medium.xml │ │ ├── mtrl_switch_track_decoration_medium.xml │ │ ├── mtrl_switch_track_medium.xml │ │ ├── popup_menu_background.xml │ │ ├── popup_menu_item_background.xml │ │ ├── popup_menu_item_background_ripple.xml │ │ ├── spinner_rounded_border.xml │ │ └── tab_item_background_rounded.xml │ ├── drawable-v23/ │ │ └── popup_menu_background.xml │ ├── layout/ │ │ ├── auto_complete_dropdown_item.xml │ │ ├── auto_complete_dropdown_item_small.xml │ │ ├── dialog_bottom_sheet.xml │ │ ├── dialog_bottom_sheet_alert.xml │ │ ├── dialog_bottom_sheet_capsule.xml │ │ ├── dialog_scrollable_text_view.xml │ │ ├── dialog_searchable_multi_choice.xml │ │ ├── dialog_searchable_single_choice.xml │ │ ├── dialog_text_input.xml │ │ ├── dialog_text_input_dropdown.xml │ │ ├── dialog_title_toolbar.xml │ │ ├── dialog_title_with_two_icons.xml │ │ ├── item_reflow_menu.xml │ │ ├── m3_alert_select_dialog_item.xml │ │ ├── m3_preference.xml │ │ ├── m3_preference_alert.xml │ │ ├── m3_preference_button.xml │ │ ├── m3_preference_category.xml │ │ ├── m3_preference_image_frame.xml │ │ ├── m3_preference_top_switch.xml │ │ ├── m3_preference_widget_material_switch.xml │ │ └── view_selection_panel.xml │ ├── values/ │ │ ├── attrs.xml │ │ ├── bools.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ ├── values-large/ │ │ └── bools.xml │ ├── values-ldrtl/ │ │ └── styles.xml │ ├── values-night/ │ │ ├── colors.xml │ │ └── styles.xml │ ├── values-v23/ │ │ └── styles.xml │ ├── values-v27/ │ │ └── styles.xml │ └── values-v29/ │ └── styles.xml ├── libopenpgp/ │ ├── .gitignore │ ├── build.gradle │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── aidl/ │ │ └── org/ │ │ └── openintents/ │ │ └── openpgp/ │ │ ├── IOpenPgpService.aidl │ │ └── IOpenPgpService2.aidl │ └── java/ │ └── org/ │ └── openintents/ │ └── openpgp/ │ ├── AutocryptPeerUpdate.java │ ├── OpenPgpDecryptionResult.java │ ├── OpenPgpError.java │ ├── OpenPgpMetadata.java │ ├── OpenPgpSignatureResult.java │ └── util/ │ ├── OpenPgpApi.java │ ├── OpenPgpServiceConnection.java │ ├── OpenPgpUtils.java │ └── ParcelFileDescriptorUtil.java ├── libs/ │ ├── README.md │ ├── add_lib.php │ └── libsmali.jsonl ├── libserver/ │ ├── .gitignore │ ├── build.gradle │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── aidl/ │ │ └── io/ │ │ └── github/ │ │ └── muntashirakon/ │ │ └── AppManager/ │ │ └── server/ │ │ └── common/ │ │ └── IRootServiceManager.aidl │ └── java/ │ └── io/ │ └── github/ │ └── muntashirakon/ │ └── AppManager/ │ └── server/ │ └── common/ │ ├── BaseCaller.java │ ├── Caller.java │ ├── CallerResult.java │ ├── ClassUtils.java │ ├── ConfigParams.java │ ├── Constants.java │ ├── DataTransmission.java │ ├── FLog.java │ ├── ParamsFixer.java │ ├── ParcelableUtil.java │ ├── ServerActions.java │ ├── ServerInfo.java │ ├── ServerUtils.java │ ├── Shell.java │ └── ShellCaller.java ├── schema/ │ ├── changlelog.dtd │ └── packages.dtd ├── scripts/ │ ├── aab_to_apks.sh │ ├── backup_github_project.sh │ ├── docs.php │ ├── fix_strings.php │ ├── keep-five.sh │ ├── make_debloat_list.php │ ├── make_docs.sh │ ├── make_suggestions.php │ ├── push_to_mirrors.sh │ ├── update_libraries.php │ └── utils.php ├── server/ │ ├── .gitignore │ ├── build.gradle │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── io/ │ └── github/ │ └── muntashirakon/ │ └── AppManager/ │ └── server/ │ ├── BroadcastSender.java │ ├── LifecycleAgent.java │ ├── RootServiceMain.java │ ├── Server.java │ ├── ServerHandler.java │ └── ServerRunner.java ├── settings.gradle └── versions.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: Bug report description: Create a report to help us improve App Manager labels: [Bug] body: - type: checkboxes attributes: label: Please check before submitting an issue description: Checking the docs before submitting an issue may solve your problem. options: - label: I know what my device, OS and App Manager versions are required: true - label: I know how to take logs required: true - label: I know how to reproduce the issue which may not be specific to my device required: false - type: textarea attributes: label: Describe the bug description: A clear and concise description of what the bug is validations: required: true - type: textarea attributes: label: To Reproduce description: Steps to reproduce the behaviour placeholder: | - 1. Go to '...' - 2. Click on '....' - 3. Scroll down to '....' - 4. See error - type: textarea attributes: label: Expected behavior description: A clear and concise description of what you expected to happen. - type: textarea attributes: label: Screenshots description: If applicable, add screenshots to help explain your problem. - type: textarea attributes: label: Logs description: If applicable, add crash or any other logs to help us figure out the problem. - type: textarea attributes: label: Device info value: | - Device: - OS Version: - App Manager Version: - Mode: Root/ADB/NonRoot validations: required: true - type: textarea attributes: label: Additional context description: Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: 🐜 android-libraries Repo url: https://github.com/MuntashirAkon/android-libraries about: Please send requests for new trackers and libraries to this repository - name: 📗 App Manager Docs url: https://muntashirakon.github.io/AppManager/ about: Please read documentation carefully before submitting an issue ================================================ FILE: .github/ISSUE_TEMPLATE/documentation.yml ================================================ name: Docs improvement description: Improve Docs and Readme labels: [Documentation] body: - type: checkboxes attributes: label: Please check before submitting an issue description: You must be familiar with the technical terms specified in the documentation. Also, if your problem comes from the translated version rather than the English original, please correct it from Weblate. options: - label: This issue is not related to the translated version of the documentation required: true - label: I know technical terms used in the documentation required: true - type: textarea attributes: label: Describe the problem area description: A short and concise description of the already existing feature or documentation, e.g. uninstall in batch operations. validations: required: true - type: textarea attributes: label: Current documentation if it present description: Link or copy-and-paste the current description or refer by section number and title - type: textarea attributes: label: Describe your suggestion description: A clear and concise description of what you expect the documentation to contain, e.g. it should've been noted that uninstall in batch operations does not work in no-root mode validations: required: true - type: textarea attributes: label: Additional context description: Add any other context or screenshots about the documentation here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yml ================================================ name: Feature request description: Suggest an idea for this project labels: [Feature] body: - type: checkboxes attributes: label: Please check before submitting an issue description: Checking the docs before submitting an issue may solve your problem. options: - label: I am using the latest version of App Manager required: true - label: I have searched the issues and haven't found anything relevant required: true - label: I have read the docs required: true - type: textarea attributes: label: Describe a description of the new feature description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] validations: required: true - type: textarea attributes: label: Describe the solution you'd like description: A clear and concise description of what you want to happen. - type: textarea attributes: label: Describe alternatives you've considered description: A clear and concise description of any alternative solutions or features you've considered - type: textarea attributes: label: Additional context description: Add any other context or screenshots about the feature request here. ================================================ FILE: .github/ISSUE_TEMPLATE/help-wanted.yml ================================================ name: Help wanted description: Ask for help regarding a feature labels: [Help Wanted] body: - type: textarea attributes: label: Describe the existing feature/documentation description: A short and concise description of the already existing feature or documentation, e.g. uninstall in batch operations. The purpose of this template is to improve your knowledge regarding the feature. For new features, use the feature request template. Use the bug report template if the feature is not working as expected. - type: textarea attributes: label: Describe your problem(s) description: Describe the problem(s) you've faced while using the feature or reading the documentation. - type: textarea attributes: label: Additional context description: Add any other context or screenshots about the feature/documentation here. ================================================ FILE: .github/workflows/codeql.yml ================================================ name: "CodeQL" on: push: branches: [ "master", "AppManager-*" ] pull_request: branches: [ "master" ] schedule: - cron: '43 14 * * 4' jobs: analyze: name: Analyze runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ 'java' ] steps: - name: Checkout repository uses: actions/checkout@v4 with: submodules: 'recursive' - name: Set up JDK uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: '21' cache: 'gradle' - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} - name: Autobuild uses: github/codeql-action/autobuild@v3 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 with: category: "/language:${{matrix.language}}" ================================================ FILE: .github/workflows/lint.yml ================================================ name: Lint on: push: branches: - 'master' - 'AppManager-*' paths-ignore: - 'fastlane/**' - 'scripts/**' - '*.md' pull_request: branches: - 'master' - 'AppManager-*' paths-ignore: - 'fastlane/**' - 'scripts/**' - '*.md' jobs: build: runs-on: ubuntu-latest steps: - name: Clone the repository uses: actions/checkout@v4 with: submodules: 'recursive' - name: Set up JDK uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: '21' cache: 'gradle' - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Run lint run: ./gradlew lint - name: Upload lint results if: ${{ always() }} uses: actions/upload-artifact@v4 with: path: ./app/build/reports/lint-results-debug.html ================================================ FILE: .github/workflows/tests.yml ================================================ name: Tests on: push: branches: - 'master' - 'AppManager-*' paths-ignore: - 'fastlane/**' - 'scripts/**' - '*.md' pull_request: branches: - 'master' - 'AppManager-*' paths-ignore: - 'fastlane/**' - 'scripts/**' - '*.md' jobs: build: runs-on: ubuntu-latest steps: - name: Clone the repository uses: actions/checkout@v4 with: submodules: 'recursive' - name: Set up JDK uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: '21' cache: 'gradle' - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Run tests run: ./gradlew test - name: Upload test results if: ${{ always() }} uses: actions/upload-artifact@v4 with: name: unitTestResults path: ./app/build/reports/tests/testDebugUnitTest/ ================================================ FILE: .gitignore ================================================ .gradle/ .idea/ crowdin.properties local.properties .DS_Store build/ app/preRelease/ app/release/ app/debug/ app/libs/am-common.jar toybox/src/main/jniLibs app/src/main/assets/am.jar app/src/main/assets/main.jar pids logs node_modules npm-debug.log coverage/ run dist .nyc_output .basement config.local.js basement_dist *.apk *.iml *.jks *~ scripts/KeyStore.sh ================================================ FILE: .gitmodules ================================================ [submodule "scripts/android-libraries"] path = scripts/android-libraries url = https://github.com/MuntashirAkon/android-libraries.git [submodule "scripts/android-debloat-list"] path = scripts/android-debloat-list url = https://github.com/MuntashirAkon/android-debloat-list.git ================================================ FILE: .run/Documentation.run.xml ================================================ PDFLATEX pdflatex NONE open -shell-escape $PROJECT_DIR$/docs/raw/en/main_vanilla.tex $PROJECT_DIR$/docs/raw/en {projectDir}/auxil false PDF TEXLIVE true [] [] ================================================ FILE: .run/app.run.xml ================================================ ================================================ FILE: .run/app_details.run.xml ================================================ ================================================ FILE: .run/fm.run.xml ================================================ ================================================ FILE: .run/lint.run.xml ================================================ true true false ================================================ FILE: .run/settings.run.xml ================================================ ================================================ FILE: .run/test.run.xml ================================================ true true false ================================================ FILE: BUILDING.rst ================================================ .. SPDX-License-Identifier: GPL-3.0-or-later OR CC-BY-SA-4.0 ==================== Building App Manager ==================== Requirements ============ * **Hardware:** Any computer with 8 GB RAM and 20 GB storage * **Operating system:** Linux/macOS/WSL * **Software:** Android Studio/IntelliJ IDEA, Gradle, Latex, pandoc, JDK 17+ * **Active network connection:** Depending on your development environment, you may need at least 20 GB data package. macOS ===== The following steps are required only if you want to build APKS: - Install Homebrew:: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" - Install bundletool:: brew install bundletool Linux|GNU ========= - Install the development tools. * For Debian/Ubuntu:: sudo apt-get install build-essential * For Fedora/CentOS/RHEL:: sudo yum groupinstall "Development Tools" * For Arch/Artix/Manjaro:: sudo pacman -S base-devel - Install `bundletool-all.jar`_ if you want to build APKS, and make sure it is available as ``bundletool`` command. A quick way would be to create an alias as follows (assuming you're using ``bash``):: echo "alias bundletool='java -jar path/to/bundletool.jar'" >> ~/.bashrc Make sure to replace ``/path/to/bundletool-all.jar`` with the actual path for **bundletool-all.jar**. * For Arch/Artix/Majaro (with ``yay``):: yay -S bundletool Clone and Build App Manager =========================== 1. Clone the repo along with submodules:: git clone --recurse-submodules https://github.com/MuntashirAkon/AppManager.git You can use the `--depth 1` argument if you don't want to clone past commits. 2. Open the project **AppManager** using Android Studio/IntelliJ IDEA. The IDE should start syncing automatically. It will also download all the necessary dependencies automatically provided you have a working network connection. 3. Build debug version of App Manager from *Menu* > *Build* > *Make Project*, or, from the terminal:: ./gradlew packageDebugUniversalApk The command will generate a universal APK instead of a bundled app. Create Bundled App ================== To create a bundled app in APKS format, run the following command:: ./scripts/aab_to_apks.sh type Replace ``type`` with ``release`` or ``debug`` based on your requirements. It will ask for KeyStore credentials interactively. The script above will also generate a universal APK. .. _bundletool-all.jar: https://github.com/google/bundletool Build documentation =================== See `docs/raw/en/README.md `_ ================================================ FILE: CONTRIBUTING.rst ================================================ .. SPDX-License-Identifier: GPL-3.0-or-later OR CC-BY-SA-4.0 ============ Contributing ============ You are welcome contribute to App Manager! This doesn't mean that you need coding skills. You can contribute to App Manager by creating helpful issues, attending discussions, improving documentations and translations, making icon for icon packs, adding unrecognised libraries or ad/tracking signatures, reviewing the source code, as well as reporting security vulnerabilities. AI-Generated Contributions ========================== To maintain the legal integrity and clear provenance of App Manager's codebase under the GPL-3.0-or-later license, we DO NOT accept contributions that are generated, in whole part or in part, by Artificial Intelligence (AI) or Large Language Models (LLMs). All contributions MUST be the original work of the human author(s) submitting the pull or merge request. By submitting a pull or merge request, you affirm that the code was authored by you without the use of generative AI tools that produce functional code blocks. Rules ===== - If you are going to implement or work on any specific feature, please inform us before doing so. Due to the complex nature of the project, integrating a new feature could be challenging. - Your contributions are licensed under ``GPL-3.0-or-later`` by default. Please see related `Linux documentations`_ to see how to add license headers to a file, and remember the following: * If the files your are contributing to do not have ``GPL-3.0-or-later``, add it to the existing ``SPDX-License-Identifier`` using ``AND``, e.g. :: SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-or-later * If the entire file or Java class is copied from another person or project, you have to add a copyright statement adding the person who wrote it first like this:: // Copyright 2004 Linus Torvalds You can also add other contributors but they are not mandatory. You do not need to include your name because it can already be available via the version control system. * Do not add the **@author** tag as it is considered a bad practice. - You have to sign-off your work. You can do that using the ``--signoff`` argument. If you are not using command line or a software that does not support it, you can add the following line at the end of your commit message:: Signed-off-by: My Name We also support most of the `commit message conventions`_ from Linux. App Manager is a legal software and its contributions are protected by copyright laws. Consider using real credentials ie. real name and email as we may be required to delete your valuable contributions in the event of introducing new license or adding exceptions to the existing license. **Note:** Repositories located in sites other than GitHub are currently considered mirrors and any pull or merge requests submitted there will not be accepted. Instead, you can submit patches (as ``.patch`` files) via email attachment. My email address is am4android [at] riseup [dot] net. Beware that such emails may be publicly accessible in future. GitHub pull requests will be merged manually using the corresponding patches. As a result, GitHub may falsely mark them *closed* instead of *merged*. **Warning.** Every commit made by other users are thoroughly examined with the exception of commits made through Weblate. So, if it is found that you are abusing the Weblate platform, you will be blocked on Weblate without a warning, and ALL your contributions to this project shall be removed. This is a hobby project, and like any hobby, I want to make things neat and clean. Existing contributors are also encouraged to report any abuse. Your identity shall be kept secret. .. _Linux documentations: https://github.com/torvalds/linux/blob/master/Documentation/process/license-rules.rst .. _commit message conventions: https://git.wiki.kernel.org/index.php/CommitMessageConventions ================================================ FILE: COPYING ================================================ App Manager is provided under: SPDX-License-Identifier: GPL-3.0-or-later Being under the terms of the GNU General Public License version 3 or later, according with: LICENSES/GPL-3.0 In addition, other licenses may also apply. Please navigate to: LICENSES/ to see all the licenses used in this project. All contributions to the App Manager are subject to this COPYING file. ================================================ FILE: LICENSES/Apache-2.0 ================================================ Valid-License-Identifier: Apache-2.0 SPDX-URL: https://spdx.org/licenses/Apache-2.0.html Usage-Guide: To use the Apache License version 2.0 put the following SPDX tag/value pair into a comment according to the placement guidelines in the licensing rules documentation: SPDX-License-Identifier: Apache-2.0 Do NOT use this license unless the files are copied from another work under the same license. In such cases, use "AND GPL-3.0-or-later" so that your contributions are under GPL-3.0+ license. License-Text: Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: a. You must give any other recipients of the Work or Derivative Works a copy of this License; and b. You must cause any modified files to carry prominent notices stating that You changed the files; and c. You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and d. If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS ================================================ FILE: LICENSES/BSD-2-Clause ================================================ Valid-License-Identifier: BSD-2-Clause SPDX-URL: https://spdx.org/licenses/BSD-2-Clause.html Usage-Guide: To use the BSD 2-clause "Simplified" License put the following SPDX tag/value pair into a comment according to the placement guidelines in the licensing rules documentation: SPDX-License-Identifier: BSD-2-Clause Do NOT use this license unless the files are copied from another work under the same license. In such cases, use "AND GPL-3.0-or-later" so that your contributions are under GPL-3.0+ license. License-Text: Copyright (c) . All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: LICENSES/BSD-3-Clause ================================================ Valid-License-Identifier: BSD-3-Clause SPDX-URL: https://spdx.org/licenses/BSD-3-Clause.html Usage-Guide: To use the BSD 3-clause "New" or "Revised" License put the following SPDX tag/value pair into a comment according to the placement guidelines in the licensing rules documentation: SPDX-License-Identifier: BSD-3-Clause License-Text: Copyright (c) . All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: LICENSES/CC-BY-SA-4.0 ================================================ Valid-License-Identifier: CC-BY-SA-4.0 SPDX-URL: https://spdx.org/licenses/CC-BY-SA-4.0 Usage-Guide: Do NOT use this license for code, but it's acceptable for content like artwork or documentation. When using it for the latter, it's best to use it together GPL-3.0-or-later license using "OR". To use the Creative Commons Attribution-ShareAlike 4.0 International license put the following SPDX tag/value pair into a comment according to the placement guidelines in the licensing rules documentation: SPDX-License-Identifier: CC-BY-SA-4.0 License-Text: Creative Commons Attribution-ShareAlike 4.0 International ======================================================================= Creative Commons Corporation ("Creative Commons") is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an "as-is" basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. Using Creative Commons Public Licenses Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. Considerations for licensors: Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC- licensed material, or material used under an exception or limitation to copyright. More considerations for licensors: wiki.creativecommons.org/Considerations_for_licensors Considerations for the public: By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor's permission is not necessary for any reason--for example, because of any applicable exception or limitation to copyright--then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. More_considerations for the public: wiki.creativecommons.org/Considerations_for_licensees ======================================================================= Creative Commons Attribution-ShareAlike 4.0 International Public License By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-ShareAlike 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. Section 1 -- Definitions. a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. c. BY-SA Compatible License means a license listed at creativecommons.org/compatiblelicenses, approved by Creative Commons as essentially the equivalent of this Public License. d. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. e. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. f. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. g. License Elements means the license attributes listed in the name of a Creative Commons Public License. The License Elements of this Public License are Attribution and ShareAlike. h. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. i. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. j. Licensor means the individual(s) or entity(ies) granting rights under this Public License. k. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. l. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. m. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. Section 2 -- Scope. a. License grant. 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: a. reproduce and Share the Licensed Material, in whole or in part; and b. produce, reproduce, and Share Adapted Material. 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. 3. Term. The term of this Public License is specified in Section 6(a). 4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a) (4) never produces Adapted Material. 5. Downstream recipients. a. Offer from the Licensor -- Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. b. Additional offer from the Licensor -- Adapted Material. Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapter's License You apply. c. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. 6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). b. Other rights. 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. 2. Patent and trademark rights are not licensed under this Public License. 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties. Section 3 -- License Conditions. Your exercise of the Licensed Rights is expressly made subject to the following conditions. a. Attribution. 1. If You Share the Licensed Material (including in modified form), You must: a. retain the following if it is supplied by the Licensor with the Licensed Material: i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); ii. a copyright notice; iii. a notice that refers to this Public License; iv. a notice that refers to the disclaimer of warranties; v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; b. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and c. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. b. ShareAlike. In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply. 1. The Adapter's License You apply must be a Creative Commons license with the same License Elements, this version or later, or a BY-SA Compatible License. 2. You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material. 3. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply. Section 4 -- Sui Generis Database Rights. Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database; b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material, including for purposes of Section 3(b); and c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. Section 5 -- Disclaimer of Warranties and Limitation of Liability. a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. Section 6 -- Term and Termination. a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or 2. upon express reinstatement by the Licensor. For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. Section 7 -- Other Terms and Conditions. a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. Section 8 -- Interpretation. a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. ======================================================================= Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” The text of the Creative Commons public licenses is dedicated to the public domain under the CC0 Public Domain Dedication. Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark "Creative Commons" or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. Creative Commons may be contacted at creativecommons.org. ================================================ FILE: LICENSES/GPL-2.0 ================================================ Valid-License-Identifier: GPL-2.0+ Valid-License-Identifier: GPL-2.0-or-later SPDX-URL: https://spdx.org/licenses/GPL-2.0-or-later.html Usage-Guide: To use this license in source code, put one of the following SPDX tag/value pairs into a comment according to the placement guidelines in the licensing rules documentation. For 'GNU General Public License (GPL) version 2 or any later version' use: SPDX-License-Identifier: GPL-2.0+ or SPDX-License-Identifier: GPL-2.0-or-later License-Text: GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License. ================================================ FILE: LICENSES/GPL-3.0 ================================================ Valid-License-Identifier: GPL-3.0+ Valid-License-Identifier: GPL-3.0-or-later SPDX-URL: https://spdx.org/licenses/GPL-3.0-or-later.html Usage-Guide: To use this license in source code, put one of the following SPDX tag/value pairs into a comment according to the placement guidelines in the licensing rules documentation. For 'GNU General Public License (GPL) version 3 or any later version' use: SPDX-License-Identifier: GPL-3.0+ or SPDX-License-Identifier: GPL-3.0-or-later License-Text: GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: LICENSES/ISC ================================================ Valid-License-Identifier: ISC SPDX-URL: https://spdx.org/licenses/ISC.html Usage-Guide: To use the ISC License put the following SPDX tag/value pair into a comment according to the placement guidelines in the licensing rules documentation: SPDX-License-Identifier: ISC Do NOT use this license unless the files are copied from another work under the same license. In such cases, use "AND GPL-3.0-or-later" so that your contributions are under GPL-3.0+ license. License-Text: ISC License Copyright (c) Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ================================================ FILE: LICENSES/MIT ================================================ Valid-License-Identifier: MIT SPDX-URL: https://spdx.org/licenses/MIT.html Usage-Guide: To use the MIT License put the following SPDX tag/value pair into a comment according to the placement guidelines in the licensing rules documentation: SPDX-License-Identifier: MIT Do NOT use this license unless the files are copied from another work under the same license. In such cases, use "AND GPL-3.0-or-later" so that your contributions are under GPL-3.0+ license. License-Text: MIT License Copyright (c) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: LICENSES/WTFPL ================================================ Valid-License-Identifier: WTFPL SPDX-URL: https://spdx.org/licenses/WTFPL.html Usage-Guide: To use the WTFPL put the following SPDX tag/value pair into a comment according to the placement guidelines in the licensing rules documentation: SPDX-License-Identifier: WTFPL Do NOT use this license unless the files are copied from another work under the same license. In such cases, use "AND GPL-3.0-or-later" so that your contributions are under GPL-3.0+ license. License-Text: DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE Version 2, December 2004 Copyright (C) 2004 Sam Hocevar Everyone is permitted to copy and distribute verbatim or modified copies of this license document, and changing it is allowed as long as the name is changed. DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. You just DO WHAT THE FUCK YOU WANT TO. ================================================ FILE: PRIVACY_POLICY.rst ================================================ .. SPDX-License-Identifier: GPL-3.0-or-later OR CC-BY-SA-4.0 ============== Privacy Policy ============== (DRAFT REVISION NO. 3) 1. Definition ============= - "The Project" refers to the App Manager project which includes the git repositories (excluding The Project Hosting Providers), E-Mails, and their maintainers. (See §6) - "We", "Us", and similar capitalized pronouns refer to the maintainers of The Project. - "E-Mail" or "E-Mails" refers to the messages sent directly to the maintainers of The Project. - "The Software" refers to App Manager software distributed by its maintainers. - "The Project Hosting Providers" refers to GitHub, GitLab, Codeberg, RiseUp, and SourceHut. - "Third-party Services" refers to The Project Hosting Providers along with VirusTotal, Pithus, F-Droid, and Hosted Weblate. - "Third-party Websites" refers to websites We do not control or operate. - "You", "Yours", and similar capitalized pronouns refer to anyone who uses The Software or has contributed to The Project in any capacity. - "PII" refers to personally identifiable information. 2. Information Collected from You ================================= 2.1. The Project --------------- We DO NOT collect any information that You have not provided voluntarily. The sources of this information include your contributions on Git and any E-Mails you send. This information may include PII, such as Your real name and E-Mail address. If You send crash reports and logs via E-Mail, they may also contain non-PII details, such as Your device name, operating system version, software version, language, and more. The official website for The Project is located at https://muntashirakon.github.io/AppManager/ and is hosted by GitHub (see §2.3). We do not collect any information from this website. 2.2. The Software ----------------- We DO NOT collect any information from The Software. 2.3. Third-party Services ------------------------- Depending on the services used, the privacy policy of the following services will apply to You: - `GitHub`_ (Stars, issues, pull requests, discussions, releases, traffic) - `GitLab`_ (Stars, merge requests, traffic) - `Codeberg`_ (Stars, issues, pull requests) - `RiseUp`_ (Stars, issues, merge requests) - `SourceHut`_ (Tickets) - `VirusTotal`_ (Malware reports, file uploads, traffic) - `F-Droid`_ (F-Droid official repository and app store) - `Hosted Weblate`_ (Translations). 2.4. Third-party Websites ------------------------- Links to Third-party Websites are provided for your benefit. For your safety, it is recommended that You read and understand the privacy policy of these websites before visiting them. 3. Data Retention Policy ======================== Information collected through Git is stored indefinitely and is accessible to anyone, anywhere. E-Mails that do not have legal significance can be retained for a maximum of one year. These E-Mails are stored offline in a partition that is encrypted using FileVault. However, E-Mails deemed legally significant can be kept permanently, both online and offline. To understand the data retention policies of the Third-party Services (as specified in §2.3) and Third-party Websites (as outlined in §2.4), please refer to their respective privacy policies. 4. Removal of Information ========================= You can request the removal of PII by either sending Us an E-Mail or creating an issue. You can also request the removal of non-PII, but please note that the removal is not guaranteed. In both cases, the following types of information cannot be removed by Us: - Information present in a commit message, such as the ``Signed-off-by:`` tag - Information contained in a file (since they are a part of git history) - Forked repositories (You need to ask the person who forked the repository) - Reactions to GitHub issues, comments, and discussions (You need to remove them Yourself) - Information stored by the Third-party Services or Third-party Websites (You need to ask them Yourself). The following information may or may not be removed: - Mentions in a comment - E-Mails deemed legally significant. 5. Changes to the Privacy Policy ================================ All changes, except those related to spelling or grammar, will be announced on all the official channels. Unless stated otherwise, the updated privacy policy will apply only to The Software released after the changes. 6. Project Maintainers ====================== 1. **Name:** Muntashir Al-Islam **Email:** muntashirakon [at] riseup [dot] net .. _GitHub: https://docs.github.com/en/site-policy/privacy-policies/github-privacy-statement .. _GitLab: https://about.gitlab.com/privacy/ .. _Codeberg: https://codeberg.org/codeberg/org/src/PrivacyPolicy.md .. _RiseUp: https://riseup.net/en/privacy-policy .. _SourceHut: https://man.sr.ht/privacy.md .. _VirusTotal: https://support.virustotal.com/hc/en-us/articles/115002168385-Privacy-Policy .. _F-Droid: https://f-droid.org/en/about/#terms-etc .. _Hosted Weblate: https://hosted.weblate.org/legal/privacy/ ================================================ FILE: README.md ================================================

App Manager Logo

App Manager

Docs · Releases · Telegram Channel

--- ## Features ### General features - Fully reproducible, copylefted libre software (GPLv3+) - Material 3 with dynamic colours - Display as much information as possible in the main page - List activities, broadcast receivers, services, providers, app ops, permissions, signatures, shared libraries, etc. of an application - Launch activities and services - Create shortcuts of activities - [Intercept activities](https://muntashirakon.github.io/AppManager/#sec:interceptor-page) - Scan for trackers and libraries in apps and list (all or only) tracking classes (and their code dump) - View/save the manifest of an app - Display app usage, data usage (mobile and Wi-Fi), and app storage info (requires “Usage Access” permission) - Install/uninstall APK files (including APKS, APKM and XAPK with OBB files) - Share APK files - Back up/restore APK files - Batch operations - Single-click operations - Logcat viewer, manager and exporter - [Profiles](https://muntashirakon.github.io/AppManager/#sec:profiles-page) - Debloater - Code editor - File manager - Simple terminal emulator - Open an app in Aurora Store or in your favourite F-Droid client - Sign APK files with custom signatures before installing - Backup encryption: OpenPGP via OpenKeychain, RSA, ECC (hybrid encryption with AES) and AES. - Track foreground UI components ### Root/ADB-only features - Revoke runtime (AKA dangerous) and development permissions - Change the mode of an app op - Display/kill/force-stop running apps or processes - Clear app data or app cache - View/change net policy - Control battery optimization - Freeze/unfreeze apps ### Root-only features - Block any activities, broadcast receivers, services, or providers of an app with native import/export as well as Watt and Blocker import support - View/edit/delete shared preferences of any app - Back up/restore apps with data, rules and extras (such as permissions, battery optimization, SSAID, etc.) - View system configurations including blacklisted or whitelisted apps, permissions, etc. - View/change SSAID. …and many more! This single app combines the features of 5 or 6 apps any tech-savvy person needs! ### Upcoming features - Finder: Find app components, permissions etc. in all apps - Basic APK editing - Routine operations - Enable/disable app actions such as launch on boot - Crash monitor - Systemless disabling/uninstalling of the system apps - Import app list exported by App Manager - More advance terminal emulator - Database viewer and editor, etc. [Get it on F-Droid](https://f-droid.org/packages/io.github.muntashirakon.AppManager) ## Translations Help translate [the app strings](https://hosted.weblate.org/engage/app-manager/) and [the docs](https://hosted.weblate.org/projects/app-manager/docs/) at Hosted Weblate. [![Translation status](https://hosted.weblate.org/widgets/app-manager/-/multi-auto.svg)](https://hosted.weblate.org/engage/app-manager/) ## Mirrors [Codeberg](https://codeberg.org/muntashir/AppManager) · [GitLab](https://gitlab.com/muntashir/AppManager) · [Riseup](https://0xacab.org/muntashir/AppManager) · [sourcehut](https://git.sr.ht/~muntashir/AppManager) ## Screenshots ## Build Instructions See [BUILDING.rst](BUILDING.rst) ## Contributing See [CONTRIBUTING.rst](CONTRIBUTING.rst) ## Donation and Funding As of September 2024, App Manager is not accepting any financial support until further notice. But you may still be able to send gifts (e.g., gift cards, subscriptions, food and drink, flowers, or even cash). Please contact the maintainer at muntashirakon [at] riseup [dot] net for further assistance. In addition, the maintainers and contributors of this project DO NOT consent to the creation, sale, or promotion of tokens, cryptocurrencies, NFTs, or any other financial instruments that claim to represent this project, its code, or its community. Any such attempts are unauthorized and not affiliated with this project in any way. ## Credits and Libraries A list of credits and libraries are available in the **About** section of the app. ================================================ FILE: app/.gitignore ================================================ /build *.apk .cxx *~ ================================================ FILE: app/build.gradle ================================================ // SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-or-later plugins { id('com.android.application') id('dev.rikka.tools.refine') version "${refine_version}" } android { namespace 'io.github.muntashirakon.AppManager' compileSdk compile_sdk buildToolsVersion = build_tools defaultConfig { applicationId 'io.github.muntashirakon.AppManager' minSdk min_sdk targetSdk target_sdk versionCode 445 versionName "4.0.5" javaCompileOptions { annotationProcessorOptions { arguments += [ "room.schemaLocation": "$projectDir/schemas".toString(), "room.incremental" : "true" ] } } externalNativeBuild { cmake { arguments "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON" } } // Add build time to BuildConfig buildConfigField "long", "BUILD_TIME_MILLIS", "${buildTime()}" } signingConfigs { debug { storeFile file('dev_keystore.jks') storePassword 'kJCp!Bda#PBdN2RLK%yMK@hatq&69E' keyPassword 'kJCp!Bda#PBdN2RLK%yMK@hatq&69E' keyAlias 'key0' } } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' resValue "string", "app_name", "App Manager" } debug { applicationIdSuffix '.debug' versionNameSuffix '-DEBUG' signingConfig signingConfigs.debug resValue "string", "app_name", "AM Debug" } } lint { checkReleaseBuilds false abortOnError false checkDependencies true } compileOptions { encoding "UTF-8" // Flag to enable support for the new language APIs coreLibraryDesugaringEnabled true sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } externalNativeBuild { cmake { path 'src/main/cpp/CMakeLists.txt' } } splits { abi { reset() include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' universalApk true } } aaptOptions { noCompress 'jar', 'sh' } testOptions { unitTests { includeAndroidResources = true } } sourceSets { androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) } dependenciesInfo { includeInApk false includeInBundle false } packagingOptions { jniLibs { useLegacyPackaging true } resources { excludes += ['META-INF/*.version'] merges += ['baksmali.properties'] } } buildFeatures { aidl true buildConfig true } } dependencies { compileOnly project(path: ':hiddenapi') coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:${desugar_jdk_version}" // Core Libraries implementation project(path: ':libcore:compat') implementation project(path: ':libcore:io') implementation project(path: ':libcore:ui') implementation project(path: ':libserver') implementation project(path: ':docs') // API implementation "com.github.MuntashirAkon:unapkm-android:${unapkm_version}" implementation project(path: ':libopenpgp') // APK Editing implementation "com.github.REAndroid:ARSCLib:${arsclib_version}" implementation "com.github.MuntashirAkon:apksig-android:${apksig_version}" implementation "com.github.MuntashirAkon:sun-security-android:${sun_security_version}" implementation "org.bouncycastle:bcprov-jdk15to18:${bouncycastle_version}" implementation "org.bouncycastle:bcpkix-jdk15to18:${bouncycastle_version}" implementation "com.android.tools.smali:smali-baksmali:${baksmali_version}" implementation "com.android.tools.smali:smali:${baksmali_version}" implementation "com.github.MuntashirAkon.jadx:jadx-core:${jadx_version}" // Replace SLF4J with a placeholder configurations { configureEach { exclude group: 'org.slf4j', module: 'slf4j-api' } } implementation "com.github.MuntashirAkon.jadx:jadx-dex-input:${jadx_version}" // Replace SLF4J with a placeholder configurations { configureEach { exclude group: 'org.slf4j', module: 'slf4j-api' } } // DB implementation "androidx.room:room-runtime:${room_version}" annotationProcessor "androidx.room:room-compiler:${room_version}" // FM implementation "com.j256.simplemagic:simplemagic:${simplemagic_version}" // Privileged implementation "com.github.MuntashirAkon:libadb-android:${libadb_version}" implementation "com.github.topjohnwu.libsu:core:${libsu_version}" implementation "org.lsposed.hiddenapibypass:hiddenapibypass:${hiddenapibypass_version}" implementation "dev.rikka.tools.refine:runtime:${refine_version}" // UI implementation "com.google.android.material:material:${material_version}" implementation "androidx.core:core:${androidx_core_version}" implementation "androidx.appcompat:appcompat:${appcompat_version}" // Fix duplicate classes issue in material configurations { configureEach { exclude group: 'androidx.lifecycle', module: 'lifecycle-viewmodel-ktx' } } implementation "androidx.documentfile:documentfile:${documentfile_version}" implementation "androidx.activity:activity:${activity_version}" implementation "androidx.core:core-splashscreen:${splashscreen_version}" implementation "androidx.biometric:biometric:${biometric_version}" implementation "androidx.webkit:webkit:${webkit_version}" implementation "io.github.Rosemoe.sora-editor:editor:${sora_editor_version}" implementation "io.github.Rosemoe.sora-editor:language-textmate:${sora_editor_version}" implementation "com.github.MuntashirAkon:time-duration-picker:${duration_picker}" // Utility implementation "com.google.code.gson:gson:${gson_version}" implementation "com.github.luben:zstd-jni:${zstd_version}@aar" // debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1' // Espresso UI Testing // androidTestImplementation "com.android.support.test.espresso:espresso-core:3.0.2" // Optional if you need to detect intents. // androidTestImplementation "com.android.support.test.espresso:espresso-intents:3.0.2" // Unit Testing testImplementation "junit:junit:${junit_version}" testImplementation "org.robolectric:robolectric:${robolectric_version}" } preBuild.dependsOn ":server:build" def buildTime() { var commitTime = "git show --no-patch --format=%ct000".execute([], project.rootDir).text.trim() if (isDigitsOnly(commitTime)) { return Long.parseLong(commitTime) } println("Using system time as the build time.") return System.currentTimeMillis() } static def isDigitsOnly(CharSequence str) { final int len = str.length() for (int cp, i = 0; i < len; i += Character.charCount(cp)) { cp = Character.codePointAt(str, i) if (!Character.isDigit(cp)) { return false } } return true } ================================================ FILE: app/lint.xml ================================================ ================================================ FILE: app/proguard-rules.pro ================================================ # Specify compression level -optimizationpasses 5 # Algorithm for confusion -optimizations !code/simplification/arithmetic,!field/*,!class/merging/* # Allow access to and modification of classes and class members with modifiers during optimization -allowaccessmodification # Rename file source to "Sourcefile" string -renamesourcefileattribute SourceFile # Keep line number -keepattributes SourceFile,LineNumberTable # Keep generics -keepattributes Signature # Keep all class members that implement the serializable interface -keepclassmembers class * implements java.io.Serializable { static final long serialVersionUID; private static final java.io.ObjectStreamField[] serialPersistentFields; private void writeObject(java.io.ObjectOutputStream); private void readObject(java.io.ObjectInputStream); java.lang.Object writeReplace(); java.lang.Object readResolve(); } # Keep all class members that implement the percelable interface -keepclassmembers class * implements android.os.Parcelable { public static final ** CREATOR; public int describeContents(); public void writeToParcel(android.os.Parcel, int); } # Keep preference fragments -keep public class * extends androidx.preference.PreferenceFragmentCompat {} # Keep XmlPullParsers FIXME: Otherwise abstract method exception would occur -keep public class * extends org.xmlpull.v1.XmlPullParser { *; } -keep public class * extends org.xmlpull.v1.XmlSerializer { *; } # Don't minify server-related classes FIXME -keep public class io.github.muntashirakon.AppManager.servermanager.** { *; } -keep public class io.github.muntashirakon.AppManager.server.** { *; } -keep public class io.github.muntashirakon.AppManager.ipc.** { *; } # Don't minify debug-sepcific resource file -keep public class io.github.muntashirakon.AppManager.debug.R$raw {*;} # Don't minify OpenPGP API -keep public class org.openintents.openpgp.IOpenPgpService { *; } -keep public class org.openintents.openpgp.IOpenPgpService2 { *; } # Don't minify Spake2 library -keep public class io.github.muntashirakon.crypto.spake2.** { *; } # Don't minify AOSP private APIs -keep class android.** { *; } -keep class com.android.** { *; } -keep class libcore.util.** { *; } -keep class org.xmlpull.v1.** { *; } ================================================ FILE: app/schemas/io.github.muntashirakon.AppManager.db.AppsDb/1.json ================================================ { "formatVersion": 1, "database": { "version": 1, "identityHash": "d363fa70deeafe04c0457034f211df42", "entities": [ { "tableName": "app", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `user_id` INTEGER NOT NULL DEFAULT -10000, `label` TEXT, `version_name` TEXT, `version_code` INTEGER NOT NULL, `flags` INTEGER NOT NULL DEFAULT 0, `uid` INTEGER NOT NULL DEFAULT 0, `shared_uid` TEXT DEFAULT NULL, `first_install_time` INTEGER NOT NULL DEFAULT 0, `last_update_time` INTEGER NOT NULL DEFAULT 0, `target_sdk` INTEGER NOT NULL DEFAULT 0, `cert_name` TEXT DEFAULT '', `cert_algo` TEXT DEFAULT '', `is_installed` INTEGER NOT NULL DEFAULT true, `is_enabled` INTEGER NOT NULL DEFAULT false, `has_activities` INTEGER NOT NULL DEFAULT false, `has_splits` INTEGER NOT NULL DEFAULT false, `rules_count` INTEGER NOT NULL DEFAULT 0, `tracker_count` INTEGER NOT NULL DEFAULT 0, `last_action_time` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`package_name`, `user_id`))", "fields": [ { "fieldPath": "packageName", "columnName": "package_name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "userId", "columnName": "user_id", "affinity": "INTEGER", "notNull": true, "defaultValue": "-10000" }, { "fieldPath": "packageLabel", "columnName": "label", "affinity": "TEXT", "notNull": false }, { "fieldPath": "versionName", "columnName": "version_name", "affinity": "TEXT", "notNull": false }, { "fieldPath": "versionCode", "columnName": "version_code", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "flags", "columnName": "flags", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "uid", "columnName": "uid", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "sharedUserId", "columnName": "shared_uid", "affinity": "TEXT", "notNull": false, "defaultValue": "NULL" }, { "fieldPath": "firstInstallTime", "columnName": "first_install_time", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "lastUpdateTime", "columnName": "last_update_time", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "sdk", "columnName": "target_sdk", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "certName", "columnName": "cert_name", "affinity": "TEXT", "notNull": false, "defaultValue": "''" }, { "fieldPath": "certAlgo", "columnName": "cert_algo", "affinity": "TEXT", "notNull": false, "defaultValue": "''" }, { "fieldPath": "isInstalled", "columnName": "is_installed", "affinity": "INTEGER", "notNull": true, "defaultValue": "true" }, { "fieldPath": "isEnabled", "columnName": "is_enabled", "affinity": "INTEGER", "notNull": true, "defaultValue": "false" }, { "fieldPath": "hasActivities", "columnName": "has_activities", "affinity": "INTEGER", "notNull": true, "defaultValue": "false" }, { "fieldPath": "hasSplits", "columnName": "has_splits", "affinity": "INTEGER", "notNull": true, "defaultValue": "false" }, { "fieldPath": "rulesCount", "columnName": "rules_count", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "trackerCount", "columnName": "tracker_count", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "lastActionTime", "columnName": "last_action_time", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" } ], "primaryKey": { "columnNames": [ "package_name", "user_id" ], "autoGenerate": false }, "indices": [], "foreignKeys": [] }, { "tableName": "log_filter", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT)", "fields": [ { "fieldPath": "id", "columnName": "id", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "name", "columnName": "name", "affinity": "TEXT", "notNull": false } ], "primaryKey": { "columnNames": [ "id" ], "autoGenerate": true }, "indices": [ { "name": "index_name", "unique": true, "columnNames": [ "name" ], "orders": [], "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_name` ON `${TABLE_NAME}` (`name`)" } ], "foreignKeys": [] }, { "tableName": "file_hash", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`path` TEXT NOT NULL, `hash` TEXT, PRIMARY KEY(`path`))", "fields": [ { "fieldPath": "path", "columnName": "path", "affinity": "TEXT", "notNull": true }, { "fieldPath": "hash", "columnName": "hash", "affinity": "TEXT", "notNull": false } ], "primaryKey": { "columnNames": [ "path" ], "autoGenerate": false }, "indices": [], "foreignKeys": [] }, { "tableName": "backup", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `backup_name` TEXT NOT NULL, `label` TEXT, `version_name` TEXT, `version_code` INTEGER NOT NULL, `is_system` INTEGER NOT NULL, `has_splits` INTEGER NOT NULL, `has_rules` INTEGER NOT NULL, `backup_time` INTEGER NOT NULL, `crypto` TEXT, `meta_version` INTEGER NOT NULL, `flags` INTEGER NOT NULL, `user_id` INTEGER NOT NULL, `tar_type` TEXT, `has_key_store` INTEGER NOT NULL, `installer_app` TEXT, `info_hash` TEXT, PRIMARY KEY(`backup_name`, `package_name`))", "fields": [ { "fieldPath": "packageName", "columnName": "package_name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "backupName", "columnName": "backup_name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "label", "columnName": "label", "affinity": "TEXT", "notNull": false }, { "fieldPath": "versionName", "columnName": "version_name", "affinity": "TEXT", "notNull": false }, { "fieldPath": "versionCode", "columnName": "version_code", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "isSystem", "columnName": "is_system", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "hasSplits", "columnName": "has_splits", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "hasRules", "columnName": "has_rules", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "backupTime", "columnName": "backup_time", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "crypto", "columnName": "crypto", "affinity": "TEXT", "notNull": false }, { "fieldPath": "version", "columnName": "meta_version", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "flags", "columnName": "flags", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "userId", "columnName": "user_id", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "tarType", "columnName": "tar_type", "affinity": "TEXT", "notNull": false }, { "fieldPath": "hasKeyStore", "columnName": "has_key_store", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "installer", "columnName": "installer_app", "affinity": "TEXT", "notNull": false }, { "fieldPath": "hash", "columnName": "info_hash", "affinity": "TEXT", "notNull": false } ], "primaryKey": { "columnNames": [ "backup_name", "package_name" ], "autoGenerate": false }, "indices": [], "foreignKeys": [] } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd363fa70deeafe04c0457034f211df42')" ] } } ================================================ FILE: app/schemas/io.github.muntashirakon.AppManager.db.AppsDb/2.json ================================================ { "formatVersion": 1, "database": { "version": 2, "identityHash": "175f99951d829b88618a0e192ef9bedb", "entities": [ { "tableName": "app", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `user_id` INTEGER NOT NULL DEFAULT -10000, `label` TEXT, `version_name` TEXT, `version_code` INTEGER NOT NULL, `flags` INTEGER NOT NULL DEFAULT 0, `uid` INTEGER NOT NULL DEFAULT 0, `shared_uid` TEXT DEFAULT NULL, `first_install_time` INTEGER NOT NULL DEFAULT 0, `last_update_time` INTEGER NOT NULL DEFAULT 0, `target_sdk` INTEGER NOT NULL DEFAULT 0, `cert_name` TEXT DEFAULT '', `cert_algo` TEXT DEFAULT '', `is_installed` INTEGER NOT NULL DEFAULT true, `is_enabled` INTEGER NOT NULL DEFAULT false, `has_activities` INTEGER NOT NULL DEFAULT false, `has_splits` INTEGER NOT NULL DEFAULT false, `has_keystore` INTEGER NOT NULL DEFAULT false, `uses_saf` INTEGER NOT NULL DEFAULT false, `ssaid` TEXT DEFAULT '', `code_size` INTEGER NOT NULL DEFAULT 0, `data_size` INTEGER NOT NULL DEFAULT 0, `mobile_data` INTEGER NOT NULL DEFAULT 0, `wifi_data` INTEGER NOT NULL DEFAULT 0, `rules_count` INTEGER NOT NULL DEFAULT 0, `tracker_count` INTEGER NOT NULL DEFAULT 0, `open_count` INTEGER NOT NULL DEFAULT 0, `screen_time` INTEGER NOT NULL DEFAULT 0, `last_usage_time` INTEGER NOT NULL DEFAULT 0, `last_action_time` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`package_name`, `user_id`))", "fields": [ { "fieldPath": "packageName", "columnName": "package_name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "userId", "columnName": "user_id", "affinity": "INTEGER", "notNull": true, "defaultValue": "-10000" }, { "fieldPath": "packageLabel", "columnName": "label", "affinity": "TEXT", "notNull": false }, { "fieldPath": "versionName", "columnName": "version_name", "affinity": "TEXT", "notNull": false }, { "fieldPath": "versionCode", "columnName": "version_code", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "flags", "columnName": "flags", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "uid", "columnName": "uid", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "sharedUserId", "columnName": "shared_uid", "affinity": "TEXT", "notNull": false, "defaultValue": "NULL" }, { "fieldPath": "firstInstallTime", "columnName": "first_install_time", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "lastUpdateTime", "columnName": "last_update_time", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "sdk", "columnName": "target_sdk", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "certName", "columnName": "cert_name", "affinity": "TEXT", "notNull": false, "defaultValue": "''" }, { "fieldPath": "certAlgo", "columnName": "cert_algo", "affinity": "TEXT", "notNull": false, "defaultValue": "''" }, { "fieldPath": "isInstalled", "columnName": "is_installed", "affinity": "INTEGER", "notNull": true, "defaultValue": "true" }, { "fieldPath": "isEnabled", "columnName": "is_enabled", "affinity": "INTEGER", "notNull": true, "defaultValue": "false" }, { "fieldPath": "hasActivities", "columnName": "has_activities", "affinity": "INTEGER", "notNull": true, "defaultValue": "false" }, { "fieldPath": "hasSplits", "columnName": "has_splits", "affinity": "INTEGER", "notNull": true, "defaultValue": "false" }, { "fieldPath": "hasKeystore", "columnName": "has_keystore", "affinity": "INTEGER", "notNull": true, "defaultValue": "false" }, { "fieldPath": "usesSaf", "columnName": "uses_saf", "affinity": "INTEGER", "notNull": true, "defaultValue": "false" }, { "fieldPath": "ssaid", "columnName": "ssaid", "affinity": "TEXT", "notNull": false, "defaultValue": "''" }, { "fieldPath": "codeSize", "columnName": "code_size", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "dataSize", "columnName": "data_size", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "mobileDataUsage", "columnName": "mobile_data", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "wifiDataUsage", "columnName": "wifi_data", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "rulesCount", "columnName": "rules_count", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "trackerCount", "columnName": "tracker_count", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "openCount", "columnName": "open_count", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "screenTime", "columnName": "screen_time", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "lastUsageTime", "columnName": "last_usage_time", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "lastActionTime", "columnName": "last_action_time", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" } ], "primaryKey": { "columnNames": [ "package_name", "user_id" ], "autoGenerate": false }, "indices": [], "foreignKeys": [] }, { "tableName": "log_filter", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT)", "fields": [ { "fieldPath": "id", "columnName": "id", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "name", "columnName": "name", "affinity": "TEXT", "notNull": false } ], "primaryKey": { "columnNames": [ "id" ], "autoGenerate": true }, "indices": [ { "name": "index_name", "unique": true, "columnNames": [ "name" ], "orders": [], "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_name` ON `${TABLE_NAME}` (`name`)" } ], "foreignKeys": [] }, { "tableName": "file_hash", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`path` TEXT NOT NULL, `hash` TEXT, PRIMARY KEY(`path`))", "fields": [ { "fieldPath": "path", "columnName": "path", "affinity": "TEXT", "notNull": true }, { "fieldPath": "hash", "columnName": "hash", "affinity": "TEXT", "notNull": false } ], "primaryKey": { "columnNames": [ "path" ], "autoGenerate": false }, "indices": [], "foreignKeys": [] }, { "tableName": "backup", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `backup_name` TEXT NOT NULL, `label` TEXT, `version_name` TEXT, `version_code` INTEGER NOT NULL, `is_system` INTEGER NOT NULL, `has_splits` INTEGER NOT NULL, `has_rules` INTEGER NOT NULL, `backup_time` INTEGER NOT NULL, `crypto` TEXT, `meta_version` INTEGER NOT NULL, `flags` INTEGER NOT NULL, `user_id` INTEGER NOT NULL, `tar_type` TEXT, `has_key_store` INTEGER NOT NULL, `installer_app` TEXT, `info_hash` TEXT, PRIMARY KEY(`backup_name`, `package_name`))", "fields": [ { "fieldPath": "packageName", "columnName": "package_name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "backupName", "columnName": "backup_name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "label", "columnName": "label", "affinity": "TEXT", "notNull": false }, { "fieldPath": "versionName", "columnName": "version_name", "affinity": "TEXT", "notNull": false }, { "fieldPath": "versionCode", "columnName": "version_code", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "isSystem", "columnName": "is_system", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "hasSplits", "columnName": "has_splits", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "hasRules", "columnName": "has_rules", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "backupTime", "columnName": "backup_time", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "crypto", "columnName": "crypto", "affinity": "TEXT", "notNull": false }, { "fieldPath": "version", "columnName": "meta_version", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "flags", "columnName": "flags", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "userId", "columnName": "user_id", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "tarType", "columnName": "tar_type", "affinity": "TEXT", "notNull": false }, { "fieldPath": "hasKeyStore", "columnName": "has_key_store", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "installer", "columnName": "installer_app", "affinity": "TEXT", "notNull": false }, { "fieldPath": "hash", "columnName": "info_hash", "affinity": "TEXT", "notNull": false } ], "primaryKey": { "columnNames": [ "backup_name", "package_name" ], "autoGenerate": false }, "indices": [], "foreignKeys": [] } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '175f99951d829b88618a0e192ef9bedb')" ] } } ================================================ FILE: app/schemas/io.github.muntashirakon.AppManager.db.AppsDb/3.json ================================================ { "formatVersion": 1, "database": { "version": 3, "identityHash": "ac92def333f7b8a38eb1ceab89033e99", "entities": [ { "tableName": "app", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `user_id` INTEGER NOT NULL DEFAULT -10000, `label` TEXT, `version_name` TEXT, `version_code` INTEGER NOT NULL, `flags` INTEGER NOT NULL DEFAULT 0, `uid` INTEGER NOT NULL DEFAULT 0, `shared_uid` TEXT DEFAULT NULL, `first_install_time` INTEGER NOT NULL DEFAULT 0, `last_update_time` INTEGER NOT NULL DEFAULT 0, `target_sdk` INTEGER NOT NULL DEFAULT 0, `cert_name` TEXT DEFAULT '', `cert_algo` TEXT DEFAULT '', `is_installed` INTEGER NOT NULL DEFAULT true, `is_enabled` INTEGER NOT NULL DEFAULT false, `has_activities` INTEGER NOT NULL DEFAULT false, `has_splits` INTEGER NOT NULL DEFAULT false, `has_keystore` INTEGER NOT NULL DEFAULT false, `uses_saf` INTEGER NOT NULL DEFAULT false, `ssaid` TEXT DEFAULT '', `code_size` INTEGER NOT NULL DEFAULT 0, `data_size` INTEGER NOT NULL DEFAULT 0, `mobile_data` INTEGER NOT NULL DEFAULT 0, `wifi_data` INTEGER NOT NULL DEFAULT 0, `rules_count` INTEGER NOT NULL DEFAULT 0, `tracker_count` INTEGER NOT NULL DEFAULT 0, `open_count` INTEGER NOT NULL DEFAULT 0, `screen_time` INTEGER NOT NULL DEFAULT 0, `last_usage_time` INTEGER NOT NULL DEFAULT 0, `last_action_time` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`package_name`, `user_id`))", "fields": [ { "fieldPath": "packageName", "columnName": "package_name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "userId", "columnName": "user_id", "affinity": "INTEGER", "notNull": true, "defaultValue": "-10000" }, { "fieldPath": "packageLabel", "columnName": "label", "affinity": "TEXT", "notNull": false }, { "fieldPath": "versionName", "columnName": "version_name", "affinity": "TEXT", "notNull": false }, { "fieldPath": "versionCode", "columnName": "version_code", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "flags", "columnName": "flags", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "uid", "columnName": "uid", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "sharedUserId", "columnName": "shared_uid", "affinity": "TEXT", "notNull": false, "defaultValue": "NULL" }, { "fieldPath": "firstInstallTime", "columnName": "first_install_time", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "lastUpdateTime", "columnName": "last_update_time", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "sdk", "columnName": "target_sdk", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "certName", "columnName": "cert_name", "affinity": "TEXT", "notNull": false, "defaultValue": "''" }, { "fieldPath": "certAlgo", "columnName": "cert_algo", "affinity": "TEXT", "notNull": false, "defaultValue": "''" }, { "fieldPath": "isInstalled", "columnName": "is_installed", "affinity": "INTEGER", "notNull": true, "defaultValue": "true" }, { "fieldPath": "isEnabled", "columnName": "is_enabled", "affinity": "INTEGER", "notNull": true, "defaultValue": "false" }, { "fieldPath": "hasActivities", "columnName": "has_activities", "affinity": "INTEGER", "notNull": true, "defaultValue": "false" }, { "fieldPath": "hasSplits", "columnName": "has_splits", "affinity": "INTEGER", "notNull": true, "defaultValue": "false" }, { "fieldPath": "hasKeystore", "columnName": "has_keystore", "affinity": "INTEGER", "notNull": true, "defaultValue": "false" }, { "fieldPath": "usesSaf", "columnName": "uses_saf", "affinity": "INTEGER", "notNull": true, "defaultValue": "false" }, { "fieldPath": "ssaid", "columnName": "ssaid", "affinity": "TEXT", "notNull": false, "defaultValue": "''" }, { "fieldPath": "codeSize", "columnName": "code_size", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "dataSize", "columnName": "data_size", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "mobileDataUsage", "columnName": "mobile_data", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "wifiDataUsage", "columnName": "wifi_data", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "rulesCount", "columnName": "rules_count", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "trackerCount", "columnName": "tracker_count", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "openCount", "columnName": "open_count", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "screenTime", "columnName": "screen_time", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "lastUsageTime", "columnName": "last_usage_time", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "lastActionTime", "columnName": "last_action_time", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" } ], "primaryKey": { "autoGenerate": false, "columnNames": [ "package_name", "user_id" ] }, "indices": [], "foreignKeys": [] }, { "tableName": "log_filter", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT)", "fields": [ { "fieldPath": "id", "columnName": "id", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "name", "columnName": "name", "affinity": "TEXT", "notNull": false } ], "primaryKey": { "autoGenerate": true, "columnNames": [ "id" ] }, "indices": [ { "name": "index_name", "unique": true, "columnNames": [ "name" ], "orders": [], "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_name` ON `${TABLE_NAME}` (`name`)" } ], "foreignKeys": [] }, { "tableName": "file_hash", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`path` TEXT NOT NULL, `hash` TEXT, PRIMARY KEY(`path`))", "fields": [ { "fieldPath": "path", "columnName": "path", "affinity": "TEXT", "notNull": true }, { "fieldPath": "hash", "columnName": "hash", "affinity": "TEXT", "notNull": false } ], "primaryKey": { "autoGenerate": false, "columnNames": [ "path" ] }, "indices": [], "foreignKeys": [] }, { "tableName": "backup", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `backup_name` TEXT NOT NULL, `label` TEXT, `version_name` TEXT, `version_code` INTEGER NOT NULL, `is_system` INTEGER NOT NULL, `has_splits` INTEGER NOT NULL, `has_rules` INTEGER NOT NULL, `backup_time` INTEGER NOT NULL, `crypto` TEXT, `meta_version` INTEGER NOT NULL, `flags` INTEGER NOT NULL, `user_id` INTEGER NOT NULL, `tar_type` TEXT, `has_key_store` INTEGER NOT NULL, `installer_app` TEXT, `info_hash` TEXT, PRIMARY KEY(`backup_name`, `package_name`))", "fields": [ { "fieldPath": "packageName", "columnName": "package_name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "backupName", "columnName": "backup_name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "label", "columnName": "label", "affinity": "TEXT", "notNull": false }, { "fieldPath": "versionName", "columnName": "version_name", "affinity": "TEXT", "notNull": false }, { "fieldPath": "versionCode", "columnName": "version_code", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "isSystem", "columnName": "is_system", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "hasSplits", "columnName": "has_splits", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "hasRules", "columnName": "has_rules", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "backupTime", "columnName": "backup_time", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "crypto", "columnName": "crypto", "affinity": "TEXT", "notNull": false }, { "fieldPath": "version", "columnName": "meta_version", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "flags", "columnName": "flags", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "userId", "columnName": "user_id", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "tarType", "columnName": "tar_type", "affinity": "TEXT", "notNull": false }, { "fieldPath": "hasKeyStore", "columnName": "has_key_store", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "installer", "columnName": "installer_app", "affinity": "TEXT", "notNull": false }, { "fieldPath": "uuid", "columnName": "info_hash", "affinity": "TEXT", "notNull": false } ], "primaryKey": { "autoGenerate": false, "columnNames": [ "backup_name", "package_name" ] }, "indices": [], "foreignKeys": [] }, { "tableName": "op_history", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` TEXT NOT NULL, `time` INTEGER NOT NULL, `data` TEXT NOT NULL, `status` TEXT NOT NULL, `extra` TEXT)", "fields": [ { "fieldPath": "id", "columnName": "id", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "type", "columnName": "type", "affinity": "TEXT", "notNull": true }, { "fieldPath": "execTime", "columnName": "time", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "serializedData", "columnName": "data", "affinity": "TEXT", "notNull": true }, { "fieldPath": "status", "columnName": "status", "affinity": "TEXT", "notNull": true }, { "fieldPath": "serializedExtra", "columnName": "extra", "affinity": "TEXT", "notNull": false } ], "primaryKey": { "autoGenerate": true, "columnNames": [ "id" ] }, "indices": [], "foreignKeys": [] } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ac92def333f7b8a38eb1ceab89033e99')" ] } } ================================================ FILE: app/schemas/io.github.muntashirakon.AppManager.db.AppsDb/4.json ================================================ { "formatVersion": 1, "database": { "version": 4, "identityHash": "7bb1cd35f0800b965e5a475458918b80", "entities": [ { "tableName": "app", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `user_id` INTEGER NOT NULL DEFAULT -10000, `label` TEXT, `version_name` TEXT, `version_code` INTEGER NOT NULL, `flags` INTEGER NOT NULL DEFAULT 0, `uid` INTEGER NOT NULL DEFAULT 0, `shared_uid` TEXT DEFAULT NULL, `first_install_time` INTEGER NOT NULL DEFAULT 0, `last_update_time` INTEGER NOT NULL DEFAULT 0, `target_sdk` INTEGER NOT NULL DEFAULT 0, `cert_name` TEXT DEFAULT '', `cert_algo` TEXT DEFAULT '', `is_installed` INTEGER NOT NULL DEFAULT true, `is_enabled` INTEGER NOT NULL DEFAULT false, `has_activities` INTEGER NOT NULL DEFAULT false, `has_splits` INTEGER NOT NULL DEFAULT false, `has_keystore` INTEGER NOT NULL DEFAULT false, `uses_saf` INTEGER NOT NULL DEFAULT false, `ssaid` TEXT DEFAULT '', `code_size` INTEGER NOT NULL DEFAULT 0, `data_size` INTEGER NOT NULL DEFAULT 0, `mobile_data` INTEGER NOT NULL DEFAULT 0, `wifi_data` INTEGER NOT NULL DEFAULT 0, `rules_count` INTEGER NOT NULL DEFAULT 0, `tracker_count` INTEGER NOT NULL DEFAULT 0, `open_count` INTEGER NOT NULL DEFAULT 0, `screen_time` INTEGER NOT NULL DEFAULT 0, `last_usage_time` INTEGER NOT NULL DEFAULT 0, `last_action_time` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`package_name`, `user_id`))", "fields": [ { "fieldPath": "packageName", "columnName": "package_name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "userId", "columnName": "user_id", "affinity": "INTEGER", "notNull": true, "defaultValue": "-10000" }, { "fieldPath": "packageLabel", "columnName": "label", "affinity": "TEXT", "notNull": false }, { "fieldPath": "versionName", "columnName": "version_name", "affinity": "TEXT", "notNull": false }, { "fieldPath": "versionCode", "columnName": "version_code", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "flags", "columnName": "flags", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "uid", "columnName": "uid", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "sharedUserId", "columnName": "shared_uid", "affinity": "TEXT", "notNull": false, "defaultValue": "NULL" }, { "fieldPath": "firstInstallTime", "columnName": "first_install_time", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "lastUpdateTime", "columnName": "last_update_time", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "sdk", "columnName": "target_sdk", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "certName", "columnName": "cert_name", "affinity": "TEXT", "notNull": false, "defaultValue": "''" }, { "fieldPath": "certAlgo", "columnName": "cert_algo", "affinity": "TEXT", "notNull": false, "defaultValue": "''" }, { "fieldPath": "isInstalled", "columnName": "is_installed", "affinity": "INTEGER", "notNull": true, "defaultValue": "true" }, { "fieldPath": "isEnabled", "columnName": "is_enabled", "affinity": "INTEGER", "notNull": true, "defaultValue": "false" }, { "fieldPath": "hasActivities", "columnName": "has_activities", "affinity": "INTEGER", "notNull": true, "defaultValue": "false" }, { "fieldPath": "hasSplits", "columnName": "has_splits", "affinity": "INTEGER", "notNull": true, "defaultValue": "false" }, { "fieldPath": "hasKeystore", "columnName": "has_keystore", "affinity": "INTEGER", "notNull": true, "defaultValue": "false" }, { "fieldPath": "usesSaf", "columnName": "uses_saf", "affinity": "INTEGER", "notNull": true, "defaultValue": "false" }, { "fieldPath": "ssaid", "columnName": "ssaid", "affinity": "TEXT", "notNull": false, "defaultValue": "''" }, { "fieldPath": "codeSize", "columnName": "code_size", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "dataSize", "columnName": "data_size", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "mobileDataUsage", "columnName": "mobile_data", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "wifiDataUsage", "columnName": "wifi_data", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "rulesCount", "columnName": "rules_count", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "trackerCount", "columnName": "tracker_count", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "openCount", "columnName": "open_count", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "screenTime", "columnName": "screen_time", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "lastUsageTime", "columnName": "last_usage_time", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "lastActionTime", "columnName": "last_action_time", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" } ], "primaryKey": { "autoGenerate": false, "columnNames": [ "package_name", "user_id" ] }, "indices": [], "foreignKeys": [] }, { "tableName": "log_filter", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT)", "fields": [ { "fieldPath": "id", "columnName": "id", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "name", "columnName": "name", "affinity": "TEXT", "notNull": false } ], "primaryKey": { "autoGenerate": true, "columnNames": [ "id" ] }, "indices": [ { "name": "index_name", "unique": true, "columnNames": [ "name" ], "orders": [], "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_name` ON `${TABLE_NAME}` (`name`)" } ], "foreignKeys": [] }, { "tableName": "file_hash", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`path` TEXT NOT NULL, `hash` TEXT, PRIMARY KEY(`path`))", "fields": [ { "fieldPath": "path", "columnName": "path", "affinity": "TEXT", "notNull": true }, { "fieldPath": "hash", "columnName": "hash", "affinity": "TEXT", "notNull": false } ], "primaryKey": { "autoGenerate": false, "columnNames": [ "path" ] }, "indices": [], "foreignKeys": [] }, { "tableName": "backup", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `backup_name` TEXT NOT NULL, `label` TEXT, `version_name` TEXT, `version_code` INTEGER NOT NULL, `is_system` INTEGER NOT NULL, `has_splits` INTEGER NOT NULL, `has_rules` INTEGER NOT NULL, `backup_time` INTEGER NOT NULL, `crypto` TEXT, `meta_version` INTEGER NOT NULL, `flags` INTEGER NOT NULL, `user_id` INTEGER NOT NULL, `tar_type` TEXT, `has_key_store` INTEGER NOT NULL, `installer_app` TEXT, `info_hash` TEXT, PRIMARY KEY(`backup_name`, `package_name`))", "fields": [ { "fieldPath": "packageName", "columnName": "package_name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "backupName", "columnName": "backup_name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "label", "columnName": "label", "affinity": "TEXT", "notNull": false }, { "fieldPath": "versionName", "columnName": "version_name", "affinity": "TEXT", "notNull": false }, { "fieldPath": "versionCode", "columnName": "version_code", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "isSystem", "columnName": "is_system", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "hasSplits", "columnName": "has_splits", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "hasRules", "columnName": "has_rules", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "backupTime", "columnName": "backup_time", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "crypto", "columnName": "crypto", "affinity": "TEXT", "notNull": false }, { "fieldPath": "version", "columnName": "meta_version", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "flags", "columnName": "flags", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "userId", "columnName": "user_id", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "tarType", "columnName": "tar_type", "affinity": "TEXT", "notNull": false }, { "fieldPath": "hasKeyStore", "columnName": "has_key_store", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "installer", "columnName": "installer_app", "affinity": "TEXT", "notNull": false }, { "fieldPath": "uuid", "columnName": "info_hash", "affinity": "TEXT", "notNull": false } ], "primaryKey": { "autoGenerate": false, "columnNames": [ "backup_name", "package_name" ] }, "indices": [], "foreignKeys": [] }, { "tableName": "op_history", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` TEXT NOT NULL, `time` INTEGER NOT NULL, `data` TEXT NOT NULL, `status` TEXT NOT NULL, `extra` TEXT)", "fields": [ { "fieldPath": "id", "columnName": "id", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "type", "columnName": "type", "affinity": "TEXT", "notNull": true }, { "fieldPath": "execTime", "columnName": "time", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "serializedData", "columnName": "data", "affinity": "TEXT", "notNull": true }, { "fieldPath": "status", "columnName": "status", "affinity": "TEXT", "notNull": true }, { "fieldPath": "serializedExtra", "columnName": "extra", "affinity": "TEXT", "notNull": false } ], "primaryKey": { "autoGenerate": true, "columnNames": [ "id" ] }, "indices": [], "foreignKeys": [] }, { "tableName": "fm_favorite", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `uri` TEXT NOT NULL, `init_uri` TEXT, `options` INTEGER NOT NULL, `order` INTEGER NOT NULL, `type` INTEGER NOT NULL)", "fields": [ { "fieldPath": "id", "columnName": "id", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "name", "columnName": "name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "uri", "columnName": "uri", "affinity": "TEXT", "notNull": true }, { "fieldPath": "initUri", "columnName": "init_uri", "affinity": "TEXT", "notNull": false }, { "fieldPath": "options", "columnName": "options", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "order", "columnName": "order", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "type", "columnName": "type", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "autoGenerate": true, "columnNames": [ "id" ] }, "indices": [], "foreignKeys": [] } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7bb1cd35f0800b965e5a475458918b80')" ] } } ================================================ FILE: app/schemas/io.github.muntashirakon.AppManager.db.AppsDb/5.json ================================================ { "formatVersion": 1, "database": { "version": 5, "identityHash": "c692d8d6e1657fb5d1332a3fb631e9d7", "entities": [ { "tableName": "app", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `user_id` INTEGER NOT NULL DEFAULT -10000, `label` TEXT, `version_name` TEXT, `version_code` INTEGER NOT NULL, `flags` INTEGER NOT NULL DEFAULT 0, `uid` INTEGER NOT NULL DEFAULT 0, `shared_uid` TEXT DEFAULT NULL, `first_install_time` INTEGER NOT NULL DEFAULT 0, `last_update_time` INTEGER NOT NULL DEFAULT 0, `target_sdk` INTEGER NOT NULL DEFAULT 0, `cert_name` TEXT DEFAULT '', `cert_algo` TEXT DEFAULT '', `is_installed` INTEGER NOT NULL DEFAULT true, `is_enabled` INTEGER NOT NULL DEFAULT false, `has_activities` INTEGER NOT NULL DEFAULT false, `has_splits` INTEGER NOT NULL DEFAULT false, `has_keystore` INTEGER NOT NULL DEFAULT false, `uses_saf` INTEGER NOT NULL DEFAULT false, `ssaid` TEXT DEFAULT '', `code_size` INTEGER NOT NULL DEFAULT 0, `data_size` INTEGER NOT NULL DEFAULT 0, `mobile_data` INTEGER NOT NULL DEFAULT 0, `wifi_data` INTEGER NOT NULL DEFAULT 0, `rules_count` INTEGER NOT NULL DEFAULT 0, `tracker_count` INTEGER NOT NULL DEFAULT 0, `open_count` INTEGER NOT NULL DEFAULT 0, `screen_time` INTEGER NOT NULL DEFAULT 0, `last_usage_time` INTEGER NOT NULL DEFAULT 0, `last_action_time` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`package_name`, `user_id`))", "fields": [ { "fieldPath": "packageName", "columnName": "package_name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "userId", "columnName": "user_id", "affinity": "INTEGER", "notNull": true, "defaultValue": "-10000" }, { "fieldPath": "packageLabel", "columnName": "label", "affinity": "TEXT", "notNull": false }, { "fieldPath": "versionName", "columnName": "version_name", "affinity": "TEXT", "notNull": false }, { "fieldPath": "versionCode", "columnName": "version_code", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "flags", "columnName": "flags", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "uid", "columnName": "uid", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "sharedUserId", "columnName": "shared_uid", "affinity": "TEXT", "notNull": false, "defaultValue": "NULL" }, { "fieldPath": "firstInstallTime", "columnName": "first_install_time", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "lastUpdateTime", "columnName": "last_update_time", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "sdk", "columnName": "target_sdk", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "certName", "columnName": "cert_name", "affinity": "TEXT", "notNull": false, "defaultValue": "''" }, { "fieldPath": "certAlgo", "columnName": "cert_algo", "affinity": "TEXT", "notNull": false, "defaultValue": "''" }, { "fieldPath": "isInstalled", "columnName": "is_installed", "affinity": "INTEGER", "notNull": true, "defaultValue": "true" }, { "fieldPath": "isEnabled", "columnName": "is_enabled", "affinity": "INTEGER", "notNull": true, "defaultValue": "false" }, { "fieldPath": "hasActivities", "columnName": "has_activities", "affinity": "INTEGER", "notNull": true, "defaultValue": "false" }, { "fieldPath": "hasSplits", "columnName": "has_splits", "affinity": "INTEGER", "notNull": true, "defaultValue": "false" }, { "fieldPath": "hasKeystore", "columnName": "has_keystore", "affinity": "INTEGER", "notNull": true, "defaultValue": "false" }, { "fieldPath": "usesSaf", "columnName": "uses_saf", "affinity": "INTEGER", "notNull": true, "defaultValue": "false" }, { "fieldPath": "ssaid", "columnName": "ssaid", "affinity": "TEXT", "notNull": false, "defaultValue": "''" }, { "fieldPath": "codeSize", "columnName": "code_size", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "dataSize", "columnName": "data_size", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "mobileDataUsage", "columnName": "mobile_data", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "wifiDataUsage", "columnName": "wifi_data", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "rulesCount", "columnName": "rules_count", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "trackerCount", "columnName": "tracker_count", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "openCount", "columnName": "open_count", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "screenTime", "columnName": "screen_time", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "lastUsageTime", "columnName": "last_usage_time", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "lastActionTime", "columnName": "last_action_time", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" } ], "primaryKey": { "autoGenerate": false, "columnNames": [ "package_name", "user_id" ] }, "indices": [], "foreignKeys": [] }, { "tableName": "log_filter", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT)", "fields": [ { "fieldPath": "id", "columnName": "id", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "name", "columnName": "name", "affinity": "TEXT", "notNull": false } ], "primaryKey": { "autoGenerate": true, "columnNames": [ "id" ] }, "indices": [ { "name": "index_name", "unique": true, "columnNames": [ "name" ], "orders": [], "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_name` ON `${TABLE_NAME}` (`name`)" } ], "foreignKeys": [] }, { "tableName": "file_hash", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`path` TEXT NOT NULL, `hash` TEXT, PRIMARY KEY(`path`))", "fields": [ { "fieldPath": "path", "columnName": "path", "affinity": "TEXT", "notNull": true }, { "fieldPath": "hash", "columnName": "hash", "affinity": "TEXT", "notNull": false } ], "primaryKey": { "autoGenerate": false, "columnNames": [ "path" ] }, "indices": [], "foreignKeys": [] }, { "tableName": "backup", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `backup_name` TEXT NOT NULL, `label` TEXT, `version_name` TEXT, `version_code` INTEGER NOT NULL, `is_system` INTEGER NOT NULL, `has_splits` INTEGER NOT NULL, `has_rules` INTEGER NOT NULL, `backup_time` INTEGER NOT NULL, `crypto` TEXT, `meta_version` INTEGER NOT NULL, `flags` INTEGER NOT NULL, `user_id` INTEGER NOT NULL, `tar_type` TEXT, `has_key_store` INTEGER NOT NULL, `installer_app` TEXT, `info_hash` TEXT, PRIMARY KEY(`backup_name`, `package_name`))", "fields": [ { "fieldPath": "packageName", "columnName": "package_name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "backupName", "columnName": "backup_name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "label", "columnName": "label", "affinity": "TEXT", "notNull": false }, { "fieldPath": "versionName", "columnName": "version_name", "affinity": "TEXT", "notNull": false }, { "fieldPath": "versionCode", "columnName": "version_code", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "isSystem", "columnName": "is_system", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "hasSplits", "columnName": "has_splits", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "hasRules", "columnName": "has_rules", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "backupTime", "columnName": "backup_time", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "crypto", "columnName": "crypto", "affinity": "TEXT", "notNull": false }, { "fieldPath": "version", "columnName": "meta_version", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "flags", "columnName": "flags", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "userId", "columnName": "user_id", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "tarType", "columnName": "tar_type", "affinity": "TEXT", "notNull": false }, { "fieldPath": "hasKeyStore", "columnName": "has_key_store", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "installer", "columnName": "installer_app", "affinity": "TEXT", "notNull": false }, { "fieldPath": "uuid", "columnName": "info_hash", "affinity": "TEXT", "notNull": false } ], "primaryKey": { "autoGenerate": false, "columnNames": [ "backup_name", "package_name" ] }, "indices": [], "foreignKeys": [] }, { "tableName": "op_history", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` TEXT NOT NULL, `time` INTEGER NOT NULL, `data` TEXT NOT NULL, `status` TEXT NOT NULL, `extra` TEXT)", "fields": [ { "fieldPath": "id", "columnName": "id", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "type", "columnName": "type", "affinity": "TEXT", "notNull": true }, { "fieldPath": "execTime", "columnName": "time", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "serializedData", "columnName": "data", "affinity": "TEXT", "notNull": true }, { "fieldPath": "status", "columnName": "status", "affinity": "TEXT", "notNull": true }, { "fieldPath": "serializedExtra", "columnName": "extra", "affinity": "TEXT", "notNull": false } ], "primaryKey": { "autoGenerate": true, "columnNames": [ "id" ] }, "indices": [], "foreignKeys": [] }, { "tableName": "fm_favorite", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `uri` TEXT NOT NULL, `init_uri` TEXT, `options` INTEGER NOT NULL, `order` INTEGER NOT NULL, `type` INTEGER NOT NULL)", "fields": [ { "fieldPath": "id", "columnName": "id", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "name", "columnName": "name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "uri", "columnName": "uri", "affinity": "TEXT", "notNull": true }, { "fieldPath": "initUri", "columnName": "init_uri", "affinity": "TEXT", "notNull": false }, { "fieldPath": "options", "columnName": "options", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "order", "columnName": "order", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "type", "columnName": "type", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "autoGenerate": true, "columnNames": [ "id" ] }, "indices": [], "foreignKeys": [] }, { "tableName": "freeze_type", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `type` INTEGER NOT NULL, PRIMARY KEY(`package_name`))", "fields": [ { "fieldPath": "packageName", "columnName": "package_name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "type", "columnName": "type", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "autoGenerate": false, "columnNames": [ "package_name" ] }, "indices": [], "foreignKeys": [] } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c692d8d6e1657fb5d1332a3fb631e9d7')" ] } } ================================================ FILE: app/schemas/io.github.muntashirakon.AppManager.db.AppsDb/6.json ================================================ { "formatVersion": 1, "database": { "version": 6, "identityHash": "f72fb62329689ce1c1c5cda5c0ede949", "entities": [ { "tableName": "app", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `user_id` INTEGER NOT NULL DEFAULT -10000, `label` TEXT, `version_name` TEXT, `version_code` INTEGER NOT NULL, `flags` INTEGER NOT NULL DEFAULT 0, `uid` INTEGER NOT NULL DEFAULT 0, `shared_uid` TEXT DEFAULT NULL, `first_install_time` INTEGER NOT NULL DEFAULT 0, `last_update_time` INTEGER NOT NULL DEFAULT 0, `target_sdk` INTEGER NOT NULL DEFAULT 0, `cert_name` TEXT DEFAULT '', `cert_algo` TEXT DEFAULT '', `is_installed` INTEGER NOT NULL DEFAULT true, `is_only_data_installed` INTEGER NOT NULL DEFAULT 0, `is_enabled` INTEGER NOT NULL DEFAULT false, `has_activities` INTEGER NOT NULL DEFAULT false, `has_splits` INTEGER NOT NULL DEFAULT false, `has_keystore` INTEGER NOT NULL DEFAULT false, `uses_saf` INTEGER NOT NULL DEFAULT false, `ssaid` TEXT DEFAULT '', `code_size` INTEGER NOT NULL DEFAULT 0, `data_size` INTEGER NOT NULL DEFAULT 0, `mobile_data` INTEGER NOT NULL DEFAULT 0, `wifi_data` INTEGER NOT NULL DEFAULT 0, `rules_count` INTEGER NOT NULL DEFAULT 0, `tracker_count` INTEGER NOT NULL DEFAULT 0, `open_count` INTEGER NOT NULL DEFAULT 0, `screen_time` INTEGER NOT NULL DEFAULT 0, `last_usage_time` INTEGER NOT NULL DEFAULT 0, `last_action_time` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`package_name`, `user_id`))", "fields": [ { "fieldPath": "packageName", "columnName": "package_name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "userId", "columnName": "user_id", "affinity": "INTEGER", "notNull": true, "defaultValue": "-10000" }, { "fieldPath": "packageLabel", "columnName": "label", "affinity": "TEXT", "notNull": false }, { "fieldPath": "versionName", "columnName": "version_name", "affinity": "TEXT", "notNull": false }, { "fieldPath": "versionCode", "columnName": "version_code", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "flags", "columnName": "flags", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "uid", "columnName": "uid", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "sharedUserId", "columnName": "shared_uid", "affinity": "TEXT", "notNull": false, "defaultValue": "NULL" }, { "fieldPath": "firstInstallTime", "columnName": "first_install_time", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "lastUpdateTime", "columnName": "last_update_time", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "sdk", "columnName": "target_sdk", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "certName", "columnName": "cert_name", "affinity": "TEXT", "notNull": false, "defaultValue": "''" }, { "fieldPath": "certAlgo", "columnName": "cert_algo", "affinity": "TEXT", "notNull": false, "defaultValue": "''" }, { "fieldPath": "isInstalled", "columnName": "is_installed", "affinity": "INTEGER", "notNull": true, "defaultValue": "true" }, { "fieldPath": "isOnlyDataInstalled", "columnName": "is_only_data_installed", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "isEnabled", "columnName": "is_enabled", "affinity": "INTEGER", "notNull": true, "defaultValue": "false" }, { "fieldPath": "hasActivities", "columnName": "has_activities", "affinity": "INTEGER", "notNull": true, "defaultValue": "false" }, { "fieldPath": "hasSplits", "columnName": "has_splits", "affinity": "INTEGER", "notNull": true, "defaultValue": "false" }, { "fieldPath": "hasKeystore", "columnName": "has_keystore", "affinity": "INTEGER", "notNull": true, "defaultValue": "false" }, { "fieldPath": "usesSaf", "columnName": "uses_saf", "affinity": "INTEGER", "notNull": true, "defaultValue": "false" }, { "fieldPath": "ssaid", "columnName": "ssaid", "affinity": "TEXT", "notNull": false, "defaultValue": "''" }, { "fieldPath": "codeSize", "columnName": "code_size", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "dataSize", "columnName": "data_size", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "mobileDataUsage", "columnName": "mobile_data", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "wifiDataUsage", "columnName": "wifi_data", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "rulesCount", "columnName": "rules_count", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "trackerCount", "columnName": "tracker_count", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "openCount", "columnName": "open_count", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "screenTime", "columnName": "screen_time", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "lastUsageTime", "columnName": "last_usage_time", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "lastActionTime", "columnName": "last_action_time", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" } ], "primaryKey": { "autoGenerate": false, "columnNames": [ "package_name", "user_id" ] }, "indices": [], "foreignKeys": [] }, { "tableName": "log_filter", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT)", "fields": [ { "fieldPath": "id", "columnName": "id", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "name", "columnName": "name", "affinity": "TEXT", "notNull": false } ], "primaryKey": { "autoGenerate": true, "columnNames": [ "id" ] }, "indices": [ { "name": "index_name", "unique": true, "columnNames": [ "name" ], "orders": [], "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_name` ON `${TABLE_NAME}` (`name`)" } ], "foreignKeys": [] }, { "tableName": "file_hash", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`path` TEXT NOT NULL, `hash` TEXT, PRIMARY KEY(`path`))", "fields": [ { "fieldPath": "path", "columnName": "path", "affinity": "TEXT", "notNull": true }, { "fieldPath": "hash", "columnName": "hash", "affinity": "TEXT", "notNull": false } ], "primaryKey": { "autoGenerate": false, "columnNames": [ "path" ] }, "indices": [], "foreignKeys": [] }, { "tableName": "backup", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `backup_name` TEXT NOT NULL, `label` TEXT, `version_name` TEXT, `version_code` INTEGER NOT NULL, `is_system` INTEGER NOT NULL, `has_splits` INTEGER NOT NULL, `has_rules` INTEGER NOT NULL, `backup_time` INTEGER NOT NULL, `crypto` TEXT, `meta_version` INTEGER NOT NULL, `flags` INTEGER NOT NULL, `user_id` INTEGER NOT NULL, `tar_type` TEXT, `has_key_store` INTEGER NOT NULL, `installer_app` TEXT, `info_hash` TEXT, PRIMARY KEY(`backup_name`, `package_name`))", "fields": [ { "fieldPath": "packageName", "columnName": "package_name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "backupName", "columnName": "backup_name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "label", "columnName": "label", "affinity": "TEXT", "notNull": false }, { "fieldPath": "versionName", "columnName": "version_name", "affinity": "TEXT", "notNull": false }, { "fieldPath": "versionCode", "columnName": "version_code", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "isSystem", "columnName": "is_system", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "hasSplits", "columnName": "has_splits", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "hasRules", "columnName": "has_rules", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "backupTime", "columnName": "backup_time", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "crypto", "columnName": "crypto", "affinity": "TEXT", "notNull": false }, { "fieldPath": "version", "columnName": "meta_version", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "flags", "columnName": "flags", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "userId", "columnName": "user_id", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "tarType", "columnName": "tar_type", "affinity": "TEXT", "notNull": false }, { "fieldPath": "hasKeyStore", "columnName": "has_key_store", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "installer", "columnName": "installer_app", "affinity": "TEXT", "notNull": false }, { "fieldPath": "uuid", "columnName": "info_hash", "affinity": "TEXT", "notNull": false } ], "primaryKey": { "autoGenerate": false, "columnNames": [ "backup_name", "package_name" ] }, "indices": [], "foreignKeys": [] }, { "tableName": "op_history", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` TEXT NOT NULL, `time` INTEGER NOT NULL, `data` TEXT NOT NULL, `status` TEXT NOT NULL, `extra` TEXT)", "fields": [ { "fieldPath": "id", "columnName": "id", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "type", "columnName": "type", "affinity": "TEXT", "notNull": true }, { "fieldPath": "execTime", "columnName": "time", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "serializedData", "columnName": "data", "affinity": "TEXT", "notNull": true }, { "fieldPath": "status", "columnName": "status", "affinity": "TEXT", "notNull": true }, { "fieldPath": "serializedExtra", "columnName": "extra", "affinity": "TEXT", "notNull": false } ], "primaryKey": { "autoGenerate": true, "columnNames": [ "id" ] }, "indices": [], "foreignKeys": [] }, { "tableName": "fm_favorite", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `uri` TEXT NOT NULL, `init_uri` TEXT, `options` INTEGER NOT NULL, `order` INTEGER NOT NULL, `type` INTEGER NOT NULL)", "fields": [ { "fieldPath": "id", "columnName": "id", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "name", "columnName": "name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "uri", "columnName": "uri", "affinity": "TEXT", "notNull": true }, { "fieldPath": "initUri", "columnName": "init_uri", "affinity": "TEXT", "notNull": false }, { "fieldPath": "options", "columnName": "options", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "order", "columnName": "order", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "type", "columnName": "type", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "autoGenerate": true, "columnNames": [ "id" ] }, "indices": [], "foreignKeys": [] }, { "tableName": "freeze_type", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `type` INTEGER NOT NULL, PRIMARY KEY(`package_name`))", "fields": [ { "fieldPath": "packageName", "columnName": "package_name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "type", "columnName": "type", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "autoGenerate": false, "columnNames": [ "package_name" ] }, "indices": [], "foreignKeys": [] } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f72fb62329689ce1c1c5cda5c0ede949')" ] } } ================================================ FILE: app/schemas/io.github.muntashirakon.AppManager.db.AppsDb/7.json ================================================ { "formatVersion": 1, "database": { "version": 7, "identityHash": "3b4a41c98cdc8966b52bd7a0a0f8ad58", "entities": [ { "tableName": "app", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `user_id` INTEGER NOT NULL DEFAULT -10000, `label` TEXT, `version_name` TEXT, `version_code` INTEGER NOT NULL, `flags` INTEGER NOT NULL DEFAULT 0, `uid` INTEGER NOT NULL DEFAULT 0, `shared_uid` TEXT DEFAULT NULL, `first_install_time` INTEGER NOT NULL DEFAULT 0, `last_update_time` INTEGER NOT NULL DEFAULT 0, `target_sdk` INTEGER NOT NULL DEFAULT 0, `cert_name` TEXT DEFAULT '', `cert_algo` TEXT DEFAULT '', `is_installed` INTEGER NOT NULL DEFAULT true, `is_only_data_installed` INTEGER NOT NULL DEFAULT 0, `is_enabled` INTEGER NOT NULL DEFAULT false, `has_activities` INTEGER NOT NULL DEFAULT false, `has_splits` INTEGER NOT NULL DEFAULT false, `has_keystore` INTEGER NOT NULL DEFAULT false, `uses_saf` INTEGER NOT NULL DEFAULT false, `ssaid` TEXT DEFAULT '', `code_size` INTEGER NOT NULL DEFAULT 0, `data_size` INTEGER NOT NULL DEFAULT 0, `mobile_data` INTEGER NOT NULL DEFAULT 0, `wifi_data` INTEGER NOT NULL DEFAULT 0, `rules_count` INTEGER NOT NULL DEFAULT 0, `tracker_count` INTEGER NOT NULL DEFAULT 0, `open_count` INTEGER NOT NULL DEFAULT 0, `screen_time` INTEGER NOT NULL DEFAULT 0, `last_usage_time` INTEGER NOT NULL DEFAULT 0, `last_action_time` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`package_name`, `user_id`))", "fields": [ { "fieldPath": "packageName", "columnName": "package_name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "userId", "columnName": "user_id", "affinity": "INTEGER", "notNull": true, "defaultValue": "-10000" }, { "fieldPath": "packageLabel", "columnName": "label", "affinity": "TEXT" }, { "fieldPath": "versionName", "columnName": "version_name", "affinity": "TEXT" }, { "fieldPath": "versionCode", "columnName": "version_code", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "flags", "columnName": "flags", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "uid", "columnName": "uid", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "sharedUserId", "columnName": "shared_uid", "affinity": "TEXT", "defaultValue": "NULL" }, { "fieldPath": "firstInstallTime", "columnName": "first_install_time", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "lastUpdateTime", "columnName": "last_update_time", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "sdk", "columnName": "target_sdk", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "certName", "columnName": "cert_name", "affinity": "TEXT", "defaultValue": "''" }, { "fieldPath": "certAlgo", "columnName": "cert_algo", "affinity": "TEXT", "defaultValue": "''" }, { "fieldPath": "isInstalled", "columnName": "is_installed", "affinity": "INTEGER", "notNull": true, "defaultValue": "true" }, { "fieldPath": "isOnlyDataInstalled", "columnName": "is_only_data_installed", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "isEnabled", "columnName": "is_enabled", "affinity": "INTEGER", "notNull": true, "defaultValue": "false" }, { "fieldPath": "hasActivities", "columnName": "has_activities", "affinity": "INTEGER", "notNull": true, "defaultValue": "false" }, { "fieldPath": "hasSplits", "columnName": "has_splits", "affinity": "INTEGER", "notNull": true, "defaultValue": "false" }, { "fieldPath": "hasKeystore", "columnName": "has_keystore", "affinity": "INTEGER", "notNull": true, "defaultValue": "false" }, { "fieldPath": "usesSaf", "columnName": "uses_saf", "affinity": "INTEGER", "notNull": true, "defaultValue": "false" }, { "fieldPath": "ssaid", "columnName": "ssaid", "affinity": "TEXT", "defaultValue": "''" }, { "fieldPath": "codeSize", "columnName": "code_size", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "dataSize", "columnName": "data_size", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "mobileDataUsage", "columnName": "mobile_data", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "wifiDataUsage", "columnName": "wifi_data", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "rulesCount", "columnName": "rules_count", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "trackerCount", "columnName": "tracker_count", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "openCount", "columnName": "open_count", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "screenTime", "columnName": "screen_time", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "lastUsageTime", "columnName": "last_usage_time", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" }, { "fieldPath": "lastActionTime", "columnName": "last_action_time", "affinity": "INTEGER", "notNull": true, "defaultValue": "0" } ], "primaryKey": { "autoGenerate": false, "columnNames": [ "package_name", "user_id" ] } }, { "tableName": "log_filter", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT)", "fields": [ { "fieldPath": "id", "columnName": "id", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "name", "columnName": "name", "affinity": "TEXT" } ], "primaryKey": { "autoGenerate": true, "columnNames": [ "id" ] }, "indices": [ { "name": "index_name", "unique": true, "columnNames": [ "name" ], "orders": [], "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_name` ON `${TABLE_NAME}` (`name`)" } ] }, { "tableName": "backup", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `backup_name` TEXT NOT NULL, `label` TEXT, `version_name` TEXT, `version_code` INTEGER NOT NULL, `is_system` INTEGER NOT NULL, `has_splits` INTEGER NOT NULL, `has_rules` INTEGER NOT NULL, `backup_time` INTEGER NOT NULL, `crypto` TEXT, `meta_version` INTEGER NOT NULL, `flags` INTEGER NOT NULL, `user_id` INTEGER NOT NULL, `tar_type` TEXT, `has_key_store` INTEGER NOT NULL, `installer_app` TEXT, `info_hash` TEXT, PRIMARY KEY(`backup_name`, `package_name`))", "fields": [ { "fieldPath": "packageName", "columnName": "package_name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "backupName", "columnName": "backup_name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "label", "columnName": "label", "affinity": "TEXT" }, { "fieldPath": "versionName", "columnName": "version_name", "affinity": "TEXT" }, { "fieldPath": "versionCode", "columnName": "version_code", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "isSystem", "columnName": "is_system", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "hasSplits", "columnName": "has_splits", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "hasRules", "columnName": "has_rules", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "backupTime", "columnName": "backup_time", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "crypto", "columnName": "crypto", "affinity": "TEXT" }, { "fieldPath": "version", "columnName": "meta_version", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "flags", "columnName": "flags", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "userId", "columnName": "user_id", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "tarType", "columnName": "tar_type", "affinity": "TEXT" }, { "fieldPath": "hasKeyStore", "columnName": "has_key_store", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "installer", "columnName": "installer_app", "affinity": "TEXT" }, { "fieldPath": "uuid", "columnName": "info_hash", "affinity": "TEXT" } ], "primaryKey": { "autoGenerate": false, "columnNames": [ "backup_name", "package_name" ] } }, { "tableName": "op_history", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` TEXT NOT NULL, `time` INTEGER NOT NULL, `data` TEXT NOT NULL, `status` TEXT NOT NULL, `extra` TEXT)", "fields": [ { "fieldPath": "id", "columnName": "id", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "type", "columnName": "type", "affinity": "TEXT", "notNull": true }, { "fieldPath": "execTime", "columnName": "time", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "serializedData", "columnName": "data", "affinity": "TEXT", "notNull": true }, { "fieldPath": "status", "columnName": "status", "affinity": "TEXT", "notNull": true }, { "fieldPath": "serializedExtra", "columnName": "extra", "affinity": "TEXT" } ], "primaryKey": { "autoGenerate": true, "columnNames": [ "id" ] } }, { "tableName": "fm_favorite", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `uri` TEXT NOT NULL, `init_uri` TEXT, `options` INTEGER NOT NULL, `order` INTEGER NOT NULL, `type` INTEGER NOT NULL)", "fields": [ { "fieldPath": "id", "columnName": "id", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "name", "columnName": "name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "uri", "columnName": "uri", "affinity": "TEXT", "notNull": true }, { "fieldPath": "initUri", "columnName": "init_uri", "affinity": "TEXT" }, { "fieldPath": "options", "columnName": "options", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "order", "columnName": "order", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "type", "columnName": "type", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "autoGenerate": true, "columnNames": [ "id" ] } }, { "tableName": "freeze_type", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `type` INTEGER NOT NULL, PRIMARY KEY(`package_name`))", "fields": [ { "fieldPath": "packageName", "columnName": "package_name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "type", "columnName": "type", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "autoGenerate": false, "columnNames": [ "package_name" ] } } ], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3b4a41c98cdc8966b52bd7a0a0f8ad58')" ] } } ================================================ FILE: app/src/debug/AndroidManifest.xml ================================================ ================================================ FILE: app/src/debug/java/io/github/muntashirakon/AppManager/debug/R.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.debug; public final class R { public static final class raw { public static final int appops = io.github.muntashirakon.AppManager.docs.R.raw.appops; public static final int custom = io.github.muntashirakon.AppManager.docs.R.raw.custom; public static final int icon = io.github.muntashirakon.AppManager.docs.R.raw.icon; public static final int index = io.github.muntashirakon.AppManager.docs.R.raw.index; public static final int main_page_entry_info_labeled = io.github.muntashirakon.AppManager.docs.R.raw.main_page_entry_info_labeled; } } ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/aidl/io/github/muntashirakon/AppManager/IAMService.aidl ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager; import android.os.IBinder; import aosp.android.content.pm.ParceledListSlice; import io.github.muntashirakon.AppManager.IRemoteProcess; import io.github.muntashirakon.AppManager.IRemoteShell; // Transact code starts from 3 interface IAMService { IRemoteProcess newProcess(in String[] cmd, in String[] env, in String dir) = 3; IRemoteShell getShell(in String[] cmd) = 4; ParceledListSlice getRunningProcesses() = 6; int getUid() = 12; void symlink(in String file, in String link) = 13; IBinder getService(in String serviceName) = 14; } ================================================ FILE: app/src/main/aidl/io/github/muntashirakon/AppManager/IRemoteProcess.aidl ================================================ // SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-or-later package io.github.muntashirakon.AppManager; // Copyright 2020 Rikka interface IRemoteProcess { ParcelFileDescriptor getOutputStream(); void closeOutputStream(); ParcelFileDescriptor getInputStream(); ParcelFileDescriptor getErrorStream(); int waitFor(); int exitValue(); void destroy(); boolean alive(); boolean waitForTimeout(long timeout, String unit); } ================================================ FILE: app/src/main/aidl/io/github/muntashirakon/AppManager/IRemoteShell.aidl ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager; import io.github.muntashirakon.AppManager.IShellResult; interface IRemoteShell { void addCommand(in String[] commands); void addInputStream(in ParcelFileDescriptor inputStream); IShellResult exec(); } ================================================ FILE: app/src/main/aidl/io/github/muntashirakon/AppManager/IShellResult.aidl ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager; import aosp.android.content.pm.StringParceledListSlice; interface IShellResult { StringParceledListSlice getStdout(); StringParceledListSlice getStderr(); int getExitCode(); boolean isSuccessful(); } ================================================ FILE: app/src/main/aidl/io/github/muntashirakon/AppManager/ipc/ps/ProcessEntry.aidl ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.ipc.ps; parcelable ProcessEntry; ================================================ FILE: app/src/main/aidl/io/github/muntashirakon/AppManager/ipc/ps/ProcessUsers.aidl ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.ipc.ps; parcelable ProcessUsers; ================================================ FILE: app/src/main/annotations/android/content/pm/annotations.xml ================================================ ================================================ FILE: app/src/main/assets/blanks/blank.txt ================================================ ================================================ FILE: app/src/main/assets/debloat.json ================================================ [{"id":"android","description":"Android System\nAndroid system framework? Apk file name: framework-res\nCould be THE core of the android system.\nProbably very unsafe to disable.","removal":"unsafe","type":"aosp"},{"id":"android.aosp.overlay","description":"Refers to the Runtime Resource Overlay (RRO) framework that is built into the AOSP.\nRRO allows for the dynamic modification of an app's resources at runtime,\nEnabling the customization of the app's appearance and behavior without modifying its source code\nhttps://source.android.com/docs/core/runtime/rros","removal":"unsafe","type":"aosp"},{"id":"android.auto_generated_rro__","label":"android.auto_generated_rro__","description":"RRO = Runtime Resources Overlay. Changes values of a package config, based in the overlay definitions. Overlays are heavily used by OEMs to customize the look and feel of Android.","web":["https://source.android.com/devices/architecture/rros","https://code.tutsplus.com/tutorials/quick-tip-theme-android-with-the-runtime-resource-overlay-framework--cms-29708"],"removal":"caution","type":"aosp"},{"id":"android.auto_generated_rro_product__","label":"android.auto_generated_rro_product__","description":"RRO = Runtime Resources Overlay. Used by OEMs to customize look and feel of certain applications.","web":["https://source.android.com/devices/architecture/rros","https://code.tutsplus.com/tutorials/quick-tip-theme-android-with-the-runtime-resource-overlay-framework--cms-29708"],"removal":"caution","type":"aosp"},{"id":"android.auto_generated_rro_vendor__","label":"android.auto_generated_rro_vendor__","description":"RRO = Runtime Resources Overlay. Used by OEMs to customize look and feel of certain applications.","web":["https://source.android.com/devices/architecture/rros","https://code.tutsplus.com/tutorials/quick-tip-theme-android-with-the-runtime-resource-overlay-framework--cms-29708"],"removal":"caution","type":"aosp"},{"id":"android.auto_generated_vendor_","label":"android.auto_generated_vendor_","description":"Auto generated vendor's stuff for Android Auto.","web":["https://www.android.com/intl/en_en/auto/"],"removal":"delete","warning":"You may need this if you use Android Auto","type":"aosp"},{"id":"android.overlay.common","description":"It has some important settings and configurations related to Android.","removal":"unsafe","type":"aosp"},{"id":"android.overlay.target","description":"It has some important settings and configurations related to Android.","removal":"unsafe","type":"aosp"},{"id":"android.qvaoverlay.common","description":"This app has no code and is safe to remove.","removal":"delete","type":"aosp"},{"id":"com.android.adservices.api","description":"Android AdServices. Introduced in Android 13 privacy sandbox beta components disabled on default.\nhttps://source.android.com/docs/core/ota/modular-system/adservices","removal":"delete","type":"aosp"},{"id":"com.android.apps.tag","label":"Tags","description":"Support for NFC tags interactions (5 permissions, Contacts/Phone On by default).\nNFC Tags are for instance used in buses to validate your transport card with your phone.\nOther example: https://en.wikipedia.org/wiki/TecTile\nYou will still be able to connect to a NFC device (e.g a speaker) with this disabled.","removal":"replace","type":"aosp"},{"id":"com.android.avatarpicker","description":"Lets you assign pictures to contacts. It has two options: take picture from the camera, or choose from the gallery.\nSource code: https://android.googlesource.com/platform/packages/apps/AvatarPicker","removal":"replace","type":"aosp"},{"id":"com.android.backupconfirm","label":"com.android.backupconfirm","description":"Restores Google settings with Google Backup restore.\nDisplays confirmation popup when doing ADB backup.","removal":"caution","warning":"Disabling this package breaks ADB Backup and crashes on attempting to add a Google account","type":"aosp"},{"id":"com.android.basicsmsreceiver","description":"Gets SMS and creates notifications:\nhttps://android.googlesource.com/platform/packages/apps/BasicSmsReceiver/+/jb-dev/src/com/android/basicsmsreceiver/BasicSmsReceiverApp.java","removal":"caution","type":"aosp"},{"id":"com.android.bio.face.service","label":"com.android.bio.face.service","description":"Handles facial recognition.","removal":"caution","type":"aosp"},{"id":"com.android.bips","label":"Default Print Service","description":"Generic printing service that should work with most printers.\nWill break printing functionality if disabled, but other replacement print services can be downloaded from the Play Store.","removal":"replace","type":"aosp"},{"id":"com.android.bluetooth","label":"Bluetooth","description":"Handles all Bluetooth functionality including device discovery, pairing, and data transfer.","removal":"caution","warning":"Removing this will completely break Bluetooth functionality on the device.","type":"aosp"},{"id":"com.android.bluetooth.overlay.common","description":"Overlays are usually themes.","removal":"caution","type":"aosp"},{"id":"com.android.bluetoothmidiservice","label":"Bluetooth MIDI Service","description":"Provides classes for using the MIDI protocol over Bluetooth.","removal":"delete","warning":"Do not remove if you connect to a MIDI device via Bluetooth","type":"aosp"},{"id":"com.android.bookmarkprovider","label":"Bookmark Provider","description":"Only exists for compatibility reasons to prevent apps querying it from getting null cursors they do not expect and crash.","removal":"caution","warning":"Apps targeting a very old SDK might crash. For example, disabling this on LDPlayer emulator crashes the default browser.","type":"aosp"},{"id":"com.android.browser","label":"Mi Browser","description":"Mi Browser and browser for the LDPlayer emulator. It is a privacy nightmare and should be replaced.","web":["https://www.xda-developers.com/xiaomi-mi-web-browser-pro-mint-collecting-browsing-data-incognito-mode/"],"removal":"replace","suggestions":"browsers","type":"aosp"},{"id":"com.android.browser.provider","label":"com.android.browser.provider","description":"Old package (2014). Chrome bookmarks provider? Injects Picasa URL (https://picasaweb.google.com) in the Chrome browser's bookmarks in the browser.","removal":"delete","type":"aosp"},{"id":"com.android.calculator2","label":"Calculator","description":"The AOSP calculator app\nSome OEMs (e.g. Huawei and Xiaomi) use the same package name for their app","removal":"replace","suggestions":"calculators","type":"aosp"},{"id":"com.android.calendar","label":"Calendar","description":"The AOSP Calendar app.\nSome OEMs (e.g. Huawei and Xiaomi) use the same package name for their app.","removal":"replace","suggestions":"calendars","type":"aosp"},{"id":"com.android.calllogbackup","label":"Call Log Backup/Restore","description":"Call Logs Backup/Restore feature, runs in the background.","web":["https://android.googlesource.com/platform/packages/providers/CallLogProvider/+/refs/heads/master/src/com/android/calllogbackup"],"removal":"delete","type":"aosp"},{"id":"com.android.camera","description":"The stock AOSP camera app on many phones. However, on some Xiaomi phones, it is actually the Xiaomi Camera app. Deleting this will result in no camera app.\nTry Open Camera as an open source alternative:\nhttps://play.google.com/store/apps/details?id=net.sourceforge.opencamera&hl=en&gl=US","removal":"replace","type":"aosp"},{"id":"com.android.captiveportallogin","label":"CaptivePortalLogin","description":"Support for captive portal logins.\nA captive portal login is a web page where users have to log in or accept terms of use. Common for public wifi networks.","web":["https://en.wikipedia.org/wiki/Captive_portal"],"removal":"delete","type":"aosp"},{"id":"com.android.carrierconfig","label":"com.android.carrierconfig","description":"Dynamically provides configuration for the carrier network.\nThe config contains: Roaming networks, Voicemail settings, SMS/MMS settings, VoLTE/IMS settings, and more.\nIf a carrier app is installed it will be queried for overrides to these settings.\nSeems to run on boot and when you swap SIM?","web":["https://source.android.com/devices/tech/config/carrier","https://cs.android.com/android/platform/superproject/+/master:packages/apps/CarrierConfig/src/com/android/carrierconfig/DefaultCarrierConfigService.java"],"removal":"replace","type":"aosp"},{"id":"com.android.carrierconfig.overlay.common","description":"Needed for (com.android.carrierconfig).","removal":"replace","type":"aosp"},{"id":"com.android.carrierdefaultapp","label":"CarrierDefaultApp","description":"This package is a generic solution that allows carriers to indicate when a device has run OOB (Out Of Balance). Android devices that are OOB need carrier mitigation protocols to allow select data through (like to notify users their data/balance is out, or allow them to buy more data through the carrier app).\nWill probably break that functionality if disabled, but is otherwise safe to disable (should only affect users that are out of data/balance?).","web":["https://source.android.com/devices/tech/connect/oob-users"],"removal":"caution","type":"aosp"},{"id":"com.android.cellbroadcastreceiver","label":"Emergency alerts","description":"Cell broadcast is designed to deliver messages to multiple users in an area.\nThis is notably used by ISPs to send Emergency/Government alerts.\nRuns at boot time and is also triggered after exiting airplane mode.","web":["https://en.wikipedia.org/wiki/Cell_Broadcast","https://www.androidcentral.com/amber-alerts-and-android-what-you-need-know","https://android.googlesource.com/platform/packages/apps/CellBroadcastReceiver/+/refs/heads/master/src/com/android/cellbroadcastreceiver"],"removal":"caution","type":"aosp"},{"id":"com.android.cellbroadcastreceiver.basiccolorblack.overlay","label":"com.android.cellbroadcastreceiver.basiccolorblack.overlay","description":"Dark theme overlay for com.android.cellbroadcastreceiver","dependencies":["com.android.cellbroadcastreceiver"],"removal":"caution","type":"aosp"},{"id":"com.android.cellbroadcastreceiver.basiccolorwhite.overlay","label":"com.android.cellbroadcastreceiver.basiccolorwhite.overlay","description":"Light theme overlay for com.android.cellbroadcastreceiver","dependencies":["com.android.cellbroadcastreceiver"],"removal":"caution","type":"aosp"},{"id":"com.android.cellbroadcastreceiver.module","description":"Same as com.android.cellbroadcastreceiver.\nCell broadcasting used to send emergency alerts.\nhttps://en.wikipedia.org/wiki/Cell_Broadcast.","dependencies":["com.android.cellbroadcastreceiver"],"removal":"caution","type":"aosp"},{"id":"com.android.cellbroadcastreceiver.overlay.common","label":"com.android.cellbroadcastreceiver.overlay.common","description":"Overlay for com.android.cellbroadcastreceiver. Inside the APK, I found unused things: show_brazil_settings, show_cmas_settings, show_etws_settings.\nThey are unavailable and useless.","removal":"delete","type":"aosp"},{"id":"com.android.cellbroadcastservice","description":"is designed to deliver messages to multiple users in an area.\nThis is notably used by ISPs to send Emergency/Government alerts.\nRuns in the background.\nhttps://en.wikipedia.org/wiki/Cell_Broadcast\nhttps://www.androidcentral.com/amber-alerts-and-android-what-you-need-know","removal":"caution","type":"aosp"},{"id":"com.android.certinstaller","description":"Certificate installer\nUsed for accepting and revoking Internet certificates.\nCertificates identify ownership of public keys, for use in secure communications.\nBreaks Wi-Fi if disabled.","removal":"unsafe","type":"aosp"},{"id":"com.android.companiondevicemanager","label":"Companion Device Manager","description":"This handles connections to nearby (usually not remote) devices, like Bluetooth Headphones, desktop Operating Systems, etc.","removal":"caution","warning":"Removing this package may result in the inability to read the SD card from your computer's file manager (via USB).","type":"aosp"},{"id":"com.android.connectivity.resources","description":"Network connectivity resources.\nCause BOOTLOOP.","removal":"unsafe","type":"aosp"},{"id":"com.android.contacts","label":"Contacts","description":"The AOSP Contacts app.\nSome OEMs (e.g. Xiaomi) use the same package name for their app.","removal":"replace","suggestions":"contacts","type":"aosp"},{"id":"com.android.credentialmanager","description":"Credential Manager\nManages with Passwords, passkeys.","removal":"replace","type":"aosp"},{"id":"com.android.cts.ctsshim","label":"Compatibility Test Suite","description":"Used by manufacturer to test your copy of the device for performance. It just exists and doesn't run in background.","web":["https://source.android.com/docs/compatibility/cts"],"removal":"delete","type":"aosp"},{"id":"com.android.cts.priv.ctsshim","label":"Compatibility Test Suite","description":"Verifies certain upgrade scenarios.\nA shim is basically a compatibility layer for an API, that makes sure anything that uses the API does so correctly.","web":["https://android.googlesource.com/platform/frameworks/base/+/51e458e/packages/CtsShim","https://en.wikipedia.org/wiki/Shim_(computing)"],"removal":"caution","warning":"Disabling could mess with OTA updates.","type":"aosp"},{"id":"com.android.defcontainer","description":"Package Access Helper\nDetermines the recommended install location for packages and if there is enough free space for the package.","removal":"unsafe","type":"aosp"},{"id":"com.android.deskclock","label":"Clock","description":"The AOSP Clock app\nSome OEMs (e.g. Huawei and Xiaomi) use the same package name for their app.","removal":"replace","suggestions":"clocks","type":"aosp"},{"id":"com.android.devicelockcontroller","description":"This app can't be uninstalled or disabled.\nCan restrict this device if the owner doesn't make payments per month for the new phone.","removal":"unsafe","type":"aosp"},{"id":"com.android.dialer","label":"Phone","description":"The AOSP Dialer/Phone app\nDefault phone app on some older phones (like Oneplus 3).","removal":"replace","suggestions":"dialers","type":"aosp"},{"id":"com.android.dialer.basiccolorblack.overlay","description":"Dark theme overlay for AOSP Dialer?","removal":"caution","type":"aosp"},{"id":"com.android.dialer.basiccolorwhite.overlay","description":"Light theme overlay for AOSP Dialer?","removal":"caution","type":"aosp"},{"id":"com.android.documentsui","label":"Files","description":"Occasionally runs in the background.\nFile selector for other apps.","removal":"unsafe","warning":"Storage Access Framework (SAF) will break if this is disabled.","type":"aosp"},{"id":"com.android.documentsui.a_overlay","label":"com.android.documentsui.a_overlay","description":"Some overlay for for \"Files\"?","dependencies":["com.android.documentsui"],"removal":"caution","type":"aosp"},{"id":"com.android.dreams.basic","label":"Basic Daydreams","description":"Daydream (not Google Daydream VR) is an interactive screensaver mode built into Android.\nWith it turned on, it activates and shows the screensaver of your choice when you dock or charge your device.\nCan display the time, weather, quotes, photos, news, tweets, or anything else Daydream app developers can think of.","web":["https://developer.android.com/reference/android/service/dreams/DreamService"],"removal":"delete","type":"aosp"},{"id":"com.android.dreams.phototable","label":"Photo Screensavers","description":"Daydream stuff, see com.android.dreams.basic","removal":"delete","type":"aosp"},{"id":"com.android.dreams.phototable.overlay","label":"com.android.dreams.phototable.overlay","description":"Overlay for the phototable daydream? Overlays are usually themes, but not sure about this one.","removal":"delete","type":"aosp"},{"id":"com.android.dynsystem","description":"Dynamic System Updates\nRuns on boot, but doesn't seem to run in the background beyond that.\nTreble gives the ability to boot an AOSP Generic System Image (GSI) on any supported device.\nDynamic System Updates allows to boot into a Generic System Image (GSI) without interfering with the current installation.\nThat means the bootloader doesn’t need to be unlocked and the user data doesn’t need to be wiped.\nhttps://developer.android.com/topic/dsu","removal":"caution","type":"aosp"},{"id":"com.android.egg","label":"Android Easter Egg","description":"Android's easter egg feature (spam-tap on the android version in the settings)","removal":"delete","type":"aosp"},{"id":"com.android.email","label":"Email","description":"The AOSP Email app.\nSome OEMs (e.g. Huawei, Xiaomi, Oppo) use the same package name for their app.","removal":"replace","suggestions":"email_clients","type":"aosp"},{"id":"com.android.email.partnerprovider","label":"EmailPartnerProvider","description":"Lets Google partners (OEM in most of the case) customize the default email settings.\nThe manufacturer often changes the default signature displayed at the end of each of your mail (e.g \"Sent from my Nokia phone\")","removal":"delete","type":"aosp"},{"id":"com.android.emergency","label":"Emergency information","description":"Shows emergency info on lockscreen and power menu. Loads on device unlock/lockscreen and power menu, so it's basically always cached in RAM, but shouldn't use much/any battery. So, the main thing gained from disabling this package is the ~9MB RAM it uses.","removal":"caution","warning":"Removing this will break Safety and Emergency in Settings, and you will miss SOS alerts.","type":"aosp"},{"id":"com.android.emergency.basiccolorblack.overlay","description":"Dark theme for Emergency rescue?","removal":"caution","type":"aosp"},{"id":"com.android.emergency.basiccolorwhite.overlay","description":"Dark theme for Emergency rescue?","removal":"caution","type":"aosp"},{"id":"com.android.exchange","label":"Exchange Services","description":"Handles all aspects of starting, maintaining, and stopping the various sync adapters for the email accounts.\nIs it only needed for the email stock app?\n","removal":"replace","suggestions":"email_clients","type":"aosp"},{"id":"com.android.ext.adservices.api","description":"Another component of Android AdServices.\nIntroduced in Android 14.\nhttps://source.android.com/docs/core/ota/modular-system/adservices","removal":"delete","type":"aosp"},{"id":"com.android.externalstorage","label":"External Storage","description":"Needed by apps to access external storage such as memory cards.","removal":"unsafe","warning":"Storage Access Framework (SAF) will break if this is disabled.","type":"aosp"},{"id":"com.android.facelock","label":"Trusted Face","description":"Package for supporting the Face Unlock feature","removal":"caution","warning":"Do not remove if you use Face Unlock","type":"aosp"},{"id":"com.android.federatedcompute.services","description":"FederatedCompute\nAnother component of OnDevicePersonalization. But this app learns things about users.\nIntroduced in Android 14(`com.google.android.federatedcompute` Introduced in Android 13).\nhttps://source.android.com/docs/core/ota/modular-system/ondevicepersonalization","removal":"delete","type":"aosp"},{"id":"com.android.fmradio","label":"FM Radio","description":"Plug in head phones and listen to the FM radio!","removal":"replace","suggestions":"radios","type":"aosp"},{"id":"com.android.frameworkhwext.dark","description":"Required components of the androidhwext.\nBasic functionality of Huawei Phones.","removal":"unsafe","type":"aosp"},{"id":"com.android.frameworkhwext.honor","description":"Required components of the androidhwext.\nBasic functionality of Huawei Phones.","removal":"unsafe","type":"aosp"},{"id":"com.android.frameworkres.overlay","description":"Runtime Resource Overlay\nThis framework provides the ability to replace application resources while the application is running. More info:\nhttps://source.android.com/docs/core/runtime/rros","removal":"caution","type":"aosp"},{"id":"com.android.galaxy4","label":"Black Hole","description":"Built-in Dynamic wallpaper","removal":"delete","type":"aosp"},{"id":"com.android.gallery3d","label":"Gallery","description":"The AOSP Gallery app, often vendors (e.g. Xiaomi) modify it to provide their own apps.","removal":"replace","suggestions":"gallery","type":"aosp"},{"id":"com.android.health.connect.backuprestore","description":"Backups data from Health Connect app.","removal":"delete","type":"aosp"},{"id":"com.android.healthconnect.controller","description":"Health Connect\nManage the health and fitness data on your phone, and control which apps can access it.","removal":"delete","type":"aosp"},{"id":"com.android.hotspot2","label":"OsuLogin","description":"Provides wifi tethering i.e. lets you share your mobile device's Internet connection with other devices.","web":["https://en.wikipedia.org/wiki/Tethering"],"removal":"caution","type":"aosp"},{"id":"com.android.hotspot2.osulogin","label":"OsuLogin","description":"The sole purpose of the app is to provision credentials from the Wi-Fi network to the device and allow them to connect to Wi-Fi Hotspot 2.0.","web":["https://hackanons.com/2021/07/osulogin-android-everything-you-need-to-know.html"],"removal":"caution","type":"aosp"},{"id":"com.android.htmlviewer","label":"HTML Viewer","description":"Allows apps to load URLs into the WebView, which allows web content to be displayed directly in the app.","removal":"caution","warning":"Removing this causes a bootloop on some MIUI 12.5.4+ phones.","type":"aosp"},{"id":"com.android.inputdevices","label":"Input Devices","description":"Only contains a receiver named \"Android keyboard\", possibly for an external keyboard.\nLocates available keyboard layouts. Apps can offer additional keyboard layouts to the user by declaring a suitable broadcast receiver in their manifest.","removal":"caution","warning":"If you are using the default Samsung keyboard, then deleting this package on some phones may cause the keyboard to completely stop working. You may get locked out of your phone if the only method to authenticate yourself is using password.","type":"aosp"},{"id":"com.android.inputmethod.latin","label":"Android Keyboard (AOSP)","description":"The AOSP keyboard app","removal":"replace","warning":"Do NOT disable if you don't have another keyboard with direct boot mode support, or you'll be stuck at boot (no keyboard to unlock the phone).","suggestions":"keyboards","type":"aosp"},{"id":"com.android.intentresolver","description":"'Share' functionality will be disabled after uninstalling this package on Android 14 and up. Additionally, motion photos will become broken.","removal":"caution","type":"aosp"},{"id":"com.android.internal.display.cutout.emulation.corner","label":"Corner cutout","description":"Display cutout variant.","web":["https://developer.android.com/guide/topics/display-cutout","https://source.android.com/devices/tech/display/display-cutouts"],"removal":"unsafe","warning":"Removing the app will cause bootloop.","type":"aosp"},{"id":"com.android.internal.display.cutout.emulation.double","label":"Double cutout","description":"Display cutout variant.","web":["https://developer.android.com/guide/topics/display-cutout","https://source.android.com/devices/tech/display/display-cutouts"],"removal":"unsafe","warning":"Removing the app will cause bootloop.","type":"aosp"},{"id":"com.android.internal.display.cutout.emulation.hole","label":"Punch Hole cutout","description":"Display cutout variant.","web":["https://developer.android.com/guide/topics/display-cutout","https://source.android.com/devices/tech/display/display-cutouts"],"removal":"unsafe","warning":"Removing the app will cause bootloop.","type":"aosp"},{"id":"com.android.internal.display.cutout.emulation.narrow","description":"Display cutout variant.","web":["https://developer.android.com/guide/topics/display-cutout","https://source.android.com/devices/tech/display/display-cutouts"],"removal":"unsafe","warning":"Removing the app will cause bootloop.","type":"aosp"},{"id":"com.android.internal.display.cutout.emulation.noCutout","label":"Hide","description":"Display cutout variant.","web":["https://developer.android.com/guide/topics/display-cutout","https://source.android.com/devices/tech/display/display-cutouts"],"removal":"unsafe","warning":"Removing the app will cause bootloop.","type":"aosp"},{"id":"com.android.internal.display.cutout.emulation.tall","label":"Tall cutout","description":"Display cutout variant.","web":["https://developer.android.com/guide/topics/display-cutout","https://source.android.com/devices/tech/display/display-cutouts"],"removal":"unsafe","warning":"Removing the app will cause bootloop.","type":"aosp"},{"id":"com.android.internal.display.cutout.emulation.waterfall","label":"Waterfall cutout","description":"Display cutout variant.","web":["https://developer.android.com/guide/topics/display-cutout","https://source.android.com/devices/tech/display/display-cutouts"],"removal":"unsafe","warning":"Removing the app will cause bootloop.","type":"aosp"},{"id":"com.android.internal.display.cutout.emulation.wide","description":"Display cutout variant.","web":["https://developer.android.com/guide/topics/display-cutout","https://source.android.com/devices/tech/display/display-cutouts"],"removal":"unsafe","warning":"Removing the app will cause bootloop.","type":"aosp"},{"id":"com.android.internal.systemui.navbar.gestural","label":"Gestural Navigation Bar","description":"Gesture navigation\nLets you use swipes and other actions to navigate your device, rather than buttons.","web":["https://android-developers.googleblog.com/2019/08/gesture-navigation-backstory.html"],"removal":"caution","type":"aosp"},{"id":"com.android.internal.systemui.navbar.gestural_extra_wide_back","label":"Gestural Navigation Bar","description":"Enables a setting increasing how far you need to move your finger to trigger the back gesture.","removal":"caution","type":"aosp"},{"id":"com.android.internal.systemui.navbar.gestural_narrow_back","label":"Gestural Navigation Bar","description":"Enables a setting decreasing how far you need to move your finger to trigger the back gesture.","removal":"caution","type":"aosp"},{"id":"com.android.internal.systemui.navbar.gestural_wide_back","label":"Gestural Navigation Bar","description":"Enables a setting increasing how far you need to move your finger to trigger the back gesture.","removal":"caution","type":"aosp"},{"id":"com.android.internal.systemui.navbar.hidegestural","description":"Allows 'Gesture hint' to be disabled in Navigation bar > Swipe gestures.","removal":"caution","type":"aosp"},{"id":"com.android.internal.systemui.navbar.threebutton","label":"3 Button Navigation Bar","description":"The default system navbar? It's what you use when you don't use gesture navigation.","removal":"caution","type":"aosp"},{"id":"com.android.internal.systemui.navbar.transparent","description":"Allows 'Transparent navigation bar' to be enabled in Developer options.","removal":"caution","type":"aosp"},{"id":"com.android.internal.systemui.navbar.twobutton","label":"2 Button Navigation Bar","description":"Enables a setting for using just 2 buttons in the system navbar?","removal":"caution","type":"aosp"},{"id":"com.android.internal.systemui.onehanded.gestural","description":"one-handed mode, which can be found in the settings.\none-handed mode will not work. Safe to remove if you dont use these setting.","removal":"replace","type":"aosp"},{"id":"com.android.keychain","description":"Enables apps to use system wide credential KeyChain (shared credentials between apps)\nhttps://security.stackexchange.com/questions/216716/android-keychain-what-is-a-system-wide-credential\n","removal":"unsafe","type":"aosp"},{"id":"com.android.launcher3","label":"Quickstep","description":"The AOSP launcher. OEMs frequently use this to deliver their own launcher.\nYou need to install another launcher before removing it.","removal":"caution","warning":"You need to install another launcher before removing it.","suggestions":"launchers","type":"aosp"},{"id":"com.android.localtransport","description":"Backup transport for stashing stuff into a known location on disk, and later restoring from there.\nNeeded for storing backup data locally on a device?\nThis package also provides the backup confirmation UI.\nhttps://developer.android.com/guide/topics/data/testingbackup","removal":"unsafe","type":"aosp"},{"id":"com.android.location.fused","description":"Manages underlying location technologies, such as GPS and Wi-Fi.","removal":"unsafe","type":"aosp"},{"id":"com.android.magicsmoke","label":"Magic Smoke Wallpapers","description":"Bulit-in Live wallpaper.","removal":"delete","type":"aosp"},{"id":"com.android.managedprovisioning","label":"Work Setup","description":"Work Setup/Work profile setup\nManages Android user account profiles.\nThe typical use-case is setting up a corporate profile that is controlled by the employer on an employee's personal device, to keep personal and work data separate.","web":["https://support.google.com/work/android/answer/6191949","https://developers.google.com/android/work/requirements/work-profile","https://beta.pithus.org/report/922fa478f5b2a8784e33626f04ff039d510b9dd7d5fd06db5c55002b5b5afae1"],"removal":"caution","warning":"Needed for sandbox apps such as Shelter or Insular/Island.","type":"aosp"},{"id":"com.android.mms","label":"Messages","description":"The AOSP SMS app.\nOccasionally runs in the background.\nSome OEMs (like Huawei, Xiaomi, Vivo, Oppo) use the same package name for their app.","removal":"replace","suggestions":"sms","type":"aosp"},{"id":"com.android.mms.service","description":"Provides support for sending MMS.\nIt doesn't cause bootloop.","removal":"caution","type":"aosp"},{"id":"com.android.modulemetadata","label":"Module Metadata","description":"It's used to manage and store metadata about installed modules, and is accessed by the system server.","removal":"unsafe","warning":"Breaks some Android core functionalities if disabled.","type":"aosp"},{"id":"com.android.mtp","description":"MTP Host\nHandles MTP(Media Transfer Protocol), a protocol for transfering files between the device and a connected PC.","removal":"unsafe","type":"aosp"},{"id":"com.android.musicfx","label":"MusicFX","description":"Audio EQ (equalizer). Some 3rd-party music apps can use it to provide you EQ features.","removal":"replace","type":"aosp"},{"id":"com.android.musicvis","label":"Music Visualization Wallpapers","description":"Built-in live wallpaper","removal":"delete","type":"aosp"},{"id":"com.android.nearby.halfsheet","description":"Useless frameworks to Wi-Fi connections, USB tethering, auto, usage.\nEvery version has random code and the app is not running in the background.","removal":"delete","type":"aosp"},{"id":"com.android.networkstack.inprocess","label":"NetworkStack","description":"Related to the Network Stack module, which is an updatable Mainline module that ensures Android can adapt to evolving network standards and allows for interoperability with new implementations","web":["https://source.android.com/docs/core/ota/modular-system/networking"],"removal":"unsafe","type":"aosp"},{"id":"com.android.networkstack.inprocess.overlay","description":"Related to the Network Stack module,\nwhich is an updatable Mainline module that ensures Android can adapt to evolving network standards and allows for interoperability with new implementations\nhttps://source.android.com/docs/core/ota/modular-system/networking","removal":"unsafe","type":"aosp"},{"id":"com.android.networkstack.overlay","description":"WiFi will not work after remove.","removal":"unsafe","type":"aosp"},{"id":"com.android.networkstack.permissionconfig","description":"Defines a permission that enables modules to perform network-related tasks.","web":["https://source.android.com/devices/architecture/modular-system/networking"],"removal":"unsafe","type":"aosp"},{"id":"com.android.networkstack.tethering.inprocess.overlay","description":"Related to the Tethering module,\nwhich allows an Android device to share its internet connection with other connected client devices.\nThis package contains classes and components that are used for in-process overlay functionality within the Tethering module.\nhttps://source.android.com/docs/core/ota/modular-system/tethering","removal":"unsafe","type":"aosp"},{"id":"com.android.networkstack.tethering.overlay","description":"Component of the Network, Tethering module.\nPackage is not a publicly documented.","removal":"unsafe","type":"aosp"},{"id":"com.android.nfc","label":"Nfc Service","description":"Runs in the background as part of the System.\nI assume NFC breaks when disabled.\nWill probably run even if disabled, like most system packages. So disabling/uninstalling is probably pointless.","removal":"caution","type":"aosp"},{"id":"com.android.noisefield","label":"Bubbles","description":"Built-in live wallpaper.","removal":"delete","type":"aosp"},{"id":"com.android.ondevicepersonalization.services","description":"OnDevicePersonalization. Another thing to AdServices privacy sandbox.\nIntroduced in Android 13.\nhttps://source.android.com/docs/core/ota/modular-system/ondevicepersonalization","removal":"delete","type":"aosp"},{"id":"com.android.ons","label":"com.android.ons","description":"ons = Opportunistic Network Service\nFrom what I can glean in the source code it seems like this provides a list of available networks and assigns each network a priority.\nI've never seen it run on its own, so this might be part of some automatic network switching setting that I have turned off.","web":["https://cs.android.com/android/platform/superproject/+/master:packages/services/AlternativeNetworkAccess/src/com/android/ons/OpportunisticNetworkService.java","https://developer.android.com/reference/android/telephony/AvailableNetworkInfo","https://cs.android.com/android/platform/superproject/+/master:frameworks/base/telephony/java/android/telephony/AvailableNetworkInfo.java"],"removal":"caution","type":"aosp"},{"id":"com.android.otaprovisioningclient","label":"OTA Access Point Configuration","description":"OTA (Over the air) is the method used by OEMs to push updates to your device.\nAn OTA access point is used to run system software updates over a special gateway. This package is most likely customized by your OEM.","removal":"caution","type":"aosp"},{"id":"com.android.overlay.systemui","description":"On some phones, it is an overlay to app \"com.google.android.apps.safetyhub\".\nCheck out this app code and think about it.","removal":"caution","type":"aosp"},{"id":"com.android.packageinstaller","description":"Handles installation, upgrade, and removal of applications.\n","removal":"unsafe","type":"aosp"},{"id":"com.android.pacprocessor","label":"PacProcessor","description":"PAC (Proxy Auto-Config) is a file which defines how an app can automatically find the correct proxy server for fetching an URL.\nShould be safe to remove if you don't use Auto-proxy (with PAC file config).","web":["https://en.wikipedia.org/wiki/Proxy_auto-config"],"removal":"caution","type":"aosp"},{"id":"com.android.phasebeam","label":"Phase beam","description":"Built-in live wallpaper","removal":"delete","type":"aosp"},{"id":"com.android.phone","description":"AOSP Dialer\nRemoving this package breaks the software update/download and install screen on Samsung. WARNING: for me, it breaks the phone app completely with call routing enabled. Not sure about other cases.","removal":"caution","type":"aosp"},{"id":"com.android.phone.a_overlay","description":"AOSP code for dialer app features.\nSIM card will not be detected if disabled.","removal":"unsafe","type":"aosp"},{"id":"com.android.phone.basiccolorblack.overlay","description":"Dark theme for phone app?","removal":"caution","type":"aosp"},{"id":"com.android.phone.basiccolorwhite.overlay","description":"Light theme for phone app?","removal":"caution","type":"aosp"},{"id":"com.android.phone.overlay.common","description":"Location and dialer things.","removal":"unsafe","type":"aosp"},{"id":"com.android.phone.recorder","label":"Recorder","description":"AOSP Call recorder function. Most of the time OEM use their own code for this.\nSome OEMs (like Huawei & Xiaomi) use the same package name for their app","removal":"replace","suggestions":"call_recorders","type":"aosp"},{"id":"com.android.printservice.recommendation","label":"Print Service Recommendation Service","description":"Recommends 3rd-party print services apps in the PlayStore. Printing will probably still work without it (by using the default print service).","removal":"replace","type":"aosp"},{"id":"com.android.printspooler","label":"Print Spooler","description":"Manages the printing process.\nRuns on boot, but not beyond that.","removal":"caution","warning":"Apart from breaking the printing functionality, it also breaks the connection preferences submenu in the settings app on most devices.","type":"aosp"},{"id":"com.android.protips","label":"Home screen tips","description":"Runs on boot.\nThe tip popups you get on the homescreen.","removal":"delete","type":"aosp"},{"id":"com.android.providers.applications","description":"Provides a list of installed applications.\nContent providers encapsulate data, providing centralized management of data shared between apps.\nhttps://developer.android.com/guide/topics/providers/content-providers.html","removal":"unsafe","type":"aosp"},{"id":"com.android.providers.blockednumber","label":"Blocked Numbers Storage","description":"Handles blocked number storage.\nOn some devices this seems to be tied to the recent apps menu.\nContent providers encapsulate data, providing centralized management of data shared between apps.","web":["https://gitlab.com/W1nst0n/universal-android-debloater/-/issues/6","https://developer.android.com/guide/topics/providers/content-providers.html"],"removal":"caution","type":"aosp"},{"id":"com.android.providers.calendar","label":"Calendar Storage","description":"Necessary for the stock Calendar app to work correctly.\nContent providers encapsulate data, providing centralized management of data shared between apps.","web":["https://developer.android.com/guide/topics/providers/content-providers.html"],"removal":"caution","type":"aosp"},{"id":"com.android.providers.contacts","label":"Contacts Storage","description":"Provider for contact data.\nContent providers encapsulate data, providing centralized management of data shared between apps.","web":["https://developer.android.com/guide/topics/providers/content-providers.html"],"removal":"caution","warning":"Breaks contact functionality if disabled. Not recommended to disable if you plan to use your device as a phone.","type":"aosp"},{"id":"com.android.providers.downloads","description":"Downloads Manager\nProvider for downloaded files.\nContent providers encapsulate data, providing centralized management of data shared between apps.\nhttps://developer.android.com/guide/topics/providers/content-providers.html","removal":"unsafe","type":"aosp"},{"id":"com.android.providers.downloads.ui","description":"Downloads\nUser interface for downloads.\nOn some OEM's this app has ads, tracking things.","removal":"replace","type":"aosp"},{"id":"com.android.providers.drm","label":"DRM Protected Content Storage","description":"Manages DRM storage on the device?\nProbably required for some forms of DRM; disabling might break things like Netflix streaming, which relies on DRM to function.","web":["https://en.wikipedia.org/wiki/Digital_rights_management"],"removal":"caution","type":"aosp"},{"id":"com.android.providers.media","label":"Media Storage","description":"Provider of media files (images, videos and such).\nScans the device for media files and allows permitted apps access to them.\nContent providers encapsulate data, providing centralized management of data shared between apps.","web":["https://developer.android.com/guide/topics/providers/content-providers.html"],"removal":"unsafe","warning":"Breaks features related to media storage (images, videos, music, etc.) if disabled","type":"aosp"},{"id":"com.android.providers.partnerbookmarks","label":"com.android.providers.partnerbookmarks","description":"Provides bookmarks about partners of Google in Chrome.\n","removal":"delete","type":"aosp"},{"id":"com.android.providers.settings","description":"Provider for settings app data.\nContent providers encapsulate data, providing centralized management of data shared between apps.\nhttps://developer.android.com/guide/topics/providers/content-providers.html","removal":"unsafe","type":"aosp"},{"id":"com.android.providers.telephony","description":"Provider for telephony data.\nHandles phone-related data such as text messages, APN list, etc.\nContent providers encapsulate data, providing centralized management of data shared between apps.\nhttps://developer.android.com/guide/topics/providers/content-providers.html","removal":"unsafe","type":"aosp"},{"id":"com.android.providers.userdictionary","label":"User Dictionary","description":"Handles user dictionary for keyboard apps.\nContent providers encapsulate data, providing centralized management of data shared between apps.","web":["https://developer.android.com/guide/topics/providers/content-providers.html"],"removal":"caution","warning":"Removing this package may cause settings menu to crash on some Huawei phones","type":"aosp"},{"id":"com.android.provision","description":"Provisioning is the process of setting up a network connection that will allow new users. \nThis service is for example needed when the user's phone moves from one cell-tower to another.\n","removal":"unsafe","type":"aosp"},{"id":"com.android.proxyhandler","label":"ProxyHandler","description":"Handles proxy config.","removal":"caution","warning":"Do not remove if you use a system proxy","type":"aosp"},{"id":"com.android.quicksearchbox","label":"Quick Search","description":"Google quick search box. OEMs (e.g. Xiaomi) can modify this for their own use.","removal":"delete","type":"aosp"},{"id":"com.android.remoteprovisioner","description":"RemoteProvisioner. Have random stuff: security, notifications, accessibility, test modes, data usage, metrics, logs.\nIts something new introduced in Android 13.\nAt this time this app is not available for users.\nAnd looks very useless.","removal":"delete","type":"aosp"},{"id":"com.android.rkpdapp","description":"RemoteProvisioner. Have random stuff: security, notifications, accessibility, test modes, data usage, metrics, logs.\nIntroduced in android 14(it's the same app like `com.android.remoteprovisioner` Introduced in android 13).\nAgain this app is not available for users.\nAnd looks very useless.","removal":"delete","type":"aosp"},{"id":"com.android.runintest.ddrtest","label":"DDRTest","description":"RAM Stress tester\nCan be run from the bootloader\nNOTE: I'm not sure it's really from AOSP (seen in TCL Plex phone)","removal":"delete","type":"aosp"},{"id":"com.android.safetycenter.resources","description":"Google Safety Center.\nProbably affects malware detection in new app installs, Gmail, and Chrome. This will also revert back the \"Security & privacy\" look to the old style.\nYou can use a libre spam-blocking and DNS-blocking solution instead of this.\nhttps://safety.google","removal":"delete","type":"aosp"},{"id":"com.android.sdksandbox","description":"Introduced in Android 13 privacy sandbox beta disabled on default.\nCauses bootloop. Maybe this component is not only for privacy... (I think it's for testing privacy sandbox using Android Studio.)\nhttps://source.android.com/docs/core/ota/modular-system/adservices\nCause BOOTLOOP.","removal":"unsafe","type":"aosp"},{"id":"com.android.se","label":"SecureElementApplication","description":"Runs in the background as part of the system.\nUnderlying implementation for the OMAPI SE service.\nEnables apps to use the OpenMobile API to access secure elements(SE) to enable smart-card payments and other secure services.\n\nAn SE is a special chip (e.g SIM-card) for storing cryptographic secrets in a way that makes illicit use hard.\nThe Open Mobile Alliance (OPA) is a standards organization which develops open standards for the mobile phone industry.","removal":"caution","warning":"ColorOS password lock requires this package.","type":"aosp"},{"id":"com.android.se.overlay.target","description":"Looks like needed to 'com.android.se'.","removal":"replace","type":"aosp"},{"id":"com.android.server.NetworkPermissionConfig","description":"Network configurations.","removal":"unsafe","type":"aosp"},{"id":"com.android.server.telecom","description":"Manages calls via your network provider or SIM and controls the phone modem?","removal":"unsafe","type":"aosp"},{"id":"com.android.server.telecom.a_overlay","description":"Overlay for com.android.server.telecom?","removal":"unsafe","type":"aosp"},{"id":"com.android.server.telecom.basiccolorblack.overlay","description":"Dark theme for something related to call network management?","removal":"caution","type":"aosp"},{"id":"com.android.server.telecom.basiccolorwhite.overlay","description":"Light theme for something related to call network management?","removal":"caution","type":"aosp"},{"id":"com.android.server.telecom.overlay.common","description":"Location and dialer things.","removal":"unsafe","type":"aosp"},{"id":"com.android.settings","description":"AOSP Settings app.","removal":"unsafe","type":"aosp"},{"id":"com.android.settings.basiccolorblack.overlay","description":"Dark theme overlay for the Settings app?","removal":"caution","type":"aosp"},{"id":"com.android.settings.basiccolorwhite.overlay","description":"Light theme overlay for the Settings app?","removal":"caution","type":"aosp"},{"id":"com.android.settings.intelligence","label":"Settings Suggestions","description":"Handles the search and suggestions features in the settings app.\nDoesn't run in the background, so there's little benefit in disabling.","web":["https://gitlab.com/W1nst0n/universal-android-debloater/-/issues/51"],"removal":"caution","warning":"Disabling this package makes the Settings app crash when you tap on search.","type":"aosp"},{"id":"com.android.settings.intelligence.basiccolorblack.overlay","description":"Dark theme overlay for the search functionality in the Settings app?","removal":"caution","type":"aosp"},{"id":"com.android.settings.intelligence.basiccolorwhite.overlay","description":"Light theme overlay for the search functionality in the Settings app?","removal":"caution","type":"aosp"},{"id":"com.android.sharedstoragebackup","label":"com.android.sharedstoragebackup","description":"Used during backup. Backs up the shared storage? (files accessible by every app with STORAGE permission)\nThings have changed with Android 10. Don't know if this package is still relevant for new phones.","web":["https://blog.mindorks.com/understanding-the-scoped-storage-in-android"],"removal":"caution","type":"aosp"},{"id":"com.android.shell","description":"Shell\nUnix shell that receives ADB commands sent from a PC.\nThis is what UAD-ng uses to execute commands on Android devices. Proobably a bad idea to disable ;)","removal":"unsafe","type":"aosp"},{"id":"com.android.simappdialog","label":"SIM App Dialog","description":"Creates a pop-up asking if the user wants to install the carrier app when a SIM is inserted. Seems to be event-triggered, i.e., doesn't run in the background.","web":["https://android.googlesource.com/platform/frameworks/base/+/master/packages/SimAppDialog/src/com/android/simappdialog/InstallCarrierAppActivity.java"],"removal":"caution","type":"aosp"},{"id":"com.android.smspush","label":"com.android.smspush","description":"This service is used to push/send specially formatted SMS messages that display an alert message to the user, and give them the option of connecting directly to a particular app.\nFor instance, an SMS notifying the user of a new e-mail, with a URL link to connect directly to the e-mail app.","web":["https://web.archive.org/web/20200915164901/https://www.nowsms.com/doc/submitting-sms-messages/sending-wap-push-messages"],"removal":"replace","suggestions":"sms","type":"aosp"},{"id":"com.android.soundrecorder","label":"Sound Recorder","description":"AOSP Sound recorder. OEMs often use their own solution.\nSome phones (Huawei and Xiaomi) also use this package name for their own recorder app.","removal":"replace","suggestions":"audio_recorders","type":"aosp"},{"id":"com.android.statementservice","description":"Intent Filter Verification Service\nA Statement protocol allows websites to certify that some assets represent them. Android package can to subscribe to handling chosen URIs. This package will then be called to query the website and verify that it allows this. Android package can subscribe to handling chosen URIs. This package will then be called to query the website and verify that it allows this. Sources:\n- https://developer.android.com/reference/android/content/Intent\n- https://developer.android.com/guide/components/intents-filters\n - https://android.stackexchange.com/questions/191163/what-does-the-intent-filter-verification-service-app-from-google-do\n - https://github.com/google/digitalassetlinks/blob/master/well-known/details.md\n - https://android.googlesource.com/platform/frameworks/base/+/6a34bb2","removal":"unsafe","type":"aosp"},{"id":"com.android.stk","label":"SIM Toolkit","description":"Enables carriers to initiate \"value-added services\". Basically, some operators provide SIM-cards with applications installed on them.\nThis has been abused:\n- SimJacker \n- WIBattack.\nNOTE: removing this package removes the launcher icon. \"com.android.stk\" relies on \"com.android.stk2\" and vice-versa.","web":["https://en.wikipedia.org/wiki/SIM_Application_Toolkit#cite_note-CellularZA-1","https://thehackernews.com/2019/09/simjacker-mobile-hacking.html","https://www.zdnet.com/article/new-sim-card-attack-disclosed-similar-to-simjacker/","https://en.wikipedia.org/wiki/Mobile_identity_management"],"removal":"caution","warning":"Disabling/uninstalling this package will break mobile identity management which could be used by apps (for example, your Bank) to authenticate you.","type":"aosp"},{"id":"com.android.stk2","label":"SIM Toolkit","description":"Special package for dual-sim devices?\nEnables carriers to initiate \"value-added services\". Basically, some operators provide SIM-cards with applications installed on them.\nThis has been abused:\n- SimJacker \n- WIBattack.\nNOTE: removing this package removes the launcher icon. \"com.android.stk2\" relies on \"com.android.stk\" and vice-versa.","web":["https://en.wikipedia.org/wiki/SIM_Application_Toolkit#cite_note-CellularZA-1","https://thehackernews.com/2019/09/simjacker-mobile-hacking.html","https://www.zdnet.com/article/new-sim-card-attack-disclosed-similar-to-simjacker/","https://en.wikipedia.org/wiki/Mobile_identity_management"],"removal":"caution","warning":"Vulnerable to hacking, should be disabled.","type":"aosp"},{"id":"com.android.storagemanager","label":"Smart Storage","description":"Storage manager (Maintenance/Storage panel in the settings)\nClean up unused files, show size of files regrouped by categories.","removal":"caution","warning":"May break the storage settings in Android Settings.","type":"aosp"},{"id":"com.android.systemui","description":"Everything you see in Android that's not an app. User interface of Android\n","removal":"unsafe","type":"aosp"},{"id":"com.android.systemui.accessibility.accessibilitymenu","description":"Hidden menu that only shows 2 buttons:\nLarge buttons - that increases size of accessibility menu buttons,\nand Help - that redirects to support google com site accessibility.","removal":"delete","type":"aosp"},{"id":"com.android.systemui.accessibility.accessibilitymenu.auto_generated_rro_product__","description":"Product RRO for Accessibility menu.","removal":"replace","type":"aosp"},{"id":"com.android.systemui.gesture.line.overlay","description":"Configurations to navigation bar.","removal":"unsafe","type":"aosp"},{"id":"com.android.systemui.icon.overlay","description":"In code found configs icon mask.","removal":"caution","type":"aosp"},{"id":"com.android.systemui.navigation.bar.overlay","description":"Configurations to navigation bar.","removal":"unsafe","type":"aosp"},{"id":"com.android.systemui.overlay","description":"System UI Overlay. DO NOT remove this.","removal":"unsafe","type":"aosp"},{"id":"com.android.systemui.overlay.common","description":"System UI Theme pack\nThe package name is pretty self-explanatory.","removal":"delete","type":"aosp"},{"id":"com.android.systemui.theme.dark","label":"Dark","description":"Enables you to use Android dark theme.","removal":"caution","type":"aosp"},{"id":"com.android.theme.font.notoserifsource","description":"Noto Serif / Source Sans Pro","removal":"delete","type":"aosp"},{"id":"com.android.theme.icon.circle","label":"Circle","description":"Android icons pack [Circle]","removal":"delete","type":"aosp"},{"id":"com.android.theme.icon.pebble","label":"Pebble","description":"Android icons pack [Pebble]","removal":"delete","type":"aosp"},{"id":"com.android.theme.icon.square","label":"Square","description":"Android icons pack [Square]","removal":"delete","type":"aosp"},{"id":"com.android.theme.icon.taperedrect","label":"Tapered Rect","description":"Android icons pack [Taperedrect]","removal":"delete","type":"aosp"},{"id":"com.android.theme.icon.vessel","label":"Vessel","description":"Android icons pack [Vessel]","removal":"delete","type":"aosp"},{"id":"com.android.theme.icon_pack.rounded.systemui","label":"Rounded","description":"Android icons pack [Rounded]","removal":"delete","type":"aosp"},{"id":"com.android.theme.icon_pack.rounded.themepicker","label":"Rounded","description":"Obviously related to the \"rounded\" icon pack but the full package is strange. A themepicker class only for a specific icon package?","removal":"delete","type":"aosp"},{"id":"com.android.timezone.updater","label":"Time Zone Updater","description":"Automatically updates the clock to correspond to your current time zone.","removal":"caution","warning":"This may cause a bootloop if removed. Timezone packages often causes that.","type":"aosp"},{"id":"com.android.traceur","label":"System Tracing","description":"Recording device activity over a short period of time is known as system tracing. System tracing produces a trace file that can be used to generate a system report.\nNot useful if you're not a developer.","web":["https://developer.android.com/topic/performance/tracing"],"removal":"delete","type":"aosp"},{"id":"com.android.uwb.resources","description":"Ultra-wideband (UWB) communication feature.\nUWB is a radio technology that enables precise ranging between devices,\nAllowing for accurate location measurements with an accuracy of 10 cm.\nhttps://developer.android.com/develop/connectivity/uwb\nhttps://source.android.com/docs/core/connect/uwb","removal":"unsafe","type":"aosp"},{"id":"com.android.virtualmachine.res","description":"unknown app with no code that only has permissions to Use, Manage, Debug: Virtual Machine.\nIntroduced in Android 14.","removal":"delete","type":"aosp"},{"id":"com.android.voicedialer","label":"Voice Dialer","description":"The AOSP Voice dialer. Lets you call someone or open an app with your voice from the dialer.\nOEM often use their own code (embeded in their voice-controlled digital assistant)\nSome OEMs (Huawei, Sony, Xiaomi) also use this package name for their own voice dialer app.","removal":"replace","suggestions":"dialers","type":"aosp"},{"id":"com.android.vpndialogs","label":"VpnDialogs","description":"Provide VPN support to Android\nSafe to remove if you don't plan to use a VPN.","removal":"caution","type":"aosp"},{"id":"com.android.wallpaper.holospiral","label":"Holo Spiral","description":"Built-in live wallpaper.","removal":"delete","type":"aosp"},{"id":"com.android.wallpaper.livepicker","label":"Live Wallpaper Picker","description":"Enables you to pick a live wallpaper.","removal":"caution","warning":"Removing it will break some weather applications (especially ones with widgets) and wallpaper applications like Muzei.","type":"aosp"},{"id":"com.android.wallpaper.livepicker.overlay","label":"com.android.wallpaper.livepicker.overlay","description":"Overlay for live wallpaper picker? Overlays are usually themes, but not sure about this one.","removal":"delete","type":"aosp"},{"id":"com.android.wallpaperbackup","label":"com.android.wallpaperbackup","description":"Backs up and restores wallpaper and metadata related to it.\nThis agent has its own package because it does full backup as opposed to SystemBackupAgent which does key/value backup.\nThis class stages wallpaper files for backup by copying them into its own directory because of the following reasons:\nNon-system users don't have permission to read the directory that the system stores the wallpaper files in\nBackupAgent enforces that backed up files must live inside the package's getFilesDir()\nThere are 3 files to back up:\nThe \"wallpaper info\" file which contains metadata like the crop applied to the wallpaper or the live wallpaper component name.\nThe \"system\" wallpaper file.\nAn optional \"lock\" wallpaper, which is shown on the lockscreen instead of the system wallpaper if set.\nOn restore, the metadata file is parsed and WallpaperManager APIs are used to set the wallpaper.\nNote that if there's a live wallpaper, the live wallpaper package name will be part of the metadata file and the wallpaper will be applied when the package it's installed.","web":["https://android.googlesource.com/platform/frameworks/base/+/master/packages/WallpaperBackup/src/com/android/wallpaperbackup/WallpaperBackupAgent.java"],"removal":"delete","type":"aosp"},{"id":"com.android.wallpapercropper","label":"com.android.wallpapercropper","description":"Wallpaper cropper.","removal":"delete","type":"aosp"},{"id":"com.android.wallpaperpicker","label":"com.android.wallpaperpicker","description":"Enables you to pick a wallpaper.","removal":"caution","type":"aosp"},{"id":"com.android.webview","label":"Android System WebView","description":"enables Android apps to display web content within the app itself, based on Chrome.","removal":"caution","warning":"Make sure to have another Webview before uninstalling it or some apps may not work properly and crash.","suggestions":"webviews","type":"aosp"},{"id":"com.android.wifi.dialog","description":"Needed for wifi dialogs.\nCan brick basic functionality android.","removal":"unsafe","type":"aosp"},{"id":"com.android.wifi.mainline.resources.overlay","description":"Related to the Wi-Fi module in the AOSP. The Wi-Fi module is a part of Project Mainline,\nWhich allows for updates to specific system components outside of the normal Android release cycle.\nThe package contains resources and overlays that are used to customize the Wi-Fi module.\nThese overlays can be used to override default configurations and customize the behavior of the Wi-Fi module\nhttps://source.android.com/docs/core/ota/modular-system/wifi\nhttps://www.xda-developers.com/android-project-mainline-modules-explanation","removal":"unsafe","type":"aosp"},{"id":"com.android.wifi.resources","label":"System Wi-Fi Resources","description":"System Wi-Fi resources.","removal":"unsafe","type":"aosp"},{"id":"com.android.wifi.resources.overlay","label":"com.android.wifi.resources.overlay","description":"Contains resources that can be overlaid or customized to modify the behavior of the Wi-Fi module.\nhttps://source.android.com/docs/core/ota/modular-system/wifi","removal":"unsafe","type":"aosp"},{"id":"com.android.wifi.resources.overlay.WifiResScanCountryCode","label":"com.android.wifi.resources.overlay.WifiResScanCountryCode","description":"Related to overlay for Wi-Fi scanning for country code.","removal":"caution","type":"aosp"},{"id":"com.android.wifi.system.mainline.resources.overlay","description":"Related to the Wi-Fi module and its resources overlay.\nThe Wi-Fi module in Android is updatable, meaning it can receive updates to functionality outside of the normal Android release cycle.\nThe module provides a consistent Wi-Fi experience across Android devices and allows for fixes to interoperability issues through module updates\nhttps://source.android.com/docs/core/ota/modular-system/wifi","removal":"caution","type":"aosp"},{"id":"com.android.wifi.system.resources.overlay","description":"Contains resources that can be overlaid to customize the Wi-Fi module's behavior.\nhttps://source.android.com/docs/core/ota/modular-system/wifi","removal":"caution","type":"aosp"},{"id":"com.example.android.notepad","label":"NotePad","description":"(Bad) notepad app.","removal":"replace","suggestions":"note_taking_apps","type":"aosp"},{"id":"com.google.android.adservices.api","description":"Introduced in Android 13 privacy sandbox beta components disabled on default.\nhttps://source.android.com/docs/core/ota/modular-system/adservices","removal":"delete","type":"aosp"},{"id":"com.google.android.androidforwork","description":"Assistant Android Work\nNot needed, theres only user consent activity about that:\n(Your organization controls your device and keeps it secure)","removal":"delete","type":"aosp"},{"id":"com.google.android.apps.googlecamera.fishfood","description":"ApertureLensLauncher\nNot sure how it works but redirects to google app lens `com.google.android.googlequicksearchbox`.","removal":"delete","type":"aosp"},{"id":"com.google.android.appsearch.apk","description":"AppSearch is an on-device search library for managing locally stored structured data, with APIs for indexing data and retrieving data using full-text search. Use it to build custom in-app search capabilities for your users.\nhttps://developer.android.com/jetpack/androidx/releases/appsearch","removal":"caution","type":"aosp"},{"id":"com.google.android.captiveportallogin","label":"CaptivePortalLogin","description":"it's the same as (com.android.captiveportallogin). Support for captive portal.\nA captive portal login is a web page where the users have to input their login information or accept the displayed terms of use. \nSome networks (typically public wifi network) use the captive portal login to block access until the user inputs \nsome necessary information\nNOTE : This package is a now a mandatory mainline module\nhttps://en.wikipedia.org/wiki/Captive_portal\nhttps://www.xda-developers.com/android-project-mainline-modules-explanation","removal":"caution","type":"aosp"},{"id":"com.google.android.carrierconfig","label":"com.google.android.carrierconfig","description":"Same as com.android.carrierconfig? Here's that description:\nDynamically provides configuration for the carrier network.\nThe config contains: Roaming networks, Voicemail settings, SMS/MMS settings, VoLTE/IMS settings, and more.\nIf a carrier app is installed it will be queried for overrides to these settings.\nSeems to run on boot and when you swap SIM card?","web":["https://source.android.com/devices/tech/config/carrier","https://cs.android.com/android/platform/superproject/+/master:packages/apps/CarrierConfig/src/com/android/carrierconfig/DefaultCarrierConfigService.java"],"removal":"replace","type":"aosp"},{"id":"com.google.android.connectivity.resources","label":"System Connectivity Resources","description":"Handles connectivity-related resources, such as Wi-Fi, Bluetooth, and cellular network management.","removal":"unsafe","warning":"Removing this packages causes a bootloop.","type":"aosp"},{"id":"com.google.android.documentsui","label":"Files","description":"Occasionally runs in the background.\nFile selector for other apps.","removal":"unsafe","warning":"Storage Access Framework (SAF) will break if this is disabled.","type":"aosp"},{"id":"com.google.android.email","label":"Email","description":"Newer versions of AOSP Mail are renamed to com.android.email and Gmail was migrated to com.google.android.gm","removal":"replace","suggestions":"email_clients","type":"aosp"},{"id":"com.google.android.ext.services","description":"The ExtServices module updates framework components for core OS functionality such as notification ranking, autofill text-matching strategies, storage cache, package watchdog, and other services that run continually. This module is updatable, meaning it can receive updates to functionality outside of the normal Android release cycle.\nCan run before the user unlocks the device (direct-boot aware) and Android 9+ version have internet and location permissions.\n\nWARNING: Causes bootloop on most Android 11+ phones. This module is related to the Android mainline project (which is a useful project).There is no reason to mess with this.\n\nSources:\nhttps://source.android.com/devices/architecture/modular-system/extservices\nhttps://arstechnica.com/gadgets/2016/11/android-extensions-could-be-googles-plan-to-make-android-updates-suck-less/\nPithus analysis (Android 11): https://beta.pithus.org/report/e5e4a181082b88baf55e19aab0f9cb62e131d612eeaa73cddb510a52e0ff5c1a","removal":"unsafe","type":"aosp"},{"id":"com.google.android.ext.shared","label":"Android Shared Library","description":"Used to share common code between apps.","removal":"unsafe","type":"aosp"},{"id":"com.google.android.federatedcompute","description":"FederatedCompute.\nAnother component of OnDevicePersonalization. But this app learns things about users.\nIntroduced in Android 13.\nhttps://source.android.com/docs/core/ota/modular-system/ondevicepersonalization","removal":"delete","type":"aosp"},{"id":"com.google.android.gallery3d","description":"Built-in Gallery app.\nThe ID could be \"recycled\" by OEMs for their own gallery implementations.","removal":"replace","type":"aosp"},{"id":"com.google.android.hotspot2.osulogin","description":"The sole purpose of the OsuLogin App is to provision credentials from the Wi-Fi network to the device and allow them to connect to Wi-Fi Hotspot 2.0. See https://hackanons.com/2021/07/osulogin-android-everything-you-need-to-know.html for more information.","removal":"replace","type":"aosp"},{"id":"com.google.android.modulemetadata","description":"Module that contains metadata about the list of modules on the device. And that’s about it.\nI wouldn't advise you to mess with it as it could break important modules (see #37)\nGood explanation of what android modules are : https://www.xda-developers.com/android-project-mainline-modules-explanation/","removal":"unsafe","type":"aosp"},{"id":"com.google.android.nearby.halfsheet","description":"Useless frameworks to Wi-Fi connections, USB tethering, auto, usage.\nEvery version has random code and the app is not running in the background.","removal":"delete","type":"aosp"},{"id":"com.google.android.networkstack","description":"Network Stack Components\nhttps://source.android.com/devices/architecture/modular-system/networking\nProvides common IP services, network connectivity monitoring, and captive login portal detection.\n","removal":"unsafe","type":"aosp"},{"id":"com.google.android.networkstack.overlay","description":"WiFi will not work after remove.","removal":"unsafe","type":"aosp"},{"id":"com.google.android.networkstack.permissionconfig","description":"Network Stack Permission Configuration\nDefines a permission that enables modules to perform network-related tasks.\nhttps://source.android.com/devices/architecture/modular-system/networking\n","removal":"unsafe","type":"aosp"},{"id":"com.google.android.networkstack.tethering","label":"Tethering","description":"Used for USB and/or Wi-Fi tethering?","removal":"unsafe","type":"aosp"},{"id":"com.google.android.networkstack.tethering.overlay","label":"com.google.android.networkstack.tethering.overlay","description":"Needed for tethering? I found: arrays.xml\narray name (config_tether_usb_regexs)\nitem rndis d\nitem usb d","removal":"unsafe","type":"aosp"},{"id":"com.google.android.ondevicepersonalization.services","description":"OnDevicePersonalization.\nAnother thing to AdServices privacy sandbox.\nIntroduced in Android 13.\nhttps://source.android.com/docs/core/ota/modular-system/ondevicepersonalization","removal":"delete","type":"aosp"},{"id":"com.google.android.overlay.managedprovisioning","description":"In code I have seen lists of some system apps and stuff of `managedprovisioning` user.\nSafe to remove if you removed `com.android.managedprovisioning`.\nBut it probably doesn't affect anything.","removal":"caution","type":"aosp"},{"id":"com.google.android.overlay.modules.captiveportallogin.forframework","description":"Configs default captiveportallogin. (not needed)\nNo effects after remove.","removal":"delete","type":"aosp"},{"id":"com.google.android.overlay.modules.cellbroadcastservice","description":"","removal":"caution","type":"aosp"},{"id":"com.google.android.overlay.modules.ext.services","label":"com.google.android.overlay.modules.ext.services","dependencies":["com.google.android.ext.services"],"description":"It's overlay that depends on (com.google.android.ext.services)","removal":"unsafe","warning":"Removing it causes bootloop.","type":"aosp"},{"id":"com.google.android.overlay.modules.permissioncontroller","label":"com.google.android.overlay.modules.permissioncontroller","description":"I only detect on this app: help_app_permissions (link below). It looks very useless, not needed.\nIt doesnt affect com.google.android.permissioncontroller","web":["https://support.google.com/googleplay/answer/6270602"],"removal":"delete","warning":"when disabling this package on a Samsung N960F running Android Q, the pop-up UI reverts. Disabling this package (as it's an overlay) will also put the pop-up UI to (like full-screen notification) to the center instead of the bottom.","type":"aosp"},{"id":"com.google.android.overlay.modules.permissioncontroller.forframework","label":"com.google.android.overlay.modules.permissioncontroller.forframework","description":"config_incidentReportApproverPackage\nIt looks very useless.\nNot needed, it doesnt affect on (com.google.android.permissioncontroller)","removal":"caution","type":"aosp"},{"id":"com.google.android.overlay.permissioncontroller","description":"Breaks Google Play System updates (GPSu), related to Project Mainline. Page on Settings will crash altogether, or ask Play Store to be updated.\nhttps://support.google.com/product-documentation/answer/14343500","dependencies":["com.android.vending"],"removal":"replace","type":"aosp"},{"id":"com.google.android.packageinstaller","description":"Google package installer. Seems to replace com.android.packageinstaller on newer phones. It is strangely not needed on older devices (you can still install APKs without it by using the AOSP package installer) but since Android 9, it also handles permissions control and could bootloop your device if removed.\nOn Android 8.1, disabling the app also disabled the 'Permissions' settings within all the apps. Besides that, I couldn't install an '.apk' file download from outside the Play Store.\nSource: https://source.android.com/docs/core/architecture/modular-system/permissioncontroller.","removal":"unsafe","type":"aosp"},{"id":"com.google.android.packageinstaller.a_overlay","description":"Gives ability to install, update or remove applications on the device.\nIf you delete this package, your phone will probably bootloop.\n","removal":"unsafe","type":"aosp"},{"id":"com.google.android.permissioncontroller","description":"Permission controller\nControls app permissions.\nhttps://source.android.com/devices/architecture/modular-system/permissioncontroller","removal":"unsafe","type":"aosp"},{"id":"com.google.android.photopicker","description":"Photo picker\nProvides a browsable interface that presents the user with their media library, sorted by date from newest to oldest. Safe, built-in way for users to grant your app access to only selected images and videos, instead of their entire media library.\nhttps://developer.android.com/training/data-storage/shared/photopicker","removal":"caution","type":"aosp"},{"id":"com.google.android.printservice.recommendation","label":"Print Service Recommendation Service","description":"I think this has to do with recommending a printservice app you can get from the Play store. I think printing still works with this off.","removal":"caution","type":"aosp"},{"id":"com.google.android.providers.media.module","label":"Media","description":"In Android 11+ this provides file access to apps without \"Manage All Files\" permission. In previous versions it's just an index of all audio-visual (no text) files in the user's file-system (`/sdcard/`, `/storage/emulated/0/`, etc...). This allows music-players and photo-galleries to list files quickly, without caching their own indices..\nContent providers encapsulate data, providing centralized management of data shared between apps.","web":["https://developer.android.com/guide/topics/providers/content-providers.html"],"removal":"unsafe","warning":"Breaks file browsers and other forms of file access.","type":"aosp"},{"id":"com.google.android.safetycenter.resources","label":"Google Safety Center Resources","description":"Google Safe Browsing.","web":["https://www.android.com/safety/"],"removal":"caution","warning":"Crashes Security and Privacy settings if removed.","type":"aosp"},{"id":"com.google.android.sdksandbox","description":"Introduced in Android 13 privacy sandbox beta disabled on default.\nCauses bootloop. Maybe this component is not only for privacy... (I think it's for testing privacy sandbox using Android Studio.)\nhttps://source.android.com/docs/core/ota/modular-system/adservices\nCause BOOTLOOP.","removal":"unsafe","type":"aosp"},{"id":"com.google.android.speech.pumpkin","description":"PumpkinService\nHas something to speech.","removal":"replace","type":"aosp"},{"id":"com.google.mainline.adservices","description":"Adservices Train Version Package. AdServices too.\nuses ondevicepersonalization.\nIntroduced in Android 13\nNOTE: This package is a mandatory mainline module, which is also not documented. I don't trust it when it comes to adservices.","removal":"delete","type":"aosp"},{"id":"org.chromium.webview_shell","description":"Simple Browser for WebView tester used in AOSP.","removal":"delete","type":"aosp"},{"id":"ca.bell.wt.android.tunesappswidget","label":"App Widget","description":"Developped by Bell Canada, it is a home screen widget which shows advertisements, promotions, news, sports & entertainment.","web":["https://play.google.com/store/apps/details?id=ca.bell.wt.android.tunesappswidget"],"removal":"delete","type":"carrier"},{"id":"com.LogiaGroup.LogiaDeck","label":"Mobile Services Manager","description":"Seems to be a spyware","web":["https://www.reddit.com/r/lgv20/comments/6u0wnf/what_is_mobile_services_manager_did_i_catch_a/"],"removal":"delete","type":"carrier"},{"id":"com.Rogers.MyRogersTab","description":"Appears to be the tablet version of MyRogers (https://play.google.com/store/apps/details?id=com.fivemobile.myaccount), an app to manage your account with Canadian carrier Rogers.","removal":"delete","type":"carrier"},{"id":"com.aetherpal.attdh.se","label":"Device Help","description":"Device Help for AT&T Samsung device.\nDeveloped by Aetherpal, a company which sells smart remote controls tools.\nI guess this app is used for tech support.","web":["https://docs.samsungknox.com/CCMode/G935A_O.pdf","https://en.wikipedia.org/wiki/AetherPal"],"removal":"delete","type":"carrier"},{"id":"com.aetherpal.attdh.zte","label":"Device Help","description":"Device Help for AT&T ZTE devices.\nDeveloped by Aetherpal, a company which sells smart remote controls tools.\nI guess this app is used for tech support.","web":["https://en.wikipedia.org/wiki/AetherPal"],"removal":"delete","type":"carrier"},{"id":"com.altice.android.myapps","description":"MyApps\nit's probably for install apps but it's useless.","removal":"delete","type":"carrier"},{"id":"com.americanexpress.plenti","description":"Plenti\nGet points and promos with your American Express card","removal":"delete","type":"carrier"},{"id":"com.android.partnerbrowsercustomizations.tmobile","description":"The proprietary application of the mobile operator T-Mobile, presumably displays ads, is responsible for the operation of some of the operator's settings","removal":"delete","type":"carrier"},{"id":"com.android.sprint.hiddenmenuapp","label":"HiddenMenu","description":"Lets you access hidden features tests/settings (you need to type a special code in the dialer).","web":["https://bestcellular.com/dial-codes/"],"removal":"delete","type":"carrier"},{"id":"com.android.wifi.resources.overlay.WifiVodafoneOverlay","label":"com.android.wifi.resources.overlay.WifiVodafoneOverlay","description":"Not sure what it does","web":["https://beta.pithus.org/report/d8b19f854eb85ea97fbaeafb8c11842cf9b27f169b08d3e8b2659f52db9dd408"],"removal":"delete","type":"carrier"},{"id":"com.asurion.android.mobilerecovery.att","label":"AT&T Protect Plus","description":"Discontinued and replaced by AT&T ProTech (com.asurion.android.protech.att)\nHelp and support app. Lets you call or chat with a live U.S.-based AT&T ProTech support expert.","web":["https://en.wikipedia.org/wiki/Asurion"],"removal":"delete","warning":"This app is developed by Asurion, a US company whose business is to sell insurances. All US carriers use Asurion for the phone insurances.","type":"carrier"},{"id":"com.asurion.android.mobilerecovery.sprint","label":"Sprint Protect","description":"Let you find a phone, secure from viruses, back up data, and more.","web":["https://en.wikipedia.org/wiki/Asurion"],"removal":"delete","warning":"This app is developed by Asurion, a US company whose business is to sell insurances. All US carriers use Asurion for the phone insurances.","type":"carrier"},{"id":"com.asurion.android.mobilerecovery.sprint.vpl","label":"Sprint Protect","description":"Let you find a phone, secure from viruses, back up data, and more.","web":["https://en.wikipedia.org/wiki/Asurion"],"removal":"delete","warning":"This app is developed by Asurion, a US company whose business is to sell insurances. All US carriers use Asurion for the phone insurances.","type":"carrier"},{"id":"com.asurion.android.protech.att","label":"AT&T ProTech","description":"Help and support app. Lets you call or chat with a live U.S.-based AT&T ProTech support \"expert\".","web":["https://en.wikipedia.org/wiki/Asurion"],"removal":"delete","warning":"This app is developed by Asurion, a US company whose business is to sell insurances. All US carriers use Asurion for the phone insurances.","type":"carrier"},{"id":"com.asurion.android.verizon.vms","label":"Digital Secure","description":"Verizon's one-stop suite of privacy and security tools that is supposed to protect your devices from online threats, connect to public Wi-Fi with a secure VPN, take control with always-on dark web monitoring, and get guidance on online security from \"security experts\".","web":["https://play.google.com/store/apps/details?id=com.asurion.android.verizon.vms","https://en.wikipedia.org/wiki/Asurion"],"removal":"delete","warning":"This app is developed by Asurion, a US company whose business is to sell insurances. All US carriers use Asurion for the phone insurances.","type":"carrier"},{"id":"com.asurion.home.sprint","label":"Sprint Complete","description":"Lets you call or chat with live tech experts! Maybe you will find the love of your life!","web":["https://en.wikipedia.org/wiki/Asurion"],"removal":"delete","warning":"This app is developed by Asurion, a US company whose business is to sell insurances. All US carriers use Asurion for the phone insurances.","type":"carrier"},{"id":"com.asurion.home.sprint.vpl","label":"Tech Expert","description":"Replaced by \"Sprint Complete\".","web":["https://en.wikipedia.org/wiki/Asurion"],"removal":"delete","warning":"This app is developed by Asurion, a US company whose business is to sell insurances. All US carriers use Asurion for the phone insurances.","type":"carrier"},{"id":"com.att.android.attsmartwifi","label":"AT&T Smart Wi-Fi","description":"Finds and auto-connects to available hotspots to minimize cellular data consumption.\nAuto-connects is not a good idea. You are ok if you go on HTTPS websites. Use a VPN if you want to hide the domain names you visit, avoid usage restriction (no P2P, blacklisted websites...) and encrypt HTTP traffic.","web":["https://play.google.com/store/apps/details?id=com.att.android.attsmartwifi","https://www.europol.europa.eu/activities-services/public-awareness-and-prevention-guides/risks-of-using-public-wi-fi","https://www.eff.org/deeplinks/2020/01/why-public-wi-fi-lot-safer-you-think","https://thatoneprivacysite.net/choosing-the-best-vpn-for-you/"],"removal":"delete","type":"carrier"},{"id":"com.att.callprotect","label":"AT&T Call Protect","description":"Spam call blocking app provided by Hiya.","web":["https://itmunch.com/robocall-caught-sending-customers-confidential-data-without-consent/"],"removal":"delete","warning":"Never trust an app that automatically blocks spam calls.","type":"carrier"},{"id":"com.att.csoiam.mobilekey","label":"AT&T Sign in Helper","description":"Allows AT&T applications to securely authenticate on Android devices.","web":["https://play.google.com/store/apps/details?id=com.att.csoiam.mobilekey"],"removal":"delete","type":"carrier"},{"id":"com.att.deviceunlock","description":"Device Unlock\nUseless app from AT&T. it's only for unlock device.","removal":"delete","type":"carrier"},{"id":"com.att.dh","label":"Device Help","description":"Troubleshooting app from AT&T.","web":["https://play.google.com/store/apps/details?id=com.att.dh"],"removal":"delete","type":"carrier"},{"id":"com.att.dtv.shaderemote","label":"DIRECTV Remote App","description":"Lets you control DIRECTV HD receivers in your home that are connected to Internet, from your phone. DIRECTV is a subsidiary of AT&T.","web":["https://en.wikipedia.org/wiki/DirecTV#Consumer_protection_lawsuits_and_violations"],"removal":"delete","type":"carrier"},{"id":"com.att.iqi","label":"Device Health","description":"(AKA Carrier IQ) Gathers, stores and forwards diagnostic measurements on its behalf.\nGreat! A rootkit.","web":["https://docs.samsungknox.com/CCMode/G935A_O.pdf","https://en.wikipedia.org/wiki/Carrier_IQ#Rootkit_discovery_and_media_attention"],"removal":"delete","type":"carrier"},{"id":"com.att.mobile.android.vvm","label":"AT&T Visual Voicemail","description":"Lets you manage your voicemail directly from the app without the need to dial into your mailbox.","web":["https://play.google.com/store/apps/details?id=com.att.mobile.android.vvm"],"removal":"delete","type":"carrier"},{"id":"com.att.mobilesecurity","label":"AT&T ActiveArmor℠","description":"AT&T android antivirus.","web":["https://play.google.com/store/apps/details?id=com.att.mobilesecurity"],"removal":"delete","type":"carrier"},{"id":"com.att.mobiletransfer","label":"AT&T Mobile Transfer","description":"Lets you transfer user data from an older AT&T phone to a new one.","removal":"delete","type":"carrier"},{"id":"com.att.myWireless","label":"myAT&T","description":"Lets you manage your AT&T account.","web":["https://play.google.com/store/apps/details?id=com.att.myWireless"],"removal":"delete","type":"carrier"},{"id":"com.att.personalcloud","label":"AT&T Personal Cloud","description":"It has paid extra features and data are obviously not E2EE (i.e AT&T can access them)\nPrivacy nightmare and was poorly coded.","web":["https://play.google.com/store/apps/details?id=com.att.personalcloud","https://beta.pithus.org/report/bc54b5e2446ace90d9f992278d0ec320befe4983a76cb4fdcf47e565366e67b6"],"removal":"replace","suggestions":"cloud_services","type":"carrier"},{"id":"com.att.tv","label":"DIRECTV","description":"Lets you Stream TV live and on demand from your phone.","web":["https://play.google.com/store/apps/details?id=com.att.tv"],"removal":"replace","suggestions":"streaming_apps","type":"carrier"},{"id":"com.att.tv.watchtv","label":"AT&T WatchTV","description":"Lets you stream TV live and VOD form your phone.\nNo it's not the same thing than AT&T TV. Yes, it's a mess.","web":["https://www.cordcuttersnews.com/att-tv-vs-att-tv-now-whats-the-difference/"],"removal":"replace","suggestions":"streaming_apps","type":"carrier"},{"id":"com.aura.jet.att","description":"AT&T Hub\nApp from AT&T. Installs apps on oobe and its maded by advertising company 'ironSource'.","removal":"delete","type":"carrier"},{"id":"com.aura.oobe.att","description":"AppCloud\nApp from AT&T. Installs apps on oobe and its maded by advertising company 'ironSource'.","removal":"delete","type":"carrier"},{"id":"com.aura.oobe.motorola","description":"MotoApps\nIt's something to install apps, but it's an advertising company.","removal":"delete","type":"carrier"},{"id":"com.aura.oobe.samsung","label":"AppCloud","description":"It offers the \"Aura Out-Of-the-Box Experience\" (OOBE)\nIt is some kind of post-install recommended apps setup from the carrier. Asks for your age and gender and then recommends you to install popular apps.\nHas way too many permissions.","web":["https://en.wikipedia.org/wiki/IronSource","https://aura.ironsrc.com/tools/drive-app-downloads/","https://arxiv.org/pdf/2010.10088.pdf","https://github.com/0x192/universal-android-debloater/issues/278"],"removal":"delete","warning":"This app is developed by IronSource, an Israeli advertising company.","type":"carrier"},{"id":"com.aura.oobe.samsung.gl","label":"AppCloud","description":"It offers the \"Aura Out-Of-the-Box Experience\" (OOBE)\nIt is some kind of post-install recommended apps setup from the carrier. Asks for your age and gender and then recommends you to install popular apps.\nHas way too many permissions.","web":["https://en.wikipedia.org/wiki/IronSource","https://aura.ironsrc.com/tools/drive-app-downloads/","https://arxiv.org/pdf/2010.10088.pdf","https://github.com/0x192/universal-android-debloater/issues/278"],"removal":"delete","warning":"This app is developed by IronSource, an Israeli advertising company.","type":"carrier"},{"id":"com.aura.oobe.vodafone","description":"Vodafone AppBox\nIt is some kind of post-install recommended apps setup from the carrier.\nAsks for your age and gender and then recommends you to install popular apps.\nDeveloped by IronSource, an Israeli advertising company.","removal":"delete","type":"carrier"},{"id":"com.bc360.android.service","description":"Verizon Adaptive Sound\nProvides Voice Enhance, but according to the carrier.\nDoes the same thing as the 'com.bc360.control' app.","removal":"delete","type":"carrier"},{"id":"com.bc360.control","description":"Verizon Adaptive Sound\nProvides Voice Enhance, but according to the carrier.","removal":"delete","type":"carrier"},{"id":"com.claroColombia.contenedor","description":"Claro\nIt's something to install apps, but it's an advertising company.","removal":"delete","type":"carrier"},{"id":"com.cricketwireless.minus","description":"Cricket partner tab? better remove it. It probably have news or partner customization to chrome.","removal":"delete","type":"carrier"},{"id":"com.customermobile.preload.vzw","label":"Verizon Store Demo Mode","description":"Requires a lot of permissions and downloads a remote configuration file from an AWS-hosted domain over plain-text HTTP.\nThis leaves the overall device and configuration vulnerable.","web":["https://thehackernews.com/2024/08/google-pixel-devices-shipped-with.html"],"removal":"delete","type":"carrier"},{"id":"com.directv.promo.shade","description":"DIRECTV Remote\nOfficial app from DIRECTV (subsidiary of AT&T). With the DIRECTV Remote for AT&T Samsung devices, control of your favorite DIRECTV shows is just a swipe away. Swipe down from the status bar at the top of your screen to automatically connect and control your DIRECTV receivers, it's that simple.","removal":"delete","type":"carrier"},{"id":"com.dti.amx","description":"it's used to choose app install? A lot trackers, permissions.\nUseless.","removal":"delete","type":"carrier"},{"id":"com.dti.att","label":"Mobile Services Manager","description":"Formerly known as AT&T App Select.\nI guess it lets you choose AT&T apps to install.\nIt has a LOT of permissions.","web":["https://knowledge.protektoid.com/apps/com.dti.att/7a36d4f5f00bae044566221400719c75ea2f4f33bc2578a7f8210f36d718a8d6"],"removal":"delete","type":"carrier"},{"id":"com.dti.bouyguestelecom","description":"Bouygues AppCloud\nit's probably for install apps but it's useless and have ads and a lot permissions.","removal":"delete","type":"carrier"},{"id":"com.dti.cricket","description":"it's app for installing recommended apps? it's only used on first-boot setup and it's useless.","removal":"delete","type":"carrier"},{"id":"com.dti.motorola","description":"Mobile Services Manager\nit's something for install apps but it's useless and a lot permissions.","removal":"delete","type":"carrier"},{"id":"com.dti.samsung","description":"Mobile Services Manager\nDigital Turbine app, pre-install some apps/games to your phone and its made by advertising company.","removal":"delete","type":"carrier"},{"id":"com.dti.tim","description":"Mobile Service Manager\nIt's a system app that can't be opened but keeps running in the background. It can install/uninstall apps without notifying you, access internet, run at the boot of the system, kill background processes, ads and other permissions.\nIt's only bloatware, OTA updates work the same without it. Uninstalling didn't give any negative side-effects.","removal":"delete","type":"carrier"},{"id":"com.dti.tracfone","label":"Mobile Services","description":"Installs sponsored apps automatically on Tracfone and affiliated carriers (Straight Talk, Total Wireless, etc)","removal":"delete","type":"carrier"},{"id":"com.felicanetworks.mfc","description":"Chinese felicanetworks","removal":"delete","type":"carrier"},{"id":"com.felicanetworks.mfm","description":"Setup Chinese felicanetworks","removal":"delete","type":"carrier"},{"id":"com.felicanetworks.mfm.main","description":"Chinese felicanetworks","removal":"delete","type":"carrier"},{"id":"com.felicanetworks.mfs","description":"Chinese felicanetworks","removal":"delete","type":"carrier"},{"id":"com.felicanetworks.mfw.a.boot","description":"Chinese felicanetworks","removal":"delete","type":"carrier"},{"id":"com.google.omadm.trigger","description":"OemDmTrigger\nOMA Device Managment Verizon.","removal":"delete","type":"carrier"},{"id":"com.gsma.rcs","description":"RCS\nHidden RCS Messaging? or only Frameworks?\nFeature code:184501\nCarrier name: GSM Association.","removal":"caution","type":"carrier"},{"id":"com.hyperlync.Sprint.Backup","label":"Sprint Backup","description":"Lets you backup your phone’s content to your Sprint Backup account.\nThis app was developed by Hyperlync Technologies an Israel-based company which provides cyber-security solutions. It is now owned by Edition Ltd, a big Singapore based company.","web":["https://www.reuters.com/companies/EDITol.SI"],"removal":"replace","suggestions":"backup_apps","type":"carrier"},{"id":"com.hyperlync.Sprint.CloudBinder","label":"Sprint Cloud Binder","description":"Hub for all you cloud accounts.\nThis app was developed by Hyperlync Technologies an Israel-based company which provide cyber-security solutions.\nIt is now owned by Edition Ltd, a big Singapore based company.","web":["https://www.reuters.com/companies/EDITol.SI"],"removal":"delete","type":"carrier"},{"id":"com.inmobi.installer","description":"it's installer advertising company app.","removal":"delete","type":"carrier"},{"id":"com.ironsource.appcloud.oobe.hutchison","description":"AppCloud (discontinued) from ironSource, an advertising company.\nWorth reading:\nhttps://en.wikipedia.org/wiki/IronSource","removal":"delete","type":"carrier"},{"id":"com.jrd.verizonuriintentservice","description":"","removal":"caution","type":"carrier"},{"id":"com.kmsjp","description":"Kaspersky\nAnti-virus pre-installed on some Huawei phone's in Japanese.","removal":"delete","type":"carrier"},{"id":"com.kuackmedia.orange","description":"Altice Music\nIt's a music application that allows you to stream music, but it comes pre-installed.\nhttps://play.google.com/store/apps/details?id=com.kuackmedia.orange","removal":"delete","type":"carrier"},{"id":"com.locationlabs.cni.att","label":"AT&T Smart Limits℠","description":"A parental Control app.","web":["https://m.att.com/shopmobile/wireless/features/smart-limits.html"],"removal":"delete","type":"carrier"},{"id":"com.locationlabs.finder.sprint","label":"Sprint Family Locator","description":"Lets you locate any phone registered under the Sprint family plan.\nLocation labs is owned by AGV which is owned by Avast.\nYou shouldn't trust Avast.","web":["https://www.vice.com/en_us/article/qjdkq7/avast-antivirus-sells-user-browsing-data-investigation","https://www.pcmag.com/news/the-cost-of-avasts-free-antivirus-companies-can-spy-on-your-clicks"],"removal":"replace","suggestions":"locators","type":"carrier"},{"id":"com.locationlabs.finder.sprint.vpl","label":"Sprint Family Locator","description":"Lets you locate any phone registered under the Sprint family plan\nLocation labs is owned by AGV which is owned by Avast.\nYou shouldn't trust Avast.","web":["https://www.vice.com/en_us/article/qjdkq7/avast-antivirus-sells-user-browsing-data-investigation","https://www.pcmag.com/news/the-cost-of-avasts-free-antivirus-companies-can-spy-on-your-clicks"],"removal":"replace","suggestions":"locators","type":"carrier"},{"id":"com.matchboxmobile.wisp","label":"AT&T Hot Spots","description":"Runs in background. Automatically connects you to a free AT&T wifi hotspot at one of their participating partner locations, such as Starbucks.","web":["https://docs.samsungknox.com/CCMode/G935A_O.pdf"],"removal":"delete","type":"carrier"},{"id":"com.mobitv.client.sprinttvng","label":"Sprint TV & Movies","description":"Provided by MobiTV. Lets you watch live TV and VOD.","web":["https://en.wikipedia.org/wiki/MobiTV"],"removal":"replace","suggestions":"streaming_apps","type":"carrier"},{"id":"com.mobitv.client.tmobiletvhd","label":"T-Mobile TV with Mobile HD","description":"Discontinued and replaced by nl.tmobiletv.vinson, provided by MobiTV.","web":["https://en.wikipedia.org/wiki/MobiTV","https://play.google.com/store/apps/details?id=nl.tmobiletv.vinson"],"removal":"replace","suggestions":"streaming_apps","type":"carrier"},{"id":"com.mobolize.sprint.securewifi","label":"Secure WiFi","description":"Sprint VPN app provided by Mobolize. You need to pay for using it.\nYou'd better use a reliable third-party VPN if you really need to use one.\nThis one runs in background all time and every time it sees a \"unsecured network\" it will popup to encourage you to pay for this VPN.","removal":"replace","suggestions":"vpn_services","type":"carrier"},{"id":"com.motorola.att.phone.extensions","label":"ATT Phone Extension","description":"Provide access to AT&T extensions in you dialer.","web":["https://asecare.att.com/tutorials/adding-and-deleting-an-extension-on-your-officehand-mobile-app-2990/?product=AT&T%20Office@Hand"],"removal":"delete","type":"carrier"},{"id":"com.motorola.attvowifi","label":"Wi-Fi Calling","description":"AT&T Wifi-calling app.","web":["https://www.att.com/shop/wireless/features/wifi-calling.html"],"removal":"delete","type":"carrier"},{"id":"com.motorola.carrierconfig","label":"Carrier Services","description":"Related to various communication related actions.","web":["https://source.android.com/docs/core/connect/carrier"],"removal":"caution","warning":"Disabling this app may cause network-related issues","type":"carrier"},{"id":"com.motorola.ltebroadcastservices_vzw","label":"com.motorola.ltebroadcastservices_vzw","description":"LTE Broadcast services from Verizon. Allows your phone to receive broadcast message from Verizon?","removal":"delete","type":"carrier"},{"id":"com.motorola.mot5gmod","label":"5G Moto Mod","description":"Internet sharing using USB tethering and WiFi hotspot.","web":["https://play.google.com/store/apps/details?id=com.motorola.mot5gmod"],"removal":"delete","type":"carrier"},{"id":"com.motorola.omadm.sprint","label":"SprintDM","description":"Configuration of the device (including first time use), enabling and disabling features provided by carriers.\nI believe it's only useful if you want to use a Sprint service with a non branded phone (not sure at all)\nDisplays annoying notifications if you unlocked your bootloader.","web":["https://www.androidpolice.com/2015/03/10/android-5-1-includes-new-carrier-provisioning-api-allows-carriers-easier-methods-of-setting-up-services-on-devices-they-dont-control/"],"removal":"delete","type":"carrier"},{"id":"com.motorola.omadm.usc","description":"OMA Device Management for Verizon \nHandles configuration of the device (including first time use), enabling and disabling features provided by carriers.\nhttps://en.wikipedia.org/wiki/OMA_Device_Management\nI believe it's only useful if you want to use a Verizon service with a non branded phone (not sure at all)\nhttps://www.androidpolice.com/2015/03/10/android-5-1-includes-new-carrier-provisioning-api-allows-carriers-easier-methods-of-setting-up-services-on-devices-they-dont-control/\nDisplays annoying notifications if you unlocked your bootloader","removal":"delete","type":"carrier"},{"id":"com.motorola.omadm.vzw","label":"VzwDM","description":"OMA Device Management for Verizon.\nHandles configuration of the device (including first time use), enabling and disabling features provided by carriers.\nI believe it's only useful if you want to use a Verizon service with a non branded phone (not sure at all)\nDisplays annoying notifications if you unlocked your bootloader.","web":["https://en.wikipedia.org/wiki/OMA_Device_Management","https://www.androidpolice.com/2015/03/10/android-5-1-includes-new-carrier-provisioning-api-allows-carriers-easier-methods-of-setting-up-services-on-devices-they-dont-control/"],"removal":"delete","type":"carrier"},{"id":"com.motorola.service.vzw.entitlement","label":"entitlement","description":"","web":["https://android.stackexchange.com/questions/226580/how-is-verizon-suddenly-tracking-my-hot-spot-usage-on-android-and-how-do-i-disab"],"removal":"caution","warning":"Deleting this package whill disable Hotspot functionality if you're a Verizon client. What you can do is preventing the phone from notifying the carrier about when you use hotspot.","type":"carrier"},{"id":"com.motorola.sprintwfc","label":"print Wifi Calling","description":"Sprint Wifi Calling\nProvides wifi calling to Sprint customers.","removal":"delete","type":"carrier"},{"id":"com.motorola.visualvoicemail","label":"Verizon Visual Voicemail","description":"On non-Verizon phones it has a generic \"Voicemail\" name and icon, and doesn't seem to active.","removal":"delete","type":"carrier"},{"id":"com.motorola.vzw.cloudsetup","label":"Cloud setup","description":"The exact functionality is unknown.","removal":"delete","type":"carrier"},{"id":"com.motorola.vzw.loader","label":"com.motorola.vzw.loader","description":"Exact functionality is unknown. Doesn't seem to break anything once removed.","removal":"delete","type":"carrier"},{"id":"com.motorola.vzw.mot5gmod","label":"5G Moto Mod","description":"Internet sharing using USB tethering and WiFi hotspot.","removal":"delete","type":"carrier"},{"id":"com.motorola.vzw.pco.extensions.pcoreceiver","label":"PcoReceiver","description":"VZW Carrier sim(Verizon). It's only for notifications.\nYou can remove that if you don't use Verizon wireless or anything like that.","removal":"delete","type":"carrier"},{"id":"com.motorola.vzw.phone.extensions","label":"PhoneExtns","description":"Free HD wallpaper from verizon","removal":"delete","type":"carrier"},{"id":"com.motorola.vzw.provider","label":"VzwUnifiedSettingsApp","description":"Exact functionality is unknown. Label might be incorrect. Doesn't seem to break anything once removed.","removal":"delete","type":"carrier"},{"id":"com.motricity.verizon.ssodownloadable","label":"Verizon Login","description":"Originally by Motricity, now Voltari.\nVoltari provides relevance-driven mobile advertising, mobile marketing, mobile merchandising, and predictive analytics solutions.\nNeeded for \"My Verizon\".","web":["https://en.wikipedia.org/wiki/Voltari","https://www.lightreading.com/motricity-holds-on-to-verizon-account/d/d-id/678478"],"removal":"delete","type":"carrier"},{"id":"com.naviexpert.NaviExpert","description":"Navigation T-Mobile\nhttps://play.google.com/store/apps/details?id=com.naviexpert.NaviExpert","removal":"delete","type":"carrier"},{"id":"com.nextbit.app","description":"docomo LIVE UX backup\nit's on some japanese phones","removal":"delete","type":"carrier"},{"id":"com.nim.rogers","description":"Texture, a digital magazine service created by Rogers Media. Discontinued in 2019.","removal":"delete","type":"carrier"},{"id":"com.nttdocomo.android.applicationmanager","description":"Docomo Application Manager","removal":"delete","type":"carrier"},{"id":"com.nttdocomo.android.dhome","description":"Docomo Launcher","removal":"replace","type":"carrier"},{"id":"com.nttdocomo.android.iconcier_contents","description":"Diagnostics things only in this japanese app","removal":"delete","type":"carrier"},{"id":"com.nttdocomo.android.initialization","description":"Docomo Initialization app","removal":"delete","type":"carrier"},{"id":"com.nttdocomo.android.rwpushcontroller","description":"rwpushcontroller\nAnother FeliCa Networks app, with japanese language\nUseless frameworks","removal":"delete","type":"carrier"},{"id":"com.nttdocomo.android.store","description":"Docomo App Market","removal":"delete","type":"carrier"},{"id":"com.oem.euiccpartnerapp","description":"EuiccPartnerApp\nNeeded for eSIM (eUICC)?\nI think it's useless.","removal":"delete","type":"carrier"},{"id":"com.orange.aura.oobe","label":"Orange Manual Selector","description":"Makes unnecessary notifications","removal":"delete","type":"carrier"},{"id":"com.orange.mail.fr","label":"Mail Orange","description":"Managing your emails from the Mail Orange application. All your personal data is hosted in France and governed by French law.","web":["https://play.google.com/store/apps/details?id=com.orange.mail.fr"],"removal":"replace","suggestions":"email_clients","type":"carrier"},{"id":"com.orange.miorange","label":"Mi Orange","description":"Lets you access to your Orange account and services","web":["https://play.google.com/store/apps/details?id=com.orange.miorange"],"removal":"delete","type":"carrier"},{"id":"com.orange.mylivebox.fr","label":"Ma Livebox","description":"Lets you manage your Livebox from your phone.","web":["https://play.google.com/store/apps/details?id=com.orange.mylivebox.fr"],"removal":"delete","type":"carrier"},{"id":"com.orange.mysosh","label":"MySosh France","description":"Lets you access to your Sosh account","web":["https://play.google.com/store/apps/details?id=com.orange.mysosh"],"removal":"delete","type":"carrier"},{"id":"com.orange.orangeetmoi","label":"Orange et moi France","description":"Orange customer space.","web":["https://play.google.com/store/apps/details?id=com.orange.orangeetmoi"],"removal":"delete","type":"carrier"},{"id":"com.orange.owtv","label":"TV d'Orange","description":"Lets you watch TV/VOD on your phone.","web":["https://play.google.com/store/apps/details?id=com.orange.owtv"],"removal":"delete","type":"carrier"},{"id":"com.orange.tdd","label":"Transfert des données","description":"Lets you transfer wirelessly: contacts, SMS, call log, calendar, photos, videos, audio files, etc., all from your old Android.","web":["https://play.google.com/store/apps/details?id=com.orange.tdd"],"removal":"delete","type":"carrier"},{"id":"com.orange.update","label":"App Center","description":"Handles Orange apps updates.","removal":"delete","type":"carrier"},{"id":"com.orange.update.OrangeUpdateApplication","label":"com.orange.update.OrangeUpdateApplication","description":"Obviously related to update.","removal":"delete","type":"carrier"},{"id":"com.orange.vvm","label":"Messagerie vocale visuelle","description":"Lets you manage your voicemail with an app.","web":["https://play.google.com/store/apps/details?id=com.orange.vvm"],"removal":"delete","type":"carrier"},{"id":"com.orange.wifiorange","label":"Mon Réseau","description":"Lets you measure your speed connection and find better Orange wifi hotspots.\nInforms you also about near network incidents.","removal":"delete","type":"carrier"},{"id":"com.ptc.osp.gnc","description":"Playing the Waiting Game by T-Mobile.\nThis app is maded for listening to music.","removal":"delete","type":"carrier"},{"id":"com.samsung.attvvm","label":"Samsung AT&T Visual Voicemail","description":"A simple GUI for voicemail.","removal":"delete","type":"carrier"},{"id":"com.samsung.huxextension","description":"Hux Extension\nVerizon activation, registration","removal":"delete","type":"carrier"},{"id":"com.samsung.slsi.telephony.oem.oemrilhookservice","description":"Part of the Samsung cellular modem infrastructure used by the OS to provide cellular support.","removal":"caution","type":"carrier"},{"id":"com.samsung.slsi.telephony.oemril","description":"Part of the Samsung cellular modem infrastructure used by the OS to provide cellular support.","removal":"caution","type":"carrier"},{"id":"com.samsung.sprint.chameleon","description":"Chameleon service which is a service designed to store sprint-specific properties (customizes some apps).","removal":"delete","type":"carrier"},{"id":"com.sec.android.app.ewidgetatt","label":"Entertainment Widget","description":"AT&T Widget for One UI.","web":["https://docs.samsungknox.com/CCMode/F707U_Q.pdf"],"removal":"delete","type":"carrier"},{"id":"com.sec.android.app.tfstatus","description":"Tracfone app, function unknown","removal":"caution","type":"carrier"},{"id":"com.sec.omadm","label":"OMADM","description":"Open Mobile Alliance Device Management. A protocol for management of mobile devices.","web":["https://en.wikipedia.org/wiki/OMA_Device_Management"],"removal":"caution","type":"carrier"},{"id":"com.sec.omadmspr.syncmlphoneif","label":"Sprint OMADM Phone Interface","description":"OMADM = Open Mobile Alliance Device Management. A protocol for management of mobile devices.","web":["https://docs.samsungknox.com/CCMode/G950U1_P.pdf","https://en.wikipedia.org/wiki/OMA_Device_Management"],"removal":"caution","type":"carrier"},{"id":"com.sec.sprint.wfcstub","label":"com.sec.sprint.wfcstub","description":"Seems to be related to Wifi-Calling on Samsung phone.","removal":"delete","type":"carrier"},{"id":"com.securityandprivacy.android.verizon.vms","label":"Digital Secure","description":"I don't know why this apps is released twice on the Play store under 2 different package name.","web":["https://play.google.com/store/apps/details?id=com.securityandprivacy.android.verizon.vms"],"removal":"delete","type":"carrier"},{"id":"com.sfr.android.moncompte","label":"SFR & Moi","description":"Lets you manage your SFR account.","web":["https://play.google.com/store/apps/details?id=com.sfr.android.moncompte"],"removal":"delete","type":"carrier"},{"id":"com.sfr.android.sfrcloud","label":"SFR Cloud","description":"Cloud provided by SFR","web":["https://play.google.com/store/apps/details?id=com.sfr.android.sfrcloud"],"removal":"replace","suggestions":"cloud_services","type":"carrier"},{"id":"com.sfr.android.sfrjeux","description":"My Games\nit's not useful app for games and better uninstall it","removal":"delete","type":"carrier"},{"id":"com.sfr.android.sfrmail","label":"SFR Mail","description":"SFR Mail","web":["https://play.google.com/store/apps/details?id=com.sfr.android.sfrmail"],"removal":"replace","suggestions":"email_clients","type":"carrier"},{"id":"com.sfr.android.sfrplay","label":"SFR Play","description":"VOD streaming from SFR.","removal":"delete","type":"carrier"},{"id":"com.sfr.android.vvm","label":"SFR Répondeur +","description":"Lets you use your voice mail and manage your inbox without dialing into your voicemail.","web":["https://play.google.com/store/apps/details?id=com.sfr.android.vvm"],"removal":"delete","type":"carrier"},{"id":"com.shannon.imsservice","description":"Verizon IMS service.","removal":"replace","type":"carrier"},{"id":"com.shannon.rcsservice","description":"Verizon RCS service.","removal":"replace","type":"carrier"},{"id":"com.sprint.android.musicplus2033","label":"Sprint Music Plus","description":"Sprint’s official Music Store and player.","removal":"replace","suggestions":"music_apps","type":"carrier"},{"id":"com.sprint.android.musicplus2033.vpl","label":"Sprint Music Plus","description":"Sprint’s official Music Store and player.","removal":"replace","suggestions":"music_apps","type":"carrier"},{"id":"com.sprint.care","label":"My Sprint","description":"Lets you manage your Sprint Account and pay your bill.","web":["https://play.google.com/store/apps/details?id=com.sprint.care"],"removal":"delete","type":"carrier"},{"id":"com.sprint.ce.updater","label":"Mobile Installer (ソフトバンク)","description":"Used by Sprint to (force) install/update Sprint apps.","web":["https://community.sprint.com/t5/Samsung/How-to-stop-quot-Mobile-Installer-quot-from-pushing-apps-to/td-p/1036387"],"removal":"delete","type":"carrier"},{"id":"com.sprint.ecid","label":"Caller ID","description":"Enables you to hide name and phone number when you make phone calls.","web":["https://www.sprint.com/en/support/solutions/services/restrict-your-caller-id-information.html"],"removal":"delete","type":"carrier"},{"id":"com.sprint.fng","label":"Sprint Spot","description":"Provides Sprint postpaid customers a way to discover and access apps, services, games, TV & video, music, and more.","removal":"delete","type":"carrier"},{"id":"com.sprint.international.message","label":"Sprint Worldwide","description":"A help page for international travelers.","removal":"delete","type":"carrier"},{"id":"com.sprint.ms.cdm","label":"Carrier Device Manager","description":"Mobile Device Management (MDM) allows company’s IT department to reach inside your phone in the background, allowing them to ensure your device is secure, know where it is, and remotely erase your data if the phone is stolen.","web":["https://onezero.medium.com/dont-put-your-work-email-on-your-personal-phone-ef7fef956c2f","https://blog.cdemi.io/never-accept-an-mdm-policy-on-your-personal-phone/"],"removal":"delete","warning":"NEVER install a MDM tool on your personal phone.","type":"carrier"},{"id":"com.sprint.ms.cnap","label":"Caller ID","description":"CNAP = Caller Name Presentation\nLets you change the name that is displayed on caller ID when making a call.\nStrange is it the same thing than \"com.sprint.ecid\" ?","web":["https://en.wikipedia.org/wiki/Calling_Name_Presentation"],"removal":"delete","type":"carrier"},{"id":"com.sprint.ms.smf.services","label":"Carrier Hub","description":"Enables Sprint features (including Wifi calling) and products for devices operating on the Sprint network.","web":["https://play.google.com/store/apps/details?id=com.sprint.ms.smf.services"],"removal":"delete","type":"carrier"},{"id":"com.sprint.psdg.sw","label":"Carrier Setup Wizard","description":"The first time you turn your device on, a Welcome screen is displayed. It guides you through the basics of setting up your device.\nHere it handles the setup of Sprint features/services.","removal":"delete","type":"carrier"},{"id":"com.sprint.safefound","label":"Safe & Found","description":"Mobile safety and security application that helps protect and locate your \"loved ones\".\nYou have the ability to track and manage smartphones, tablets and Tracker all in one app.","web":["https://play.google.com/store/apps/details?id=com.sprint.safefound","https://www.sprint.com/en/support/solutions/services/safe-and-found.html"],"removal":"delete","suggestions":"locators","type":"carrier"},{"id":"com.sprint.safefound.vpl","label":"Safe & Found","description":"Mobile safety and security application that helps protect and locate your \"loved ones\".\nYou have the ability to track and manage smartphones, tablets and Tracker all in one app.","web":["https://play.google.com/store/apps/details?id=com.sprint.safefound","https://www.sprint.com/en/support/solutions/services/safe-and-found.html"],"removal":"delete","type":"carrier"},{"id":"com.sprint.topup","label":"Sprint World Top-Up","description":"Doesn't exist anymore?","removal":"delete","type":"carrier"},{"id":"com.sprint.w.installer","label":"Mobile ID","description":"Formerly, Sprint ID. \nProvides mobile ID Packs featuring apps, ringers, wallpapers, widgets and more.\nCan (and do) force install apps you disabled.","removal":"delete","type":"carrier"},{"id":"com.sprint.w.v8","label":"Featured Apps","description":"Old app Discover App (discontinued / new package name)\nLets you discover Sprint apps?","removal":"delete","type":"carrier"},{"id":"com.sprint.zone","label":"Sprint Zone","description":"Helps the user find new apps, in addition to some carrier-specific functionality.","removal":"delete","type":"carrier"},{"id":"com.synchronoss.dcs.att.r2g","label":"AT&T Ready2Go","description":"Discontinued. Its purpose was to help you migrating your data to your new Android device.","removal":"delete","type":"carrier"},{"id":"com.tcl.vzwintents","description":"","removal":"caution","type":"carrier"},{"id":"com.tct.vzwwifioffload","description":"","removal":"caution","type":"carrier"},{"id":"com.telcel.contenedor","description":"Telcel app, Advertising company to get promotions.\nhttps://play.google.com/store/apps/details?id=com.telcel.contenedor","removal":"delete","type":"carrier"},{"id":"com.telecomsys.directedsms.android.SCG","label":"Verizon Location Agent","description":"Location tracking (does not impact GPS function if deleted, don't worry).","removal":"delete","type":"carrier"},{"id":"com.telus.checkup","description":"Checkup app (from Mobile Klinik; a Canadian store for buying, selling, and repairing smartphones)\nMainly used to run device-health diagnostics, estimate device value post-diagnostics (for re-selling at a Mobile Klinik location), and finding nearby Mobile Klinik locations to book appointments for device repair. Also contains ads and promotions for new devices and accessories.\nGenerally regarded as bloatware because diagnostics are generic at best, and is eager to request many unnecessary and potentially invasive device permissions.\nSafe to remove if you don't use it, and can be re-downloaded from the Google Play Store at any time.","removal":"delete","type":"carrier"},{"id":"com.telus.myaccount","label":"My TELUS","description":"It's used for managing your telus account.\nSafe to remove if you don't use it","removal":"delete","type":"carrier"},{"id":"com.tmobile.pr.adapt","label":"T-Mobile","description":"A diagnostic tool for T-Mobile. This app can see all your installed apps, that you have allowed unknown sources on, that your rooted, and will deny your warranty saying your rooted. It constantly runs in the background.","removal":"delete","type":"carrier"},{"id":"com.tmobile.pr.mytmobile","label":"T-Mobile","description":"T-mobile app","web":["https://play.google.com/store/apps/details?id=com.tmobile.pr.mytmobile"],"removal":"delete","type":"carrier"},{"id":"com.tmobile.services.nameid","label":"T-Mobile Scam Shield","description":"Name ID T-Mobile (powered by Hiya or cequint if on Samsung devices).","web":["https://play.google.com/store/apps/details?id=com.tmobile.services.nameid","https://techcrunch.com/2019/08/09/many-robocall-blocking-apps-send-your-private-data-without-permission/"],"removal":"delete","warning":"Never trust a company which promotes call ID/spam blocking features.","type":"carrier"},{"id":"com.tmobile.simlock","label":"Device Unlock","description":"Allows you to request and apply a mobile device unlock directly from the device.","web":["https://support.t-mobile.com/docs/DOC-14011"],"removal":"delete","type":"carrier"},{"id":"com.tmobile.vvm.application","label":"T-Mobile Visual Voicemail","description":"Lets you use your voice mail and manage your inbox without dialing into your voicemail.","web":["https://play.google.com/store/apps/details?id=com.tmobile.vvm.application"],"removal":"delete","type":"carrier"},{"id":"com.tracfone.preload.accountservices","description":"TracPhone / StraightTalk application. It just shows IMEI, SIM, and phone number, as well as a way to see device properties.\nComes preinstalled with any TracPhone or StraightTalk device. It can be downloaded from the playstore if needed for whatever reason.\nHas Approximate and Precise location permissions, and Device ID permission.\nRuns in the background for them to collect data.","removal":"delete","type":"carrier"},{"id":"com.vcast.mediamanager","label":"Verizon Cloud","description":"Verizon Cloud","web":["https://play.google.com/store/apps/details?id=com.vcast.mediamanager"],"removal":"replace","suggestions":"cloud_services","type":"carrier"},{"id":"com.verizon.cloudsetupwizard","description":"","removal":"delete","type":"carrier"},{"id":"com.verizon.llkagent","label":"Llkagent","description":"Used for Verizon store demo mode.","removal":"delete","type":"carrier"},{"id":"com.verizon.loginengine.unbranded","label":"Carrier Login Engine","description":"Needed for wifi-calling.","web":["https://forum.xda-developers.com/t/samsung-factory-unlocked-s9-s9-will-now-have-verizon-wi-fi-calling.3841547/"],"removal":"delete","type":"carrier"},{"id":"com.verizon.messaging.vzmsgs","label":"Verizon Messages","description":"Verizon Messages","web":["https://play.google.com/store/apps/details?id=com.verizon.messaging.vzmsgs"],"removal":"delete","type":"carrier"},{"id":"com.verizon.mips.services","label":"My Verizon Services","description":"Related to My Verizon app. Required for hotspot.","removal":"delete","type":"carrier"},{"id":"com.verizon.obdm","label":"D-MAT","description":"It's a set of metrics-related modules. Google Play uses the version of the Telemetry module to determine\nif updates are available for metrics-related modules and which security patch version to display to the end user.\nThis module doesn’t contain active code and has no functionality on its own.","web":["https://gitlab.com/W1nst0n/universal-android-debloater/-/issues/27#note_410012436"],"removal":"caution","warning":"Removing modules-related packages may not be safe since Android 11.","type":"carrier"},{"id":"com.verizon.obdm_permissions","label":"OBDM_Permissions","description":"Has a LOT of permissions!\nIt is used to hold shares and securities in dematerialised/electronic format.\nSeems weird that Verizon provides this so it's likely not this.","removal":"delete","type":"carrier"},{"id":"com.verizon.permissions.appdirectedsms","label":"com.verizon.permissions.appdirectedsms","description":"Custom permissions for some verizon stuff?","removal":"delete","type":"carrier"},{"id":"com.verizon.permissions.vzwappapn","label":"com.verizon.permissions.vzwappapn","description":"Custom permissions used to set Verizon APN?","web":["https://docs.samsungknox.com/CCMode/N900V.pdf"],"removal":"delete","type":"carrier"},{"id":"com.verizon.remoteSimlock","description":"VZWRemoteSimlockService\nRemote SimLock lock, unlock, looks more danger than useful app.","removal":"delete","type":"carrier"},{"id":"com.verizon.services","description":"AppDirectedSMS\nOMA Device Management for Verizon.\nit's for wifi calling, sms","removal":"replace","type":"carrier"},{"id":"com.verizon.vzwavs","label":"VzwAVS","description":"Has a scary list of permissions. Doesn't seems to break anything if removed.","removal":"delete","type":"carrier"},{"id":"com.verizontelematics.verizonhum","label":"Hum: GPS Locator","description":"Hum is the connected car solution that helps you take care of your car and everyone in it. Keep tabs on your car’s health and track of its location.","web":["https://play.google.com/store/apps/details?id=com.verizontelematics.verizonhum"],"removal":"delete","type":"carrier"},{"id":"com.vznavigator.Generic","label":"VZ Navigator","description":"VZ Navigator (GPS app)","removal":"delete","type":"carrier"},{"id":"com.vzw.apnlib","label":"apnlib","description":"Kind of library for Verizon APN?","web":["https://developer.android.com/reference/android/telephony/data/ApnSetting"],"removal":"caution","warning":"Removing it prevents calling or texting (breaks WiFi calling). On Auto Optimization, it causes auto reboots.","type":"carrier"},{"id":"com.vzw.apnservice","label":"VZWAPN","description":"APN Services.","web":["https://developer.android.com/reference/android/telephony/data/ApnSetting"],"removal":"delete","type":"carrier"},{"id":"com.vzw.easvalidation","description":"","removal":"caution","type":"carrier"},{"id":"com.vzw.ecid","label":"Verizon Call Filter","description":"Auto-block spam and report any unwanted numbers.\nNOTE : Never trust a company which promotes call ID/spam blocking features.","web":["https://play.google.com/store/apps/details?id=com.vzw.ecid"],"removal":"delete","type":"carrier"},{"id":"com.vzw.hss.myverizon","label":"My Verizon","description":"Lets you manage your Verizon account.","web":["https://play.google.com/store/apps/details?id=com.vzw.hss.myverizon"],"removal":"delete","type":"carrier"},{"id":"com.vzw.hss.widgets.infozone.large","label":"My InfoZone™ Widget:Big Screen","description":"Gives weekly tips, access to device info and account information.","web":["https://www.droid-life.com/2013/02/12/verizon-introduces-my-infozone-widget-allows-easy-access-to-tips-device-info-and-account-information/"],"removal":"delete","type":"carrier"},{"id":"com.vzw.qualitydatalog","label":"com.vzw.qualitydatalog","description":"Logging stuff","removal":"delete","type":"carrier"},{"id":"com.wavemarket.waplauncher","label":"AT&T Secure Family™","description":"Parental control app.\n7 trackers + 16 permissions.","web":["https://play.google.com/store/apps/details?id=com.wavemarket.waplauncher","https://reports.exodus-privacy.eu.org/en/reports/com.wavemarket.waplauncher/latest/"],"removal":"delete","type":"carrier"},{"id":"com.whitepages.nameid.tmobile","label":"T-Mobile Name ID","description":"By WhitePages. Discontinued and replaced by com.tmobile.services.nameid.","web":["https://www.whitepages.com/","https://www.fiercewireless.com/wireless/t-mobile-to-offer-name-id-service-from-whitepages","https://www.geekwire.com/2016/whitepages-spins-caller-id-spam-blocking-app-startup-hiya/"],"removal":"delete","type":"carrier"},{"id":"de.telekom.tsc","label":"AppEnabler","description":"TSC = Telecom Service Center. Used to display ads in notifications panel.","removal":"delete","type":"carrier"},{"id":"fr.bouyguestelecom.ecm.android","label":"Bouygues Telecom","description":"Lets you manage your Bouygues account.","web":["https://play.google.com/store/apps/details?id=fr.bouyguestelecom.ecm.android"],"removal":"delete","type":"carrier"},{"id":"fr.bouyguestelecom.tv.android","label":"B.tv","description":"Lets you watch TV from your phone.","web":["https://play.google.com/store/apps/details?id=fr.bouyguestelecom.tv.android"],"removal":"delete","type":"carrier"},{"id":"fr.bouyguestelecom.vvmandroid","label":"Messagerie vocale visuelle","description":"Voicemail application for Bouygues Telecom. This app may inject ads into your gallery","removal":"delete","type":"carrier"},{"id":"fr.orange.cineday","label":"Orange Cineday","description":"Useless app but Cineday is pretty nice.\nEvery Tuesday you can invite the person of your choice in movies (within the limit of available seats).\nYou can just use https://cineday.orange.fr/cineday/","removal":"delete","type":"carrier"},{"id":"hdopen.vivicitta","description":"Companion\nApp for training, so bloated and from T-Mobile","removal":"delete","type":"carrier"},{"id":"hu.telekom.telekomapp","description":"Telekom\nApp for subscriptions, so bloated (a lot spying) and from T-Mobile","removal":"delete","type":"carrier"},{"id":"hu.telekom.telekomtv","description":"Mobile shopping\nApp for watch tv on phone, so bloated (a lot spying) and from T-Mobile","removal":"delete","type":"carrier"},{"id":"jp.co.daj.consumer.ifilter.aflauncher","description":"Something Japanese with useless code.","removal":"delete","type":"carrier"},{"id":"jp.co.omronsoft.iwnnime.ml","description":"iWnn IME\nJapanese keyboard pre-installed on some Huawei phone's.","removal":"delete","type":"carrier"},{"id":"jp.co.omronsoft.wnnext.skin.std_dark_type2_HW","description":"Skin Dark mode to Japanese Keyboard.","removal":"delete","type":"carrier"},{"id":"jp.co.omronsoft.wnnext.skin.std_light_type2_HW","description":"Skin Light mode to Japanese Keyboard.","removal":"delete","type":"carrier"},{"id":"jp.co.yahoo.android.ebookjapan.preinstall","description":"eBookJapan\nNot needed japanese.","removal":"delete","type":"carrier"},{"id":"net.aetherpal.device","label":"AT&T Remote Support","description":"Provided by Aetherpal (was acquired by VMware). It allows an AT&T Advanced Support representative to assist you by accessing your device remotely.","removal":"delete","type":"carrier"},{"id":"pl.tmobile.miboa","description":"My T-Mobile\nHas login activity and MailBox.\nA lot metrics, analytics.","removal":"delete","type":"carrier"},{"id":"pl.tmobile.panel","description":"MyBox\nRequire sim card to run app\nit's app store(discontinued?)","removal":"delete","type":"carrier"},{"id":"ro.cosmote.aps.wnwlite","description":"TopApps(discontinued)\nit's non english app. Installs recommended apps?\nI can't launch the app because it displays an error that it can't connect to the Internet, even when Wi-Fi is on.","removal":"delete","type":"carrier"},{"id":"telekom.hu.android.mobilvasarlas","description":"Mobile shopping\nApp for shopping or paying, so bloated and from T-Mobile","removal":"delete","type":"carrier"},{"id":"tmobile.hu.android.epgmiab","description":"Newsreel\nAnother app to tv things, so bloated and from T-Mobile, not needed","removal":"delete","type":"carrier"},{"id":"uk.co.ee.myee","label":"My EE","description":"Lets you control your EE pay monthly, pay as you go and WiFi devices. Check your data, bills, packs and more, and keep an eye on your spending.\nContains unnecessary analytics and most of the things the app does can be done by texting 150 from your mobile.","web":["https://play.google.com/store/apps/details?id=uk.co.ee.myee","https://ee.co.uk/help/help-new/billing-usage-and-top-up/call-text-and-data-charges/how-can-i-get-help-by-texting-150-on-pay-as-you-go-or-flex","https://reports.exodus-privacy.eu.org/fr/reports/uk.co.ee.myee/latest/","https://beta.pithus.org/report/6e8de7e02aba34c4f02dc966b39002f60b0852f55da923cdccc4ba4c09ed4a4a"],"removal":"delete","type":"carrier"},{"id":"us.com.dt.iq.appsource.tmobile","label":"App Source","description":"Discontinued. This app aimed at organizing all of your existing apps on the phone by category and helping you discover new apps through search and recommendations.","removal":"delete","type":"carrier"},{"id":"com.android.chrome","label":"Google Chrome","description":"Google Chrome: Slow & Painful\nOccasionally runs in the background, not to mention how it tracks everything.","web":["https://play.google.com/store/apps/details?id=com.android.chrome","https://privacytests.org/android.html","https://fidoalliance.org/passkeys/"],"removal":"replace","warning":"Removing the app may break creating and storing passkeys on your phone, so keep this enabled if you want to use that form of authentication. Google Play Services can also provide this functionality on some devices. When Chrome is updated via the Play Store, the Trichrome Library is also updated automatically. If Chrome is disabled or removed, it may impact WebView functionality.\nThis is because Chrome, WebView, and the Trichrome Library work together as a bundle starting from Android 10 (API 29) and above.","suggestions":"browsers","type":"google"},{"id":"com.android.hotwordenrollment.okgoogle","label":"OK Google enrollment","description":"\"OK Google\" detection service that hears everything.","removal":"delete","type":"google"},{"id":"com.android.hotwordenrollment.xgoogle","label":"Google Assistant","description":"Formerly, X Google enrollment. \"OK Google\" detection service that hears everything.","removal":"delete","type":"google"},{"id":"com.android.partnerbrowsercustomizations.chromeHomepage","label":"com.android.partnerbrowsercustomizations.chromeHomepage","description":"Horrible stuff for Google Chrome. This package bypass your DNS settings (for letting pass Google ads).","removal":"delete","type":"google"},{"id":"com.android.soundpicker","label":"Sounds","description":"Needed to pick up a phone ringtone. No weird permissions.","web":["https://beta.pithus.org/report/f5f7c265c6d98666c78267b91643bbfb635021d5d4f85c93407079ba4aad88ee"],"removal":"caution","type":"google"},{"id":"com.android.systemui.plugin.globalactions.wallet","label":"com.android.systemui.plugin.globalactions.wallet","description":"Apk file name: QuickAccessWallet. This is the Google Pay widget in the power menu(hold power button for 1sec to show this menu), below the Emergency, Power off and Reboot buttons.","removal":"delete","type":"google"},{"id":"com.android.vending","label":"Google Play Store","description":"The malware delivery store. Most apps are full of ads, trackers and malware.","web":["https://www.xda-developers.com/google-play-store-more-safety/"],"removal":"caution","suggestions":"app_stores","type":"google"},{"id":"com.chrome.beta","label":"Chrome Beta","description":"The beta version of Google Chrome.","web":["https://play.google.com/store/apps/details?id=com.chrome.beta"],"removal":"replace","suggestions":"browsers","type":"google"},{"id":"com.chrome.canary","label":"Chrome Canary (Unstable)","description":"Canary version of Google Chrome, usually provides nightly builds.","web":["https://play.google.com/store/apps/details?id=com.chrome.canary"],"removal":"replace","suggestions":"browsers","type":"google"},{"id":"com.chrome.dev","label":"Chrome Dev","description":"Google Chrome developer edition.","web":["https://play.google.com/store/apps/details?id=com.chrome.dev"],"removal":"replace","suggestions":"browsers","type":"google"},{"id":"com.google.android.GoogleCamera","label":"Google Camera","description":"Camera with incredible features with the cost of tracking. Try the tracker-free mods if you're too much into Google Camera.","web":["https://play.google.com/store/apps/details?id=com.google.android.GoogleCamera"],"removal":"replace","suggestions":"cameras","type":"google"},{"id":"com.google.android.accessibility.soundamplifier","description":"Accessibility sound amplifier (https://play.google.com/store/apps/details?id=com.google.android.accessibility.soundamplifier)","removal":"delete","type":"google"},{"id":"com.google.android.apps.access.wifi.consumer","label":"Google Wifi","description":"Google Wifi app","removal":"delete","type":"google"},{"id":"com.google.android.apps.adm","label":"Google Find My Device","description":"Lets you locate your Android device.","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.adm"],"removal":"delete","suggestions":"locators","type":"google"},{"id":"com.google.android.apps.ads.publisher","label":"Google AdSense","description":"Google Adsense app","removal":"delete","type":"google"},{"id":"com.google.android.apps.adwords","label":"Google Ads","description":"Lets you monitor your ad campaigns.","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.adwords"],"removal":"delete","type":"google"},{"id":"com.google.android.apps.assistant","label":"Google Assistant Go","description":"Lightweight Google Assistant for low-end devices (Go edition)","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.assistant"],"removal":"delete","type":"google"},{"id":"com.google.android.apps.authenticator2","label":"Google Authenticator","description":"Generates 2-Step Verification codes on your phone.","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2"],"removal":"replace","suggestions":"authenticators","type":"google"},{"id":"com.google.android.apps.blogger","label":"Blogger","description":"Official app for Blogger/blogspot.com","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.blogger"],"removal":"delete","type":"google"},{"id":"com.google.android.apps.books","label":"Google Play Books","description":"Lets you buy and read ebooks, audiobooks, comics and manga.","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.books"],"removal":"replace","suggestions":"ebook_readers","type":"google"},{"id":"com.google.android.apps.chromecast.app","label":"Google Home","description":"Lets you set up, manage, and control your Google Nest, Google Wifi, Google Home and Chromecast devices. Let Google harvest your data with your money.","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.chromecast.app"],"removal":"delete","type":"google"},{"id":"com.google.android.apps.cloudprint","label":"Cloud Print","description":"Let you print anywhere from any Android tablet or smartphone via Google.","removal":"replace","type":"google"},{"id":"com.google.android.apps.cultural","label":"Google Arts & Culture","description":"Know and interact with arts but with a price: Your privacy.","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.cultural"],"removal":"delete","type":"google"},{"id":"com.google.android.apps.currents","label":"Google Currents","description":"Discontinued.","removal":"delete","type":"google"},{"id":"com.google.android.apps.docs","label":"Google Drive","description":"The drive where no personal data should ever be kept unencrypted.","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.docs"],"removal":"delete","suggestions":"cloud_services","type":"google"},{"id":"com.google.android.apps.docs.editors.docs","label":"Google Docs","description":"Google Docs client for Android","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.docs.editors.docs"],"removal":"delete","type":"google"},{"id":"com.google.android.apps.docs.editors.sheets","label":"Google Sheets","description":"Google Sheets client for Android","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.docs.editors.sheets"],"removal":"delete","type":"google"},{"id":"com.google.android.apps.docs.editors.slides","label":"Google Slides","description":"Google Slides client for Android","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.docs.editors.slides"],"removal":"delete","type":"google"},{"id":"com.google.android.apps.dynamite","label":"Google Chat","description":"Previously Hangout Chat, is a communication and collaboration tool focusing on conversation.","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.dynamite"],"removal":"delete","type":"google"},{"id":"com.google.android.apps.enterprise.cpanel","label":"Google Admin","description":"Lets you manage your Google Cloud account on-the-go.","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.enterprise.cpanel"],"removal":"delete","type":"google"},{"id":"com.google.android.apps.enterprise.dmagent","label":"Google Apps Device Policy","description":"Allows your IT administrator to mandate security settings like screen lock or device encryption and keep corporate data safe.","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.enterprise.dmagent"],"removal":"delete","type":"google"},{"id":"com.google.android.apps.fireball","label":"Google Allo","description":"Discontinued.","removal":"delete","type":"google"},{"id":"com.google.android.apps.fitness","label":"Google Fit","description":"Fitness tracker that does not respect your privacy.","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.fitness"],"removal":"delete","suggestions":"fitness_trackers","type":"google"},{"id":"com.google.android.apps.freighter","label":"Google Datally","description":"Discontinued.","removal":"delete","type":"google"},{"id":"com.google.android.apps.giant","label":"Google Analytics","description":"Lets you monitor all of your Analytics properties so that you can keep track of your business while you're on the go.","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.giant"],"removal":"delete","type":"google"},{"id":"com.google.android.apps.googleassistant","label":"Google Assistant","description":"The assistant that can stab you on the back.","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.googleassistant"],"removal":"delete","type":"google"},{"id":"com.google.android.apps.handwriting.ime","label":"Google Handwriting Input","description":"Google Handwriting Input","removal":"replace","type":"google"},{"id":"com.google.android.apps.hangoutsdialer","label":"Hangouts Dialer","description":"Discontinued.","removal":"delete","type":"google"},{"id":"com.google.android.apps.inbox","label":"Inbox by Gmail","description":"Discontinued.","removal":"delete","type":"google"},{"id":"com.google.android.apps.inputmethod.hindi","label":"Google Indic Keyboard","description":"(Discontinued) Google Keyboard + Hindi characters","removal":"replace","suggestions":"keyboards","type":"google"},{"id":"com.google.android.apps.kids.familylink","label":"Google Family Link","description":"Parental controls app from Google.","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.kids.familylink"],"removal":"delete","type":"google"},{"id":"com.google.android.apps.kids.familylinkhelper","label":"Family Link parental controls","description":"Companion app to Google Family Link.","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.kids.familylinkhelper"],"removal":"delete","type":"google"},{"id":"com.google.android.apps.m4b","label":"Google My Maps","description":"Discontinued.","removal":"delete","type":"google"},{"id":"com.google.android.apps.magazines","label":"Google News","description":"A personalized news aggregator that organizes and highlights what’s happening in the world.","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.magazines"],"removal":"replace","suggestions":"rss_readers","type":"google"},{"id":"com.google.android.apps.maps","label":"Google Maps","description":"A map navigator that makes it easier for the govt agencies to locate you.","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.maps"],"removal":"replace","suggestions":"maps","type":"google"},{"id":"com.google.android.apps.mapslite","label":"Google Maps Go","description":"A map navigator that makes it easier for the govt agencies to locate you.","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.mapslite"],"removal":"replace","suggestions":"maps","type":"google"},{"id":"com.google.android.apps.mediahome.launcher","description":"Entertainment Space\nAll-in-one application for entertainment purposes like movies, games, books etc.","removal":"delete","type":"google"},{"id":"com.google.android.apps.meetings","label":"Google Meet","description":"Formerly Hangouts Meet. Lets you create, schedule or join an online meeting.","removal":"replace","suggestions":"meeting_apps","type":"google"},{"id":"com.google.android.apps.messaging","label":"Messages","description":"RCS client from Google, also supports SMS/MMS. Runs in the background.\nCould be a global dependency for SMS, MMS, RCS, OTP, and other services/verifications.\nGSMA has recently standardized E2EE, but currently, there is not alternative available for Android.","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.messaging"],"removal":"replace","warning":"Once disabled/uninstalled, even if reinstalled, a factory reset may be required to re-obtain full functionality.","suggestions":"sms","type":"google"},{"id":"com.google.android.apps.navlite","label":"Navigation for Google Maps Go","description":"Provides GPS turn-by-turn voice guided navigation and is optimized for performance on low-memory phones.","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.navlite"],"removal":"replace","suggestions":"maps","type":"google"},{"id":"com.google.android.apps.nbu.files","label":"Files","description":"Used to be for cleaning and sharing. But nowadays, it became a hybrid app. Runs in the background.\nFOSS alternative is https://github.com/TeamAmaze/AmazeFileUtilities","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.nbu.files"],"removal":"delete","warning":"Android itself provides the options to clean up your device in Settings.","type":"google"},{"id":"com.google.android.apps.nbu.paisa.user","label":"Google Pay","description":"Digital wallet and payment system.\nYou really should not trust Google not to sell your data (even if they claim the contrary).\nThe app itself has a LOT of permissions & login with your google account is mandatory to use the app.","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.nbu.paisa.user","https://support.google.com/googlepay/answer/10223752?hl=en&co=GENIE.Platform%3DAndroid#zippy=%2Cinfo-that-google-may-collect","https://venturebeat.com/2020/11/20/probeat-google-will-eventually-sell-ads-against-your-financial-data/","https://www.bleepingcomputer.com/news/google/google-payment-privacy-settings-hidden-behind-special-url/","https://beta.pithus.org/report/36b22c539b5f25c27a7699516c906351a25ba2daa2894eed08ae22f7a2a72c0e"],"removal":"delete","type":"google"},{"id":"com.google.android.apps.nexuslauncher","label":"Pixel Launcher","description":"Used to be called Nexus Launcher (back when Google phones were called Nexus, not Pixel).","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.nexuslauncher"],"removal":"caution","warning":"Your system will break if there is no other launcher. So, download another launcher before removing it.","suggestions":"launchers","type":"google"},{"id":"com.google.android.apps.paidtasks","label":"Google Opinion Rewards","description":"Answer quick surveys and earn Rewards. If you insist on keeping it for earning free credits, just give them all the wrong answers :)","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.paidtasks"],"removal":"delete","type":"google"},{"id":"com.google.android.apps.pdfviewer","label":"Google PDF Viewer","description":"Discontinued. PDF viewers are very sensitive applications. You should always use an app that is uptodate.","removal":"replace","suggestions":"ebook_readers","type":"google"},{"id":"com.google.android.apps.photos","label":"Google Photos","description":"Allows Google to scan and catalog all your photos so that it knows you and your relations more than you do.","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.photos"],"removal":"replace","suggestions":"gallery","type":"google"},{"id":"com.google.android.apps.photos.scanner","label":"PhotoScan by Google Photos","description":"Companion app to Google Photos for reviving old pictures.","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.photos.scanner"],"removal":"delete","type":"google"},{"id":"com.google.android.apps.plus","label":"Google+","description":"Discontinued.","removal":"delete","type":"google"},{"id":"com.google.android.apps.podcasts","label":"Google Podcasts","description":"Lets you explore, subscribe and play podcasts.","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.podcasts"],"removal":"replace","suggestions":"podcasts","type":"google"},{"id":"com.google.android.apps.privacy.wildlife","description":"VPN by Google One. Discontinued. Succeeded by VPN by Google and Google Fi VPN.","removal":"delete","type":"google"},{"id":"com.google.android.apps.recorder","label":"Recorder","description":"Audio recorder from Google LLC.","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.recorder"],"removal":"replace","suggestions":"audio_recorders","type":"google"},{"id":"com.google.android.apps.restore","label":"Data Restore Tool","description":"The backup restore wizard used for pulling Android system backups from your Google account.\nRuns on boot.\nYou only need this if you factory restore, in which case it’s automatically re-enabled for you.","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.restore"],"removal":"caution","type":"google"},{"id":"com.google.android.apps.safetyhub","label":"Personal Safety","description":"A Pixel app that helps you prepare and react in an emergency by quickly calling emergency services (e.g if your phone detects that you've been in a car crash, it can call for help automatically).\nThis app has obviously a lot of dangerous permissions due to its operation.","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.safetyhub","https://beta.pithus.org/report/e207f7d0f59d9df268154b90fc10cd861d0483465e30bbac8f68a7b12340c67f"],"removal":"delete","type":"google"},{"id":"com.google.android.apps.santatracker","label":"Google Santa Tracker","description":"Discontinued.","removal":"delete","type":"google"},{"id":"com.google.android.apps.scone","description":"Automatically switches basebands between LTE and 5G on demand","removal":"delete","type":"google"},{"id":"com.google.android.apps.searchlite","label":"Google Go","description":"The Google search app made for low-RAM devices.","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.searchlite"],"removal":"delete","type":"google"},{"id":"com.google.android.apps.security.securityhub","description":"Checks security your phone(you can find it in settings). Not very useful.\nhttps://play.google.com/store/apps/details?id=com.google.android.apps.security.securityhub","removal":"delete","type":"google"},{"id":"com.google.android.apps.setupwizard.searchselector","label":"Search Engine Selector","description":"The search selection screen in the setupwizard you see on new/factory reset phones. Runs on boot, but doesn't seem to run in the background beyond that.","removal":"delete","type":"google"},{"id":"com.google.android.apps.subscriptions.red","label":"Google One","description":"Lets you manage your Google cloud storage.\nOccasionally runs in the background.","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.subscriptions.red"],"removal":"delete","suggestions":"cloud_services","type":"google"},{"id":"com.google.android.apps.tachyon","label":"Google Meet","description":"Formerly Google Duo. Lets you create, schedule or join an online meeting.","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.tachyon"],"removal":"replace","suggestions":"meeting_apps","type":"google"},{"id":"com.google.android.apps.tasks","label":"Google Tasks","description":"Manage, capture, and edit your tasks with synchronisation.","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.tasks"],"removal":"replace","suggestions":"task_managers","type":"google"},{"id":"com.google.android.apps.translate","label":"Google Translate","description":"Google Translate mobile client.","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.translate"],"removal":"replace","suggestions":"translators","type":"google"},{"id":"com.google.android.apps.travel.onthego","label":"Google Trip","description":"Discontinued.","removal":"delete","type":"google"},{"id":"com.google.android.apps.turbo","label":"Device Health Services","description":"Discontinued.","removal":"caution","warning":"Breaks battery settings.","suggestions":"battery_managers","type":"google"},{"id":"com.google.android.apps.tycho","description":"Google Fi Wireless\nhttps://play.google.com/store/apps/details?id=com.google.android.apps.tycho","removal":"delete","type":"google"},{"id":"com.google.android.apps.uploader","label":"Picasa Uploader","description":"Discontinued.","removal":"delete","type":"google"},{"id":"com.google.android.apps.vega","label":"Google My Business","description":"Discontinued.","removal":"delete","type":"google"},{"id":"com.google.android.apps.walletnfcrel","label":"Google Wallet","description":"Formerly Google Pay. Use cash, protect your privacy.","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.walletnfcrel"],"removal":"delete","type":"google"},{"id":"com.google.android.apps.wallpaper","label":"Wallpapers","description":"Wallpaper app from Google. Lets you set wallpaper from various sources including Google Earth collection","removal":"delete","type":"google"},{"id":"com.google.android.apps.weather","description":"The new Weather app by Google. If removed, \"At a Glance\" and the lock screen will still redirect to the Google app (like before).\nhttps://play.google.com/store/apps/details?id=com.google.android.apps.weather","removal":"delete","type":"google"},{"id":"com.google.android.apps.wellbeing","label":"Digital Wellbeing","description":"Lets you track device and app usage and set usage limits.","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.wellbeing"],"removal":"caution","warning":"It is a hard dependency for the settings app on Android 12+ on Pixel phones. After uninstalling, there will be an empty entry on Settings. Rebooting should make it disappear.","type":"google"},{"id":"com.google.android.apps.work.oobconfig","label":"Device Setup","description":"Sets up device to be managed by EMM (Enterprise Mobility Management), which \"allows organizations to securely enable employee use of mobile devices\".\nMight also be what does the actual management on your device, if you set it up as a work device.\nOnly seems to run on boot(not in the background after boot) if you haven't set up your device as a work device.\nI tried to disable it through UAD, but nothing happens? Seems immune to disabling?\nhttps://bayton.org/2020/11/google-announce-big-changes-to-zero-touch/\nhttps://bayton.org/docs/enterprise-mobility/android/what-is-android-zero-touch-enrolment/\nContains 4 services: GcmJobService, GservicesChangedObserverService, AppMeasurementService and FirebaseInstanceIdService.\nGCM(Google Cloud Messaging) was the backend for Android's push messaging system 2012-2019, after which it was replaced by FCM(Firebase Cloud Messaging). I assume the GCM/Firebase connection is for Push notification functionality.\nThe MANAGE_CARRIER_OEM_UNLOCK_STATE permission hints at doing something with carrier locks?\nNeeds Google Play Services to function?","removal":"caution","type":"google"},{"id":"com.google.android.apps.youtube.creator","label":"YouTube Studio","description":"Intended for YouTube creators to track their channel activities and analytics.","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.youtube.creator"],"removal":"delete","type":"google"},{"id":"com.google.android.apps.youtube.gaming","label":"YouTube Gaming","description":"Discontinued in March 2019, features integrated in main YouTube app.","removal":"delete","type":"google"},{"id":"com.google.android.apps.youtube.kids","label":"YouTube Kids","description":"YouTube for kids.","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.youtube.kids","https://qz.com/youtube-has-become-the-worlds-nanny-1850047610"],"removal":"replace","suggestions":"streaming_apps","type":"google"},{"id":"com.google.android.apps.youtube.mango","label":"YouTube Go","description":"Lite version of the YouTube app. Discontinued in August 2022. Still present in Google Play Store for some reason.","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.youtube.kids","https://support.google.com/youtube/thread/162222567/youtube-go-is-going-away-in-august-of-this-year"],"removal":"replace","suggestions":"streaming_apps","type":"google"},{"id":"com.google.android.apps.youtube.music","label":"YouTube Music","description":"YouTube Music client for Android","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.youtube.music"],"removal":"replace","suggestions":"music_apps","type":"google"},{"id":"com.google.android.apps.youtube.vr","label":"YouTube VR","description":"Watch YouTube in VR.","web":["https://play.google.com/store/apps/details?id=com.google.android.apps.youtube.vr"],"removal":"delete","type":"google"},{"id":"com.google.android.as","label":"Android System Intelligence","description":"Previously, Device Personalization Services. Runs in the background.\n\"Enables intelligent features across Android\", like: Live Caption, Screen Attention, Improved Copy-Paste, App Predictions in the launcher, Notification Smart Actions, Smart Text Selection and Linkifying text in apps.\nAlso known as device learning, e.g., enhanced keyboard auto suggestions, keeps the screen on when looking at it (with help of the camera), smart replies.","web":["https://play.google.com/store/apps/details?id=com.google.android.as","https://milaq.net/android-bloatware"],"removal":"delete","type":"google"},{"id":"com.google.android.as.oss","description":"Private Compute Services. On-device behavior analysis\nEnables live caption, music recognition and smart replies.\nSeems to be a dependency of System Intelligence.\nhttps://play.google.com/store/apps/details?id=com.google.android.as.oss\nhttps://milaq.net/android-bloatware","removal":"delete","type":"google"},{"id":"com.google.android.backup","label":"Google Backup Transport","description":"Allows Android apps to back up their data on Google servers (on Android 4.2).","removal":"replace","suggestions":"backup_apps","type":"google"},{"id":"com.google.android.backuptransport","label":"Google Backup Transport","description":"Allows Android apps to back up their data on Google servers.","removal":"replace","suggestions":"backup_apps","type":"google"},{"id":"com.google.android.calculator","label":"Calculator","description":"Calculator app from Google LLC. What sort of calculator collects personal info?","web":["https://play.google.com/store/apps/details?id=com.google.android.calculator"],"removal":"replace","suggestions":"calculators","type":"google"},{"id":"com.google.android.calendar","label":"Google Calendar","description":"Calendar app from Google LLC.","web":["https://play.google.com/store/apps/details?id=com.google.android.calendar"],"removal":"replace","suggestions":"calendars","type":"google"},{"id":"com.google.android.cellbroadcastreceiver","label":"Wireless emergency alerts","dependencies":["com.google.android.cellbroadcastservice"],"description":"Cell broadcast is designed to deliver messages to multiple users in an area.\nThis is notably used by ISP to send Emergency/Government alerts.\nRuns at boot and is also triggered after exiting airplane mode.","web":["https://en.wikipedia.org/wiki/Cell_Broadcast","https://www.androidcentral.com/amber-alerts-and-android-what-you-need-know","https://android.googlesource.com/platform/packages/apps/CellBroadcastReceiver/+/refs/heads/master/src/com/android/cellbroadcastreceiver"],"removal":"caution","type":"google"},{"id":"com.google.android.cellbroadcastservice","label":"Cell Broadcast Service","required_by":["com.google.android.cellbroadcastreceiver"],"description":"Cell broadcast is designed to deliver messages to multiple users in an area.\nThis is notably used by ISP to send Emergency/Government alerts.","web":["https://en.wikipedia.org/wiki/Cell_Broadcast","https://www.androidcentral.com/amber-alerts-and-android-what-you-need-know"],"removal":"caution","type":"google"},{"id":"com.google.android.configupdater","label":"ConfigUpdater","description":"Intents used to provide unbundled updates of system data. All require the UPDATE_CONFIG permission. Updates:\n- system wide certificate pins for TLS connections.\n- System wide Intent firewall.\n- List of permium SMS short codes.\n- List of carrier provisioning URLs.\n- Set of trusted logs used for Certificate Transparency support for TLS connections language detection model file\n- Smart selection model file\n- Conversation actions model file\n- Network watchlist config file\n- Intent action indicating that the updated carrier id config is available\n- The emergency number database into the devices\n- An integer to indicate the numeric version of the new data -- devices should only install if the update version is newer than the current one\n- Hash of the database, which is encoded by base-16 SHA512.","web":["https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/os/ConfigUpdate.java"],"removal":"caution","type":"google"},{"id":"com.google.android.contacts","label":"Contacts","description":"Contacts by Google LLC.\nOccasionally runs in the background.","web":["https://play.google.com/store/apps/details?id=com.google.android.contacts"],"removal":"replace","suggestions":"contacts","type":"google"},{"id":"com.google.android.deskclock","label":"Clock","description":"Clock by Google LLC.","removal":"replace","warning":"On some phones, removing this makes it so alarms and notifications only vibrate and don't make any sound (via any installed app), and makes the 'Alarm' section unavailable in 'Settings > Sound & Vibration'","suggestions":"clocks","type":"google"},{"id":"com.google.android.dialer","label":"Phone","description":"Formerly Google Dialer.\nDefault dialer on some phones.\nGoogle Analytics are embedded in the app, assume everything is datamined.","web":["https://play.google.com/store/apps/details?id=com.google.android.dialer","https://www.virustotal.com/gui/file/a978d90f27d5947dca33ed59b73bd8efbac67253f9ef7a343beb9197c8913d1a/details"],"removal":"replace","suggestions":"dialers","type":"google"},{"id":"com.google.android.feedback","label":"Market Feedback Agent","description":"This is the package that sends crash-report feedback to the Play Store? The crash pop-up still happens with this disabled.\nDoesn't seem to run on its own.\nHas permission to access system logs and package usage stats. Only connects to 4 Google domains. App developers likely have to go through the Play Store to access any sent data.","web":["https://beta.pithus.org/report/7041823ff880c207ed2ddacdc92e5ed803b1eb105e4483696d2152bea44903aa"],"removal":"delete","type":"google"},{"id":"com.google.android.gm","label":"Gmail","description":"Gmail client from Google LLC. It also allows adding other E-Mail accounts.","web":["https://play.google.com/store/apps/details?id=com.google.android.gm"],"removal":"replace","suggestions":"email_clients","type":"google"},{"id":"com.google.android.gm.lite","label":"Gmail Go","description":"Gmail client by Google LLC for low-end devices.","removal":"replace","suggestions":"email_clients","type":"google"},{"id":"com.google.android.gms","label":"Google Play services","description":"GMS = Google Mobile Services. It is a layer that sits on top of the OS and provides a lot of proprietary Google APIs, giving apps access to various Google Services, such as: \"fused\"-location (internet and GPS chip), QR Code scanner, 2FA, G-Drive storage, Firebase API, Cloud Messaging, etc.\nIf you remove it, all the apps relying on it will either:\n- detect the lack of Play-Services and refuse to run\n- detect the lack of Play-Services but allow you to run (improperly) by dismissing an annoying popup.\nDisabling this package will improve battery life a lot.","web":["https://play.google.com/store/apps/details?id=com.google.android.gms"],"removal":"caution","warning":"Removing Google Play Services can cause bootloops. If you use “Find My Device”, you will need to remove it from the \"Device admin apps\" settings panel to be able to remove this package.","type":"google"},{"id":"com.google.android.gms.location.history","label":"Google Location History","description":"This app has nothing in the code. Only png logo google and name.","removal":"delete","type":"google"},{"id":"com.google.android.gms.policy_sidecar_aps","label":"com.google.android.gms.policy_sidecar_aps","description":"Not sure what purpose it has, but it gets some network and phone data and connects to some Google domains, but never on its own; it has no permissions and never runs on its own, it likely exists as a helper package for other Google services.\nDoesn't seem to exist in newer versions of Android; it's not in Android 11, but it is in 9.\nNeeds a Google Account and Google Play Services to work.\nGiven its name it could be related to Android auto?\nSeems safe to remove, noticed no breakage (didn't test Android Auto though).","web":["https://beta.pithus.org/report/60835b97f38d9e64d4f554a73dab71c892153486a8e0fd81461c3d85359d9fae"],"removal":"delete","type":"google"},{"id":"com.google.android.gms.supervision","description":"Family Link parental controls\nIt has something to Family Link parental controls.\nIntroduced in android 13.\nhttps://play.google.com/store/apps/details?id=com.google.android.gms.supervision","removal":"delete","type":"google"},{"id":"com.google.android.googlequicksearchbox","label":"Google","description":"Formerly, Google Search Box.\nRuns in the background.\nPointless. If you need a shortcut to Google on your homescreen just use a web-browser shortcut. Does also remove the Google Sound Search widget, but you can get that functionality from an app like Shazam, that additionally doesn't run in the background constantly like this package does.\nThis app also powers the Google Discover page (the news 'widget' when you go to the left of the home screen) and the app symbol called 'Google'.","web":["https://play.google.com/store/apps/details?id=com.google.android.googlequicksearchbox"],"removal":"caution","warning":"Disabling this package also breaks the Ok Google Search functionality as well as Google Lens.","type":"google"},{"id":"com.google.android.gsf","label":"Google Services Framework","description":"Supports the Play Services application in application updates, user authentication, location services, user searches & more.\nSame recommendation as com.google.android.gms except I've never seen a bootloop because of deleting this package.","web":["https://android.stackexchange.com/questions/216176/what-is-the-exact-functionality-of-google-play-services-google-services-framew","https://stackoverflow.com/questions/37337448/what-is-the-difference-between-google-service-frameworkgsfgoogle-mobile-servi"],"removal":"caution","type":"google"},{"id":"com.google.android.gsf.login","label":"Google Account Manager","description":"Support for managing Google accounts.","removal":"caution","warning":"Safe to remove if you don't use a Google account.","type":"google"},{"id":"com.google.android.health.connect.backuprestore","description":"Health Connect for Android backup/restore.","removal":"replace","type":"google"},{"id":"com.google.android.healthconnect.controller","description":"Health Connect by Android gives you a simple way to share data between your health, fitness, and wellbeing apps without compromising on privacy.\nOnce you've downloaded Health Connect, you can access it through your settings by going to Settings > Apps > Health Connect, or from your Quick Settings menu.\nhttps://play.google.com/store/apps/details?id=com.google.android.apps.healthdata","removal":"replace","type":"google"},{"id":"com.google.android.ims","label":"Carrier Services","description":"Runs in the background.\nPlay store description claims power savings in addition to the features, but I don't see how that could be the case.\nIMS(Ip Multimedia Subsystem) is an open industry standard for voice and multimedia communications over packet-based IP networks (VoLTE/VoIP/Wifi calling).","web":["https://play.google.com/store/apps/details?id=com.google.android.ims"],"removal":"delete","type":"google"},{"id":"com.google.android.inputmethod.japanese","label":"Google Japanese Input","description":"(Discontinued) Google Keyboard + Japanese characters.","removal":"replace","suggestions":"keyboards","type":"google"},{"id":"com.google.android.inputmethod.korean","label":"Google Korean Input","description":"(Discontinued) Google Keyboard + Korean characters.","removal":"replace","suggestions":"keyboards","type":"google"},{"id":"com.google.android.inputmethod.latin","label":"Gboard","description":"Sometimes the only keyboard app on a phone.","web":["https://play.google.com/store/apps/details?id=com.google.android.inputmethod.latin"],"removal":"replace","warning":"Make sure you have another installed before you disable.","suggestions":"keyboards","type":"google"},{"id":"com.google.android.inputmethod.pinyin","label":"Google Pinyin Input","description":"(Discontinued) Google Keyboard + Pinyin (Chinese) characters","removal":"replace","suggestions":"keyboards","type":"google"},{"id":"com.google.android.instantapps.supervisor","label":"Instant Apps","description":"Lets you try new games directly on Google Play.","web":["https://play.google.com/store/apps/details?id=com.google.android.instantapps.supervisor","https://www.zdnet.com/article/googles-instant-apps-goes-live-now-you-can-try-android-apps-before-installing-them/"],"removal":"delete","type":"google"},{"id":"com.google.android.keep","label":"Google Keep","description":"Note taking app from Google.","web":["https://play.google.com/store/apps/details?id=com.google.android.keep"],"removal":"replace","suggestions":"note_taking_apps","type":"google"},{"id":"com.google.android.launcher","label":"Google Now Launcher","description":"Launcher app from google.","web":["https://play.google.com/store/apps/details?id=com.google.android.launcher"],"removal":"replace","warning":"Make sure you have another installed before you disable.","suggestions":"launchers","type":"google"},{"id":"com.google.android.location","label":"UnifiedNlp","description":"Handles location services on older devices. On newer ones Google location services is part of Google Play Services and Android location service is provided by com.android.location.fused or com.android.location.","removal":"replace","type":"google"},{"id":"com.google.android.markup","label":"Markup","description":"Google Markup app made for modifying pictures, shipped by default on every Pie+ device.","removal":"delete","type":"google"},{"id":"com.google.android.marvin.talkback","label":"Android Accessibility Suite","description":"Helps blind and vision-impaired users.","web":["https://play.google.com/store/apps/details?id=com.google.android.marvin.talkback"],"removal":"caution","warning":"Removal causes com.motorola.dynamicvolume to not display the respective volume for individual apps when the volume button pressed. Seems to revert to an older (maybe testing) version of the dynamicvolume package?","type":"google"},{"id":"com.google.android.music","label":"Google Play Music","description":"Discontinued and replaced by com.google.android.apps.youtube.music","removal":"replace","suggestions":"music_apps","type":"google"},{"id":"com.google.android.onetimeinitializer","label":"Google One Time Init","description":"Provides first time setup, safe to remove.","removal":"delete","type":"google"},{"id":"com.google.android.overlay.gmsconfig","description":"Useless configurations about webview, wifi and bluetooth to scan for better location. Everything works without it.\nWARNING: causes the Galaxy App to force-close after a few seconds on a Samsung N960F running Android Q.","removal":"replace","type":"google"},{"id":"com.google.android.overlay.gmsconfig.asi","description":"it's another component of\nAndroid System Intelligence (previously Device Personalization Services)","removal":"delete","type":"google"},{"id":"com.google.android.overlay.gmsconfig.common","label":"com.google.android.overlay.gmsconfig.common","description":"If you delete this package, you won't be able to log in to your Google account on a Google app. But if you don't need to log in to a Google account, you can safely remove this.","removal":"caution","type":"google"},{"id":"com.google.android.overlay.gmsconfig.comms","label":"com.google.android.overlay.gmsconfig.comms","description":"Useless configurations for Google's Phone, Messages, Contacts apps, everything works without it.","removal":"delete","type":"google"},{"id":"com.google.android.overlay.gmsconfig.geotz","description":"Provides Geolocation Time Zone detection.\nand it's used by (com.google.android.overlay.gmsconfig.common)?\nNo effects after remove.","removal":"caution","type":"google"},{"id":"com.google.android.overlay.gmsconfig.gsa","label":"com.google.android.overlay.gmsconfig.gsa","description":"configures default assistant? And have config to allow disabling assist disclosure?\nI think it doesn't affect anything.\nSafe to remove if you removed (com.google.android.googlequicksearchbox).","removal":"caution","type":"google"},{"id":"com.google.android.overlay.gmsconfig.personalsafety","description":"Not needed for (com.google.android.apps.safetyhub).","removal":"caution","type":"google"},{"id":"com.google.android.overlay.gmsconfig.photos","label":"com.google.android.overlay.gmsconfig.photos","description":"Overlay to Gallery (com.google.android.apps.photos). Useless.","removal":"delete","type":"google"},{"id":"com.google.android.overlay.gmsconfig.searchlauncherqs","description":"Useless gmsconfig. it's config default launcher but not needed.","removal":"delete","type":"google"},{"id":"com.google.android.overlay.gmsconfig.searchselector","description":"Not needed for (com.google.android.apps.setupwizard.searchselector).","removal":"delete","type":"google"},{"id":"com.google.android.overlay.gmsconfig.ww","description":"useless unused overlay gmsconfig to family link.","removal":"delete","type":"google"},{"id":"com.google.android.overlay.modules.cellbroadcastreceiver","label":"com.google.android.overlay.modules.cellbroadcastreceiver","dependencies":["com.google.android.cellbroadcastreceiver"],"description":"Overlay (theme/notification) module for com.google.android.cellbroadcastreceiver","web":["https://docs.samsungknox.com/CCMode/G973F_LTE_R.pdf"],"removal":"caution","type":"google"},{"id":"com.google.android.overlay.modules.documentsui","label":"com.google.android.overlay.modules.documentsui","description":"In code only found: `is_launcher_enabled` is set to false.\nIt's only made to hide app icon (com.google.android.documentsui)\nNo effects after removal (the launcher icon will be not back), it's useless.","web":["https://docs.samsungknox.com/CCMode/G973F_LTE_R.pdf"],"removal":"delete","type":"google"},{"id":"com.google.android.partnersetup","label":"Google Partner Setup","description":"Occasionally runs in the background. Based on an unclear explanation online: Enables applications to interact with your Google account/apps, for example: adding a Google Calendar event from a To-Do app.\nProbably safe to disable; Haven't noticed any consequences of disabling from weeks of use.","removal":"caution","type":"google"},{"id":"com.google.android.pixel.setupwizard","label":"Pixel Setup","description":"It's the basic configuration setup guides you through the basics of setting up Google features on your device. The package is only present on Pixel phones.","removal":"caution","warning":"Removing the package breaks the Google Play System update page found on \"Security & privacy\".","type":"google"},{"id":"com.google.android.play.games","label":"Google Play Games","description":"Google Play Games.","web":["https://play.google.com/store/apps/details?id=com.google.android.play.games"],"removal":"delete","type":"google"},{"id":"com.google.android.projection.gearhead","label":"Android Auto","description":"Smart driving companion.","web":["https://play.google.com/store/apps/details?id=com.google.android.projection.gearhead"],"removal":"delete","type":"google"},{"id":"com.google.android.setupwizard","label":"Android Setup","description":"The factory reset device basic configuration setup guides you through the basics of setting up your device.","web":["https://en.wikipedia.org/wiki/Mobile_identity_management"],"removal":"caution","warning":"Oddly enough, disabling/uninstalling this package will break mobile identity management which could be used by apps (for example your bank) to authenticate you.","type":"google"},{"id":"com.google.android.setupwizard.a_overlay","label":"com.google.android.setupwizard.a_overlay","description":"Overlay for setupwizard?","removal":"delete","type":"google"},{"id":"com.google.android.soundpicker","label":"Google Sounds","description":"Removable if you already have another media select service.","removal":"replace","type":"google"},{"id":"com.google.android.street","label":"Google Street View","description":"(Discontinued) Google Street View","removal":"delete","type":"google"},{"id":"com.google.android.syncadapters.bookmarks","label":"Google Bookmarks Sync","description":"Synchronisation for Google Chrome bookmarks","removal":"replace","type":"google"},{"id":"com.google.android.syncadapters.calendar","label":"Google Calendar Sync","description":"Synchronisation for Google Calendar.","removal":"replace","type":"google"},{"id":"com.google.android.syncadapters.contacts","label":"Google Contacts Sync","description":"Synchronisation for Google Contacts.","removal":"replace","type":"google"},{"id":"com.google.android.tag","label":"Tags","description":"Support for NFC tags interactions (5 permissions : Contacts/Phone On by default).\nNFC Tags are for instance used in bus to let you validate your transport card with your phone\nOther example: https://en.wikipedia.org/wiki/TecTile\nYou will still be able to connect to an NFC device (e.g a speaker) if removed.","removal":"delete","type":"google"},{"id":"com.google.android.talk","label":"Hangouts","description":"(Discontinued) Google Hangouts","removal":"replace","type":"google"},{"id":"com.google.android.tts","label":"Speech Recognition and Synthesis","description":"Default Text To Speech (TTS) engine on most of Android devices. It enables apps to convert text into voice.\nNote: many apps like navigation and health/sport apps rely on a TTS engine to provide speech services.","web":["https://play.google.com/store/apps/details?id=com.google.android.tts","https://beta.pithus.org/report/f0a2303e1c1bd049bf1cdc5a3454dfe19b2aaf26008662c7a307aaec2538558b"],"removal":"replace","suggestions":"tts","type":"google"},{"id":"com.google.android.turboadapter","description":"Device Health Services Adapter\nAnother app for Device Health Services(discontinued)","removal":"delete","type":"google"},{"id":"com.google.android.tv.remote","label":"Android TV Remote Control","description":"(Discontinued) Android TV remote control","removal":"delete","type":"google"},{"id":"com.google.android.videoeditor","label":"Movie Studio","description":"(Discontinued) Google Movie Studio","removal":"replace","type":"google"},{"id":"com.google.android.videos","label":"Google TV","description":"Previously Google Play Movies & TV.","web":["https://play.google.com/store/apps/details?id=com.google.android.videos"],"removal":"delete","type":"google"},{"id":"com.google.android.voicesearch","label":"Voice Search","description":"(Discontinued) Google Voice Search (Speech-To-Text)","removal":"delete","type":"google"},{"id":"com.google.android.vr.home","label":"Daydream","description":"VR stuff.","removal":"delete","type":"google"},{"id":"com.google.android.vr.inputmethod","label":"Daydream Keyboard","description":"Daydream virtual keyboard, VR stuff.","removal":"delete","type":"google"},{"id":"com.google.android.wearable.app","label":"Wear OS","description":"Wear OS Smartwatch","web":["https://play.google.com/store/apps/details?id=com.google.android.wearable.app"],"removal":"delete","type":"google"},{"id":"com.google.android.webview","label":"Android System WebView","description":"Allows Android apps to display content from the web directly inside the app. Based on Chrome.","web":["https://play.google.com/store/apps/details?id=com.google.android.webview"],"removal":"replace","warning":"Installing a third-party webview requires root.","suggestions":"webviews","type":"google"},{"id":"com.google.android.wifi.dialog","description":"Wi-Fi dialog. App to launch user dialogs requested by the Wi-Fi service is stored here. Contains the Activity the dialogs are launched from.\nhttps://source.android.com/docs/core/ota/modular-system/wifi\nhttps://android.googlesource.com/platform/packages/apps/Settings/+/refs/heads/main/src/com/android/settings/wifi/WifiDialog.java\nhttps://android.googlesource.com/platform/packages/apps/Settings/+/refs/heads/main/src/com/android/settings/wifi/WifiDialog2.kt","removal":"caution","type":"google"},{"id":"com.google.android.wifi.resources","description":"Wi-Fi Service Resources. Overlay APK manifest is stored here. Extracts Wi-Fi configs.\nhttps://source.android.com/docs/core/ota/modular-system/wifi","removal":"unsafe","type":"google"},{"id":"com.google.android.youtube","label":"YouTube","description":"The YouTube app","web":["https://play.google.com/store/apps/details?id=com.google.android.youtube"],"removal":"replace","suggestions":"streaming_apps","type":"google"},{"id":"com.google.ar.core","label":"Google Play Services for AR","description":"Augmented Reality stuff","web":["https://play.google.com/store/apps/details?id=com.google.ar.core","https://beta.pithus.org/report/99ea324529f950fe351d22724f8b894cce19c16607fcc9c2855bc906b1f8e644"],"removal":"delete","warning":"Disabling it can mess with apps that use it, like Pokemon GO.","type":"google"},{"id":"com.google.ar.lens","label":"Google Lens","description":"Google Lens (for AR too)","web":["https://play.google.com/store/apps/details?id=com.google.ar.lens"],"removal":"delete","type":"google"},{"id":"com.google.audio.hearing.visualization.accessibility.scribe","label":"Live Transcribe & Notification","description":"Provides push notifications for critical sounds around you. This feature can be helpful for people with hearing loss. Works offline","web":["https://play.google.com/store/apps/details?id=com.google.audio.hearing.visualization.accessibility.scribe","https://blog.google/products/android/new-sound-notifications-on-android/"],"removal":"delete","type":"google"},{"id":"com.google.chromeremotedesktop","label":"Chrome Remote Desktop","description":"Lets you access your computers from your Android device.","web":["https://play.google.com/store/apps/details?id=com.google.chromeremotedesktop"],"removal":"delete","type":"google"},{"id":"com.google.earth","label":"Google Earth","description":"The Google Earth app","web":["https://play.google.com/store/apps/details?id=com.google.earth"],"removal":"delete","type":"google"},{"id":"com.google.mainline.telemetry","description":"Contains data on which versions of modules are installed. Google Play uses this data to determine if updates are available for the modules, and to show which security patch is installed.\nThis module doesn’t contain active code and has no functionality on its own.\nAnyway I wont trust it when adservices are also in mainline.\nhttps://www.xda-developers.com/android-project-mainline-modules-explanation/\nhttps://gitlab.com/W1nst0n/universal-android-debloater/-/issues/27#note_410012436","removal":"replace","type":"google"},{"id":"com.google.marvin.talkback","label":"Android Accessibility Suite","description":"Helps blind and vision-impaired users. Discontinued and replaced by com.google.android.marvin.talkback","removal":"delete","type":"google"},{"id":"com.google.samples.apps.cardboarddemo","label":"Cardboard","description":"Google Cardboard (VR stuff)","web":["https://play.google.com/store/apps/details?id=com.google.samples.apps.cardboarddemo"],"removal":"delete","type":"google"},{"id":"com.google.tango.measure","label":"Measure","description":"(Discontinued) Turn your phone into a tape measure.","removal":"delete","type":"google"},{"id":"com.google.vr.cyclops","label":"Cardboard Camera","description":"(Discontinued) Take photos you can experience in virtual reality—all on your smartphone (VR stuff).","removal":"delete","type":"google"},{"id":"com.google.vr.expeditions","label":"Expeditions","description":"See anything, go anywhere (VR stuff).","removal":"delete","type":"google"},{"id":"com.google.vr.vrcore","label":"Google VR Services","description":"Provides virtual reality functionality for Daydream and Cardboard apps.","web":["https://play.google.com/store/apps/details?id=com.google.vr.vrcore"],"removal":"delete","type":"google"},{"id":"com.google.zxing.client.android","label":"Barcode Scanner","description":"Discontinued. This is only an example app for the ZXing library.","web":["https://play.google.com/store/apps/details?id=com.google.zxing.client.android"],"removal":"replace","suggestions":"barcode_scanners","type":"google"},{"id":"air.com.playtika.slotomania","label":"Slotomania™ Slots Casino Games","description":"Preinstalled game on some Samsung phones.\nExodus report: 31 permissions, 13 trackers","web":["https://play.google.com/store/apps/details?id=air.com.playtika.slotomania","https://reports.exodus-privacy.eu.org/reports/air.com.playtika.slotomania/latest/"],"removal":"delete","type":"misc"},{"id":"cci.usage","label":"My Consumer Cellular","description":"AKA My CC. Lets you manage your Consumer Cellular account, track your usage, pay your bill.\nConsumer Cellular is an American postpaid mobile virtual network operator","web":["https://play.google.com/store/apps/details?id=cci.usage","https://en.wikipedia.org/wiki/Consumer_Cellular"],"removal":"delete","type":"misc"},{"id":"co.sitic.pp","label":"SYSdll","description":"Designed to remotely lock the phone (by sending a simple SMS) in case you don't pay your bill \nThis app was pre-installed on phone not served by that carrier (América Móvil) from South America.\nNormally you should not have this app anymore because it was removed by Nokia during an Android 10 update.\n","web":["https://www.reddit.com/r/Android/comments/fde3l6/3rd_party_telemetry_found_in_nokia_smartphones/fjh4zbx/?context=3"],"removal":"delete","type":"misc"},{"id":"com.UCMobile.intl","label":"UC Browser","description":"Insecure chinese web browser from Alibaba, which is well-known for its privacy & security issues.","web":["https://play.google.com/store/apps/details?id=com.UCMobile.intl","https://citizenlab.ca/2015/05/a-chatty-squirrel-privacy-and-security-issues-with-uc-browser/","https://www.andmp.com/2019/05/advisory-unpatched-url-address-bar-vulnerability-in-latest-versions-of-UC-browers.html","https://www.zscaler.com/blogs/security-research/uc-browser-app-abuses-may-have-exposed-500-million-users"],"removal":"replace","suggestions":"browsers","type":"misc"},{"id":"com.aaa.android.discounts","label":"AAA","description":"AAA = American Automobile Association\nKind of GPS that helps you find Point of interest (POI) like hotels, restaurants, and car repair facilities from the AAA databases.\nNOTE : You’ll have to sign up for an AAA membership to enjoy all of the features and functionality of the Android app.","web":["https://play.google.com/store/apps/details?id=com.aaa.android.discounts"],"removal":"delete","type":"misc"},{"id":"com.aaa.android.discounts.vpl","label":"AAA","description":"AAA = American Automobile Association\nKind of GPS that helps you find Point of interest (POI) like hotels, restaurants, and car repair facilities from the AAA databases.\nNOTE : You’ll have to sign up for an AAA membership to enjoy all of the features and functionality of the Android app.","removal":"delete","type":"misc"},{"id":"com.adups.fota","label":"Wireless Update","description":"(AKA System Update) FOTA = Firmware Over-the-air. Has a history of spying its users. If the installed version is below 5.4.x, it must be uninstalled.","web":["https://www.malwarebytes.com/blog/news/2017/12/mobile-menace-monday-upping-the-ante-on-adups-fwupgradeprovider","https://www.cvedetails.com/vulnerability-list/vendor_id-16034/product_id-35606/year-2017/Adups-Adups-Fota.html"],"removal":"caution","type":"misc"},{"id":"com.adups.fota.sysoper","label":"UpgradeSys","description":"FOTA = Firmware Over-the-air. Has a history of spying on its users.","web":["https://www.malwarebytes.com/blog/news/2017/12/mobile-menace-monday-upping-the-ante-on-adups-fwupgradeprovider","https://www.cvedetails.com/vulnerability-list/vendor_id-16034/product_id-35606/year-2017/Adups-Adups-Fota.html"],"removal":"delete","type":"misc"},{"id":"com.agui.toolbox","label":"Toolbox","description":"Contains a bunch of small utilites, most have there own APP but are only accessible from the Toolbox UI\nincluded; Noise test, Compass, Flashlight, Bubble Level, Picture Hanging, Heart rate, Measure height,\nMagnifier,Alarm, Plumb Bob, Protractor, Speedometer & a Pedometer.","removal":"delete","type":"misc"},{"id":"com.alibaba.aliexpresshd","description":"AliExpress shopping/marketplace.\nhttps://play.google.com/store/apps/details?id=com.alibaba.aliexpresshd","removal":"delete","type":"misc"},{"id":"com.amazon.aa","label":"Amazon Assistant","description":"A spyware that shows you recommended products available on Amazon and price compare as you shop across the web.","web":["https://www.gadgetguy.com.au/amazon-assistant-spies-on-you/"],"removal":"delete","type":"misc"},{"id":"com.amazon.aa.attribution","label":"Amazon Assistant Attribution","description":"A spyware again tool that allows sellers to measure the impact of media channels **off Amazon** on sales.","web":["https://www.repricerexpress.com/amazon-attribution/"],"removal":"delete","type":"misc"},{"id":"com.amazon.appmanager","label":"Mobile Device Information Provider","description":"Maybe related to Kindle","web":["https://forum.xda-developers.com/t/are-these-phones-preloaded-with-amazon-spyware.4260299/","https://www.reddit.com/r/AndroidQuestions/comments/89qy76/what_is_mobile_device_information_provider_app/"],"removal":"delete","type":"misc"},{"id":"com.amazon.avod.thirdpartyclient","label":"Amazon Prime Video","description":"VOD service from Amazon.","web":["https://play.google.com/store/apps/details?id=com.amazon.avod.thirdpartyclient","nhttps://en.wikipedia.org/wiki/Prime_Video"],"removal":"replace","suggestions":"streaming_apps","type":"misc"},{"id":"com.amazon.clouddrive.photos","label":"Amazon Photos","description":"Prime members get free prints delivery (US only) and unlimited photo storage (available in US, UK, CA, DE, FR, IT, ES and JP) for a lifetime of memories.\nNon-Prime members: 5 GB full-resolution photo and video storage.","web":["https://play.google.com/store/apps/details?id=com.amazon.clouddrive.photos"],"removal":"delete","type":"misc"},{"id":"com.amazon.fv","label":"Amazon App suite","description":"Provides access to Amazon digital content","removal":"delete","type":"misc"},{"id":"com.amazon.kindle","label":"Amazon Kindle","description":"Kindle eBook reader app","web":["https://play.google.com/store/apps/details?id=com.amazon.kindle"],"removal":"replace","suggestions":"ebook_readers","type":"misc"},{"id":"com.amazon.mShop.android","label":"Amazon Shopping","description":"Shopping app from Amazon","removal":"delete","type":"misc"},{"id":"com.amazon.mShop.android.shopping","label":"Amazon Shopping","description":"Shopping app from Amazon\nSame package as com.amazon.mShop.android.","web":["https://play.google.com/store/apps/details?id=com.amazon.mShop.android.shopping"],"removal":"delete","type":"misc"},{"id":"com.amazon.mShop.android.shopping.vpl","label":"Amazon Shopping","description":"Shopping app from Amazon\nSame package as com.amazon.mShop.android.","removal":"delete","type":"misc"},{"id":"com.amazon.mp3","label":"Amazon Music","description":"Amazon Music streaming app","web":["https://play.google.com/store/apps/details?id=com.amazon.mp3"],"removal":"replace","suggestions":"music_apps","type":"misc"},{"id":"com.amazon.venezia","label":"Amazon AppStore","description":"AppStore from Amazon","removal":"replace","suggestions":"app_stores","type":"misc"},{"id":"com.android.ld.appstore","label":"LD Gaming Appstore","description":"LDPlayer is an Android Gaming emulator for PC (https://ldplayer.net/)","removal":"delete","type":"misc"},{"id":"com.applovin.array.apphub.vincere","label":"AppHub","description":"Is known as tracker company, potentially adware!","removal":"delete","type":"misc"},{"id":"com.aspiro.tidal","label":"TIDAL Music","description":"Tidal Music streaming app","web":["https://play.google.com/store/apps/details?id=com.aspiro.tidal"],"removal":"replace","suggestions":"music_apps","type":"misc"},{"id":"com.aspiro.tidal.vpl","label":"TIDAL Music","description":"Tidal Music streaming app","removal":"replace","suggestions":"music_apps","type":"misc"},{"id":"com.audible.application","label":"Audible","description":"Stream Audible Audiobooks and Podcasts","web":["https://play.google.com/store/apps/details?id=com.audible.application","https://help.audible.com/s/article/audible-privacy-information?language=en_US"],"removal":"delete","type":"misc"},{"id":"com.bleacherreport.android.teamstream","label":"Bleacher Report","description":"All about Sports News","web":["https://play.google.com/store/apps/details?id=com.bleacherreport.android.teamstream"],"removal":"delete","type":"misc"},{"id":"com.blurb.checkout","label":"Blurb Checkout","description":"Provides book purchase and checkout for Samsung’s Story Album app (discontinued)","removal":"delete","type":"misc"},{"id":"com.booking","label":"Booking.com","description":"Book hotel or apartment, flights, rental cars, and more","web":["https://play.google.com/store/apps/details?id=com.booking","https://en.wikipedia.org/wiki/Booking.com","https://blog.usejournal.com/why-i-would-never-trust-booking-com-again-so-you-should-too-a2ab535ed915?gi=7ebe86eaa880","https://ro-che.info/articles/2017-09-17-booking-com-manipulation"],"removal":"delete","type":"misc"},{"id":"com.caf.fmradio","label":"FM Radio","description":"FM Radio app","web":["https://source.codeaurora.org/quic/la/platform/vendor/qcom-opensource/fm/tree/fmapp2/src/com/caf/fmradio"],"removal":"delete","type":"misc"},{"id":"com.cequint.ecid","label":"City ID","description":"Caller ID from Cequint.\nNever trust a company which promotes call ID/spam blocking features.\nCequint was acquired by TNS (https://tnsi.com/)","web":["https://www.cequint.com/","https://www.fiercewireless.com/wireless/t-mobile-to-launch-caller-id-service-from-cequint","https://itmunch.com/robocall-caught-sending-customers-confidential-data-without-consent/","https://www.geekwire.com/2013/earnouts-bad-cequint-execs-sue-parent-company/"],"removal":"delete","type":"misc"},{"id":"com.cnn.mobile.android.phone","label":"CNN Breaking US & World News","description":"News app from CNN","web":["https://play.google.com/store/apps/details?id=com.cnn.mobile.android.phone"],"removal":"replace","suggestions":"rss_readers","type":"misc"},{"id":"com.contextlogic.wish","label":"Wish","description":"Wish Shopping","web":["https://play.google.com/store/apps/details?id=com.contextlogic.wish"],"removal":"delete","type":"misc"},{"id":"com.cootek.smartinputv5.language.englishgb","label":"TouchPal Keyboard","description":"Keyboard app by Cootek a chinese company.\nAdware (lots and lots of ads)","web":["https://www.buzzfeednews.com/article/craigsilverman/google-banned-cootek-adware"],"removal":"replace","suggestions":"keyboards","type":"misc"},{"id":"com.cootek.smartinputv5.language.spanishus","label":"com.cootek.smartinputv5.language.spanishus","description":"TouchPal Keyboard by Cootek a chinese company.\nAdware (lots lots of ads)","web":["https://www.buzzfeednews.com/article/craigsilverman/google-banned-cootek-adware"],"removal":"replace","suggestions":"keyboards","type":"misc"},{"id":"com.crowdcare.agent.k","label":"com.crowdcare.agent.k","description":"Crowdcare is now Wysdom.AI.\nFrom their Twitter description : The easiest way for businesses to improve customer satisfaction, contain costs and generate revenue by using #AI to power customer experiences.\nWysdom.AI has joined the Microsoft Partner Network in 2018","web":["https://wysdom.ai/privacy-policy/"],"removal":"delete","type":"misc"},{"id":"com.debug.loggerui","description":"DebugLoggerUI\nSimilar to logcat, in some sense. Mostly focused on video graphics logging and network logging (includes Bluetooth). On some devices, it runs in the background (working non-cache RAM) even while not in use.\nhttps://peterelst.com/what-is-debug-logger-ui-android","removal":"delete","type":"misc"},{"id":"com.devhd.feedly","label":"Feedly","description":"Popular news aggregator application (RSS)","web":["https://play.google.com/store/apps/details?id=com.devhd.feedly","https://feedly.com/i/legal/privacy"],"removal":"replace","suggestions":"rss_readers","type":"misc"},{"id":"com.digitalturbine.toolbar","label":"Digital Turbine","description":"Adware and used by carriers to showcase their apps\nFYI: Digital Turbine is an advertising company.","removal":"delete","type":"misc"},{"id":"com.diotek.sec.lookup.dictionary","label":"Samsung Dictionary","description":"Samsung dictionary from Diotek (Korean company)","web":["https://en.diotek.com/"],"removal":"delete","type":"misc"},{"id":"com.directv.dvrscheduler","label":"DIRECTV on the Go","description":"Offical app from DIRECTV (subsidiary of AT&T)\nLets you watch Live TV, recorded shows, VODs and schedule recordings on your DVR","web":["https://play.google.com/store/apps/details?id=com.directv.dvrscheduler","https://en.wikipedia.org/wiki/DirecTV#Consumer_protection_lawsuits_and_violations"],"removal":"replace","suggestions":"streaming_apps","type":"misc"},{"id":"com.discoveryscreen","label":"Appflash","description":"Verizon Spyware","web":["https://play.google.com/store/apps/details?id=com.discoveryscreen","https://www.eff.org/deeplinks/2017/04/update-verizons-appflash-pre-installed-spyware-still-spyware"],"removal":"delete","type":"misc"},{"id":"com.disney.disneyplus","label":"Disney+","description":"Streaming app from Disney and co. (Pixar, Marvel, Star Wars, and National Geographic).","web":["https://play.google.com/store/apps/details?id=com.disney.disneyplus"],"removal":"replace","suggestions":"streaming_apps","type":"misc"},{"id":"com.dna.solitaireapp","label":"Solitaire – Classic Card Game","description":"Solitaire Game app from DNA company ?","web":["https://play.google.com/store/apps/details?id=com.dna.solitaireapp"],"removal":"delete","type":"misc"},{"id":"com.dolby.daxservice","label":"Dolby","description":"Runs in the background as part of the system. Runs even if disabled.\n\"Optimizes system audio performance\" or something like that. This is likely the backend audio service, possibly applying settings from com.oneplus.sound.tuner (\"Dolby Atmos\") to the audio processing.","removal":"caution","type":"misc"},{"id":"com.draftkings.dknativermgGP.vpl","label":"DraftKings - Daily Fantasy Sports for Cash","description":"App has been removed from the Playstore.","web":["https://en.wikipedia.org/wiki/DraftKings"],"removal":"delete","type":"misc"},{"id":"com.drivemode","label":"DraftKings - Daily Fantasy Sports for Cash","description":"App has been removed from the Playstore.\nDrivemode (https://play.google.com/store/apps/details?id=com.drivemode.android)\nSimplifies how you manage calls and messages while driving.","web":["https://en.wikipedia.org/wiki/DraftKings","https://drivemode.com/privacy-2/"],"removal":"delete","type":"misc"},{"id":"com.dsi.ant.plugins.antplus","label":"ANT+ Plugins Service","description":"ANT is a wireless protocol, similar to Bluetooth®, that is predominantly used for sport and fitness wireless connectivity. Pre-installed by the phone manufacturer, this service allows you to connect ANT+ devices to apps on your phone. ANT Wireless is a division of Garmin Canada Inc.","web":["https://play.google.com/store/apps/details?id=com.dsi.ant.plugins.antplus"],"removal":"delete","type":"misc"},{"id":"com.dsi.ant.sample.acquirechannels","label":"ANT + DUT","description":"I don't know why there is \"sample\" in the name. Is this package really useful to find ANT channels ? \n","removal":"delete","type":"misc"},{"id":"com.dsi.ant.server","label":"ANT Radio Service Test","description":"ANT HAL (Hardware Abstraction Layer) Server.\nANT is a wireless protocol, similar to Bluetooth, that is mainly used for sport and fitness trackers.","removal":"delete","type":"misc"},{"id":"com.dsi.ant.service.socket","label":"ANT Radio Service","description":"ANT is a wireless protocol, similar to Bluetooth, that is mainly used for sport and fitness trackers.","web":["https://play.google.com/store/apps/details?id=com.dsi.ant.service.socket"],"removal":"replace","type":"misc"},{"id":"com.ebay.carrier","label":"com.ebay.carrier","description":"Kind of weird ebay apps pre-installed by carriers.","removal":"delete","type":"misc"},{"id":"com.ebay.mobile","label":"eBay","description":"Online shopping and selling app","web":["https://play.google.com/store/apps/details?id=com.ebay.mobile"],"removal":"delete","type":"misc"},{"id":"com.ehernandez.radiolainolvidable","label":"Radio La Inolvidable Peru","description":"Spanish Radio app (no longer exist)","removal":"delete","type":"misc"},{"id":"com.emoji.keyboard.touchpal","label":"TouchPal Emoji Keyboard","description":"Developed by Cootek a chinese company.\nAdware (lots and lots of ads)","web":["https://www.buzzfeednews.com/article/craigsilverman/google-banned-cootek-adware"],"removal":"replace","suggestions":"keyboards","type":"misc"},{"id":"com.eterno","label":"Dailyhunt","description":"Daily hunt news aggregator","web":["https://play.google.com/store/apps/details?id=com.eterno"],"removal":"replace","suggestions":"rss_readers","type":"misc"},{"id":"com.evernote","label":"Evernote","description":"Popular note taking app","web":["https://play.google.com/store/apps/details?id=com.evernote","https://evernote.com/privacy","https://privacy.commonsense.org/evaluation/evernote"],"removal":"replace","suggestions":"note_taking_apps","type":"misc"},{"id":"com.example","description":"Auto Dialer test","removal":"delete","type":"misc"},{"id":"com.example.myapplication","description":"Hidden app for testing:\nBattery, WiFi, Bluetooth","removal":"delete","type":"misc"},{"id":"com.facebook.appmanager","label":"Facebook App Manager","description":"Handles Facebook apps updates.","removal":"delete","type":"misc"},{"id":"com.facebook.katana","label":"Facebook","description":"Facebook social media app","web":["https://play.google.com/store/apps/details?id=com.facebook.katana"],"removal":"delete","type":"misc"},{"id":"com.facebook.lite","description":"Facebook Lite app (https://play.google.com/store/apps/details?id=com.facebook.lite)\n","removal":"delete","type":"misc"},{"id":"com.facebook.orca","label":"Messenger","description":"Facebook Messenger","web":["https://play.google.com/store/apps/details?id=com.facebook.orca"],"removal":"delete","type":"misc"},{"id":"com.facebook.services","label":"com.facebook.services","description":"Facebook Services is a tool that lets you manage different Facebook services automatically using your Android device.\nIn particular, the tool focuses on searching for nearby shops and establishments based on your interests.\nI don't know if this a dependency for com.facebook.katana but nobody cares because we all want to delete all the Facebook stuff right ?","removal":"delete","type":"misc"},{"id":"com.facebook.system","label":"com.facebook.system","description":"Facebook App Installer (empty shell app which incites you to install the Facebook app)","removal":"delete","type":"misc"},{"id":"com.fet.fridaywallet","description":"friDay Wealth Management\nProvides customized deposit goal progress management services\nhttps://play.google.com/store/apps/details?id=com.fet.fridaywallet","removal":"delete","type":"misc"},{"id":"com.fido.asm","description":"FIDO UAF1.0 ASM\nRelated to app fingerprint unlocking and payments. Safe to remove if you don't use password-less authentication to access online services.","removal":"replace","type":"misc"},{"id":"com.fido.uafclient","description":"FIDO UAF1.0 Client\nProbably related to FIDO digital key gadget (Client), probably safe to remove if you don't have any.","removal":"replace","type":"misc"},{"id":"com.finshell.fin","label":"FinShell Pay","description":"Provides various Payment and Financial Services. Pretty bad privacy policy.","web":["https://play.google.com/store/apps/details?id=com.finshell.fin","https://rwallet.finshell.co.in/html/user/privacy_policy.html"],"removal":"delete","type":"misc"},{"id":"com.fintech.life","description":"Chinese app that spoofs as a Singapore financial and/or payment app to show advertisement notifications (mostly loans)\nIt accesses locations, contacts, camera, mic by default, some people in Thailand also reported that they cannot use legitimate regional banking apps until this app was disabled or uninstalled with ADB method. While newer devices that start with Oppo ColorOS 13 and/or Realme UI 4 (around Android 13) are already baked in, it seems likely that it comes with a system update at older devices of those brands, so this is shady and constantly lost trust from many users.\nhttps://safereddit.com/r/Thailand/comments/1hzdwhr","removal":"delete","type":"misc"},{"id":"com.fw.upgrade.sysoper","label":"UpgradeSys","description":"FOTA = Firmware Over-the-air. Has a history of spying its users.","web":["https://www.malwarebytes.com/blog/news/2017/12/mobile-menace-monday-upping-the-ante-on-adups-fwupgradeprovider","https://www.cvedetails.com/vulnerability-list/vendor_id-16034/product_id-35606/year-2017/Adups-Adups-Fota.html"],"removal":"delete","type":"misc"},{"id":"com.galaxyfirsatlari","label":"Galaxy Fırsatları","description":"Samsung-only app for Turkish people\nRecommands you stuff to buy. You are supposed to save money but we all know this kind of apps\nEncourages consumption.\nExodus found 10 trackers and 17 permissions","web":["https://play.google.com/store/apps/details?id=com.galaxyfirsatlari","https://reports.exodus-privacy.eu.org/fr/reports/143830/"],"removal":"delete","type":"misc"},{"id":"com.gd.mobicore.pa","label":"RootPA","description":"Mobicore is now Trustonic\nTrustonic is a small OS running on the CPU providing a TEE, an isolated environment that runs in parallel with the operating system, guaranteeing code and data loaded inside to be protected.\nSounds great, but it's closed source and \"normal\" devs can't use it for their apps.\nSee \"com.trustonic.tuiservice\"","removal":"caution","type":"misc"},{"id":"com.generalmobi.go2pay","label":"Go2Pay","description":"Payment app that offers mobile pre-paid recharges and post-paid bill payment, data card recharges and bill payment,\nDTH (Direct To Home Television) recharges through cashless transactions","web":["https://play.google.com/store/apps/details?id=com.generalmobi.go2pay"],"removal":"delete","type":"misc"},{"id":"com.glance.internet","label":"Glance for realme","description":"Displays unsolicited \"trending\" stories on Lockscreen","web":["https://play.google.com/store/apps/details?id=com.glance.internet"],"removal":"delete","type":"misc"},{"id":"com.gohappy.mobileapp","description":"Shopping app of friDay\nhttps://play.google.com/store/apps/details?id=com.gohappy.mobileacom.fetself","removal":"delete","type":"misc"},{"id":"com.gotv.nflgamecenter.us.lite","label":"NFL","description":"NFL related latest news, highlights, stats & more","web":["https://play.google.com/store/apps/details?id=com.gotv.nflgamecenter.us.lite"],"removal":"delete","type":"misc"},{"id":"com.groupon","label":"Groupon","description":"Online shopping deals and coupons.","web":["https://play.google.com/store/apps/details?id=com.groupon","https://en.wikipedia.org/wiki/Groupon#Reception"],"removal":"delete","type":"misc"},{"id":"com.hancom.office.editor.hidden","label":"Hancom Office Editor","description":"Legacy Hancom Office Editor (Korean alternative to Microsoft Office). Featured in Samsung and LG phones","removal":"delete","type":"misc"},{"id":"com.handmark.expressweather","label":"1Weather","description":"Forecasts alerts app (contain ads)","web":["https://play.google.com/store/apps/details?id=com.handmark.expressweather"],"removal":"delete","type":"misc"},{"id":"com.handmark.expressweather.vpl","label":"1Weather","description":"Forecasts alerts app (contain ads)","removal":"delete","type":"misc"},{"id":"com.haoba.btsmart","label":"com.haoba.btsmart","description":"Agui Unibuds","removal":"delete","warning":"May only be needed if you use Uniherts Ear Buds (Unibuds)","type":"misc"},{"id":"com.haokan.pictorial","description":"92 Lock Screen for RealMe\n92 is pronounce in Chinese as 'Hao Kan' (it's the company name), so it's a Chinese app. This not related to 'Lock Screen Magazine' and potentially inject ads in lock screen.","removal":"delete","type":"misc"},{"id":"com.heytap.accessory","label":"Quick Connect","dependencies":["com.heytap.mcs"],"required_by":["com.oplus.synergy"],"description":"AKA Accessory Framework\nQuick device connect feature. Can be disabled via hidden setting (Settings -> Search 'App Enhancement Services' -> Quick device connect) if not wanted.\nAllows you to search for nearby devices and connect to them without having to go through the Bluetooth or WiFi Direct settings' Ghosh! 32 permissions just for this?","web":["https://beta.pithus.org/report/cc0ba95f0d0867ba6d883275cd2f6c4aa252ebc874f15f1ee240bb5bac330578"],"removal":"replace","type":"misc"},{"id":"com.huaqin.FM","label":"com.huaqin.FM","description":"Radio app from huaqin a chinese company\nNOTE : Transistor [https://f-droid.org/en/packages/org.y20k.transistor/] is much better","removal":"delete","type":"misc"},{"id":"com.huaqin.batteryinfo","description":"Tests hardware things.","removal":"delete","type":"misc"},{"id":"com.huaqin.clearefs","description":"Debugging logs Wifi, Bluetooth","removal":"delete","type":"misc"},{"id":"com.huaqin.pocketbanreceiver","description":"I found in code useless permissions BAN_BROADCAST and hello world!\nThis app means nothing.","removal":"delete","type":"misc"},{"id":"com.huaqin.sarcontroller","description":"It have something like sar sensor, idk if it's for testing\nbut mainactivity have chinese words and sar is for radio regulations?","removal":"delete","type":"misc"},{"id":"com.huaqin.shutdownservice","description":"ShutdownAfterScreenOff10MinutesService. Not needed","removal":"delete","type":"misc"},{"id":"com.hulu.plus","label":"Hulu","description":"TV shows & movies streaming app.\nFYI : Hulu is owned by Disney.","web":["https://play.google.com/store/apps/details?id=com.hulu.plus","https://www.digitaltrends.com/home-theater/hulu-vs-disney-plus/","https://en.wikipedia.org/wiki/Hulu"],"removal":"delete","type":"misc"},{"id":"com.idea.questionnare","label":"com.idea.questionnare","description":"[NEED MORE INFO / NEED APK] Quizz app from MobileIdea company?","removal":"delete","type":"misc"},{"id":"com.ideashower.readitlater.pro","label":"Pocket","description":"Allows you to save an article or web page to remote servers for later reading\nWas purchased by Mozilla in 2017 but is still close source for now.\nOpen-source alternative : https://wallabag.org/","web":["https://play.google.com/store/apps/details?id=com.ideashower.readitlater.pro","https://getpocket.com/privacy","https://en.wikipedia.org/wiki/Pocket_(service)"],"removal":"delete","type":"misc"},{"id":"com.imdb.mobile","label":"IMDb","description":"IMDb mobile app","web":["https://play.google.com/store/apps/details?id=com.imdb.mobile","https://www.imdb.com/privacy","https://en.wikipedia.org/wiki/IMDb"],"removal":"delete","type":"misc"},{"id":"com.infraware.polarisoffice5","label":"Polaris Office","description":"Polaris Office from US Infraware Inc company (Microsoft Office like)\nhttps://play.google.com/store/apps/details?id=com.infraware.office.link","web":["https://en.wikipedia.org/wiki/Polaris_Office"],"removal":"replace","type":"misc"},{"id":"com.instagram.android","label":"Instagram","description":"nstagram (from Meta) allows you to create and share your photos, stories, reels and videos with the friends and followers","web":["https://play.google.com/store/apps/details?id=com.instagram.android","https://privacycenter.instagram.com/policy/","https://en.wikipedia.org/wiki/Instagram"],"removal":"delete","type":"misc"},{"id":"com.ironsource.appcloud.oobe","label":"AppCloud","description":"AppCloud (discontinued) from ironSource, an advertising company","web":["https://en.wikipedia.org/wiki/IronSource"],"removal":"delete","type":"misc"},{"id":"com.ironsource.appcloud.oobe.huawei","label":"Essential","description":"An app that promotes some other apps (and encourages you to install them)\nDeveloped by IronSource, a \"next-generation advertising company\" \nhttps://aura.ironsrc.com/ (app) | https://company.ironsrc.com/ (company)\nWhen you try to read their privacy policy you arrive to an outstanding blank PDF file!","web":["http://www.ironsrc.com/wp-content/uploads/2019/03/ironSource-Privacy-Policy.pdf"],"removal":"delete","type":"misc"},{"id":"com.ironsoure.appcloud.oobe.wiko","description":"Provides first time setup for Wiko mobile","removal":"delete","type":"misc"},{"id":"com.king.candycrush4","label":"Candy Crush Friends","description":"A Candy Crush game (com.king.candycrushsaga) variant app","web":["https://play.google.com/store/apps/details?id=com.king.candycrush4","https://www.king.com/privacyPolicy","https://en.wikipedia.org/wiki/Candy_Crush_Saga"],"removal":"delete","type":"misc"},{"id":"com.king.candycrushsaga","label":"Candy Crush Saga","description":"Main Candy Crush game app.\nPre-installed in a lot of phones that helps its popularity","web":["https://play.google.com/store/apps/details?id=com.king.candycrushsaga","https://www.king.com/privacyPolicy","https://en.wikipedia.org/wiki/Candy_Crush_Saga"],"removal":"delete","type":"misc"},{"id":"com.king.candycrushsodasaga","label":"Candy Crush Soda","description":"A Candy Crush game (com.king.candycrushsaga) variant app","web":["https://play.google.com/store/apps/details?id=com.king.candycrushsodasaga","https://www.king.com/privacyPolicy","https://en.wikipedia.org/wiki/Candy_Crush_Saga"],"removal":"delete","type":"misc"},{"id":"com.kwai.video","label":"Kwai","description":"Just another one of the useless short video apps. Depending on the phone model, it may not be possible to uninstall properly but safe to disable. (Even disabled it still runs in the background).","web":["https://play.google.com/store/apps/details?id=com.kwai.video"],"removal":"delete","type":"misc"},{"id":"com.linkedin.android","label":"Linkedin","description":"One of the largest social network apps for online jobs","web":["https://play.google.com/store/apps/details?id=com.linkedin.android","https://en.wikipedia.org/wiki/LinkedIn"],"removal":"delete","type":"misc"},{"id":"com.lookout","label":"Lookout","description":"Mobile Security & Antivirus by Lookout","web":["https://play.google.com/store/apps/details?id=com.lookout"],"removal":"delete","type":"misc"},{"id":"com.magiear.handsfree.assistant","description":"Hands-Free Assistant\nOnly has permission activity voice from mediatek.\nhttps://play.google.com/store/apps/details?id=com.magiear.handsfree.assistant","removal":"delete","type":"misc"},{"id":"com.mediatek","label":"com.mediatek","description":"Mediatek is a Taiwanese chipset manufacturer.\nCan someone share the apk? This package name is really weird.\nIt is most likely a set of general APIs for accessing general mediatek functionalities.","removal":"unsafe","warning":"Removing the app will cause bootloop.","type":"misc"},{"id":"com.mediatek.FrameworkResOverlayExt","description":"DisplayCutout founded in code.","removal":"unsafe","type":"misc"},{"id":"com.mediatek.MtkSettingsResOverlay","description":"Nothing found in this app.","removal":"delete","type":"misc"},{"id":"com.mediatek.SettingsProviderResOverlay","description":"Nothing found in this app.","removal":"delete","type":"misc"},{"id":"com.mediatek.aovtestapp","description":"It tests in camera something.","removal":"delete","type":"misc"},{"id":"com.mediatek.atci.service","description":"Well this code is hard to understand, it have only notification stuff and needed for ims wifi calling.","removal":"replace","type":"misc"},{"id":"com.mediatek.atmwifimeta","label":"ATMWifiMeta","description":"wifi data logger you don't want","removal":"delete","type":"misc"},{"id":"com.mediatek.autodialer","description":"autodialer, have a lot useless code.","removal":"delete","type":"misc"},{"id":"com.mediatek.batterywarning","label":"com.mediatek.batterywarning","description":"Issues warning when the battery is low or when the battery temperature is high.","removal":"caution","type":"misc"},{"id":"com.mediatek.bluetooth.dtt","description":"Bluetooth logging.","removal":"delete","type":"misc"},{"id":"com.mediatek.calendarimporter","label":"VCalendar","description":"Useful in China where Google isn’t available, but not needed for Google users.","removal":"delete","type":"misc"},{"id":"com.mediatek.callrecorder","label":"Call Recorder","description":"This is not the kind of feature expected from a Soc company.\nIf you remove this I guess you will not be able to record your calls from the stock dialer\nCan someone share the apk and verify this?","removal":"delete","type":"misc"},{"id":"com.mediatek.camera","label":"Camera","description":"Stock Camera app on some Mediatek phones.","removal":"replace","suggestions":"cameras","type":"misc"},{"id":"com.mediatek.capctrl.service","label":"RilCap","description":"It has mtkradioex components used for corporate device management and telephony control.","removal":"unsafe","warning":"Removing this package will break key telephony functions, such as SIM detection and carrier configuration, and cause com.android.phone to crash repeatedly.","type":"misc"},{"id":"com.mediatek.carrierexpress","description":"Hidden operator configuration. it's Carrier Express app?\nalso in code there a lot stuff about Custom Operator.\nI dont know why it should be useful when it only has notifications:\nSIM card detected or Switching the carrier etc.\nit's useless.","removal":"delete","type":"misc"},{"id":"com.mediatek.cellbroadcastuiresoverlay","description":"This app have something to emergency.","removal":"replace","type":"misc"},{"id":"com.mediatek.dataprotection","label":"Data Protection","description":"Possibly related to user partition encryption/decryption.\nA device should works flawlessly without it.","removal":"delete","type":"misc"},{"id":"com.mediatek.duraspeed","description":"A frontend to a Mediatek service that fully takes over Android's own Adaptive Battery management. Uninstalling this app will only remove the UI component, but not the system service that it's controlling. To completely disable Duraspeed you need to have it enabled first, open Duraspeed app via Settings, and set the toggle to 'Off'. Otherwise Duraspeed service will continue running despite there not being a Duraspeed entry in Settings which will lead to unexpected app freezes that affect FOSS apps such as Dialers/Phones and messengers.","removal":"caution","type":"misc"},{"id":"com.mediatek.emcamera","description":"Useless camera calibration hidden app.","removal":"delete","type":"misc"},{"id":"com.mediatek.engineermode","label":"EngineerMode","description":"Engineer mode you can access by dialing a secret code (*#*#3646633#*#* on some Xiaomi phones for instance)\nIt enables you to access the debug/logged data and some hidden firmware settings.","removal":"delete","type":"misc"},{"id":"com.mediatek.entitlement.fcm","description":"it's only FCM(Firebase Cloud Messaging). No activities, google firebase. Not needed.","removal":"delete","type":"misc"},{"id":"com.mediatek.factorymode","description":"Tests hardware things.","removal":"delete","type":"misc"},{"id":"com.mediatek.frameworkresoverlay","description":"It have something to config AOD.\nAnd power saving.","removal":"unsafe","type":"misc"},{"id":"com.mediatek.gba","description":"Generic Bootstrapping Architecture.\nHas ims, X-TMUS-IMEI, bsf.msg.lab.t-mobile.com things to encrypted calls ims probably\n(thinking by looking at the classes.dex code). Connects to site bsf.ims.mncXXX.pub.3gppnetwork.org.","removal":"caution","type":"misc"},{"id":"com.mediatek.gbaservice","description":"Generic Bootstrapping Architecture. A ‘common ground’ of code used for many MediaTek apps.\nCan be removed if all other MediaTek apps are removed.","removal":"delete","type":"misc"},{"id":"com.mediatek.gnss.nonframeworklbs","description":"Have hidden activity and it's maybe for debug location requests, modem, VoWiFi and more.\nNot useful.","removal":"delete","type":"misc"},{"id":"com.mediatek.gnssdebugreport","label":"GnssDebugReport","description":"Hidden debug stuff.","removal":"delete","type":"misc"},{"id":"com.mediatek.gpslocationupdate","label":"GPSLocationUpdate","description":"Info about gps and notifications. Probably not needed for location.","removal":"delete","type":"misc"},{"id":"com.mediatek.ims","label":"com.mediatek.im","description":"Mediatek's implementation of IMS (low-level implementation?)\nIMS(Ip Multimedia Subsystem) is an open industry standard for voice and multimedia communications over packet-based IP networks (VoLTE/VoIP/Wifi calling).","web":["https://www.programmersought.com/article/50164530665/"],"removal":"caution","type":"misc"},{"id":"com.mediatek.ims.pco","label":"com.mediatek.ims.pco","description":"Protocol Configuration Options service for IMS\nIMS(IP Multimedia Subsystem) is an open industry standard for voice and multimedia communications over packet-based IP networks (VoLTE/VoIP/Wifi calling). This package enable automatic configuration pushed by your carrier.","removal":"caution","warning":"Maybe needed if you use IMS","type":"misc"},{"id":"com.mediatek.ims.rcsua.service","description":"Needed to support IMS, RCS probably.","removal":"replace","type":"misc"},{"id":"com.mediatek.lbs.em2.ui","label":"LocationEM2","description":"Another GPS status/testing app. Removing it doesn’t stop GPS from working.","removal":"delete","type":"misc"},{"id":"com.mediatek.location.lppe.main","label":"LPPe Service","description":"LPPE = LTE Positioning Protocol enhancements/extensions (LTE = \"4G\")\nPositioning and assistance protocol between E-SMLC (mobile location center) and UE (User Equipement = phone)\nI don't know the app has the permission to read SMS","web":["https://www.gpsworld.com/wirelessexpert-advice-positioning-protocol-next-gen-cell-phones-11125/"],"removal":"caution","type":"misc"},{"id":"com.mediatek.location.mtkgeofence","description":"Mtk Geofence\nOnly logs in code","removal":"delete","type":"misc"},{"id":"com.mediatek.location.mtknlp","label":"Mtk Nlp","description":"Network Location Provider? This app has location permissions and no code.","removal":"caution","type":"misc"},{"id":"com.mediatek.magtapp","description":"MAGTApp\nUnknown, has GameEventService, MAGTEventAppReceiver, but code doesn't look very useful.","removal":"delete","type":"misc"},{"id":"com.mediatek.mdmconfig","label":"MDMConfig","description":"Mobile Device Management (MDM) allows company’s IT department to reach inside your phone in the background, allowing them to ensure your device is secure, know where it is, and remotely erase your data if the phone is stolen.\nIt's a way to ensure employees stay productive and do not breach corporate policies\nYou should NEVER have a MDM tool on your personal phone. Never.\nThis package probably isn't a MDM tool on its own but you definitively don't need it on your phone.\nCan someone share the apk?","web":["https://blog.cdemi.io/never-accept-an-mdm-policy-on-your-personal-phone/"],"removal":"delete","type":"misc"},{"id":"com.mediatek.mdmlsample","label":"MDMLSample","description":"It looks like debugging app.\nBut I found some words SUBSCRIBE_TRAP, OTA, VoLTE\nOther data is a lot of debugging code so it's not needed.","removal":"delete","type":"misc"},{"id":"com.mediatek.miravision.ui","label":"MiraVision","description":"Provides extensive hardware and software optimizations that improve the viewing quality.\nBUT I think it's not available and it's so bloated.","web":["https://www.mediatek.com/technology/miravision-for-smartphones"],"removal":"delete","type":"misc"},{"id":"com.mediatek.mms.appservice","label":"com.mediatek.mms.appservice","description":"Provides Voice message, Video message, Fax message, Text message in a messaging app?","removal":"caution","type":"misc"},{"id":"com.mediatek.mt6879.gamedriver","description":"Mediatek Arm GPU Game Driver\nGPU Game drivers for Mediatek MT6879CPU.","removal":"unsafe","type":"misc"},{"id":"com.mediatek.mt6983.gamedriver","description":"Arm GPU Game Driver","removal":"unsafe","type":"misc"},{"id":"com.mediatek.mtklogger","label":"MTKLogger","description":"Logs debug data. Has a lot of permissions and run in background all the time.\nDon't keep useless apps: reduce the attack surface\nVulnerability found in this app in 2016","web":["https://nvd.nist.gov/vuln/detail/CVE-2016-10135"],"removal":"delete","type":"misc"},{"id":"com.mediatek.mtklogger.proxy","description":"Logs debug data.","removal":"delete","type":"misc"},{"id":"com.mediatek.nlpservice","label":"Mediatek Network Location Provider","description":"Provides periodic reports on the geographical location of the device. Each provider has a set of criteria under which it may be used. For example, some providers require GPS hardware and visibility to a number of satellites, while others require the use of the cellular radio, or access to a specific carrier's network, or to the internet.\nI don't understand why this is needed; there already is one in 'com.google.android.gms'\nI wonder if NLP can be replaced by https://github.com/microg/UnifiedNlp\nI suggest testing if you get a better signal/battery performance with Mediatek NLP on or off.","removal":"caution","type":"misc"},{"id":"com.mediatek.omacp","label":"Omacp","description":"omacp = OMA Client Provisioning. A protocol specified by the Open Mobile Alliance (OMA).\nConfiguration messages parser. Used for provisioning APN settings to devices via SMS.\nIn my case, it was automatic and I never needed configuration messages.\nMaybe it's useful if carriers change their APN. But you can still change the config manually, it's not difficult.\nDunno why Mediatek handles this kind of things. Safe to remove. At worst, you'll need to manually config your APN.\nOMACP can be abused.","web":["https://research.checkpoint.com/2019/advanced-sms-phishing-attacks-against-modern-android-based-smartphones/","https://www.zdnet.com/article/samsung-huawei-lg-and-sony-phones-vulnerable-to-rogue-provisioning-messages/"],"removal":"caution","type":"misc"},{"id":"com.mediatek.presence","description":"App used to IMS, RCS. IP Voice Call.","removal":"replace","type":"misc"},{"id":"com.mediatek.providers.drm","label":"DRM provider","description":" It actually Beep Science is MediaTek’s default DRM vendor\nProbably required for some forms of DRM; disabling might break things like Netflix streaming, which relies on DRM to function.","web":["https://en.wikipedia.org/wiki/Digital_rights_management"],"removal":"replace","type":"misc"},{"id":"com.mediatek.schpwronoff","description":"Set schedule power on & off.","removal":"replace","type":"misc"},{"id":"com.mediatek.sensorhub.ui","description":"Testing sensors.","removal":"delete","type":"misc"},{"id":"com.mediatek.simprocessor","label":"com.mediatek.simprocessor","description":"This controls and imports contacts saved on a SIM card. Not needed if you don't store your contacts on the SIM card","removal":"replace","type":"misc"},{"id":"com.mediatek.smartratswitch.service","description":"it handles switching connection between network types. (4G/5G)","removal":"caution","type":"misc"},{"id":"com.mediatek.systemuiresoverlay","description":"It have nothing in code.","removal":"delete","type":"misc"},{"id":"com.mediatek.systemuiwmshellresoverlay","description":"config pip_corner_radius only found.","removal":"unsafe","type":"misc"},{"id":"com.mediatek.telephony","description":"allows you to get information about the available \nSIMs/subscriptions and listen for changes or activity on the SIM cards, such as call or data activity or \nconnected cell details. In addition, the API enables apps to create SMS messages and send them using \na specific SIM card. it's not useful.","removal":"replace","type":"misc"},{"id":"com.mediatek.thermalmanager","label":"MTK Thermal Manager","description":"Hidden testing or logging app.","removal":"delete","type":"misc"},{"id":"com.mediatek.voicecommand","description":"It's for voice commands like control music playing or voice control for alarm, camera.\nBut how to get access to it?\nProbably you need that for voice recognition but not sure.\nMaybe related to https://www.mediatek.com/products/smart-home/voice-assistant-devices","removal":"delete","type":"misc"},{"id":"com.mediatek.voiceunlock","description":"It's for voice commands like control music playing or voice control for alarm, camera.\nBut how to get access to it?\nProbably you need that for voice recognition but not sure.\nMaybe related to https://www.mediatek.com/products/smart-home/voice-assistant-devices","removal":"delete","type":"misc"},{"id":"com.mediatek.wfo.impl","label":"com.mediatek.wfo.impl","description":"According to olorin, it's a MediaTek’s default fingerprint app (and he removed it).\nCan someone confirm what this package does?\nRemember that any pre-installed apps you don't actually need just increase the surface attack.\nAny app co-located on the device could modify a system property through an exported interface without proper authorization.\nVulnerability found in 2019","web":["https://www.olorin.me/2019/09/08/debloating-the-umidigi-f1-play/","https://nvd.nist.gov/vuln/detail/CVE-2019-15368"],"removal":"delete","type":"misc"},{"id":"com.mediatek.ygps","label":"YGPS","description":"GPS test and bug report utilities, accessed via Engineer Mode. Not needed.","removal":"replace","type":"misc"},{"id":"com.mediatek.zramwritebackoverlay","description":"config zramWriteback = true. What is this used for?","removal":"caution","type":"misc"},{"id":"com.micredit.in","label":"Mi Credit","description":"App providing loans to MIUI users from India and China","web":["https://play.google.com/store/apps/details?id=com.micredit.in.gp","https://web.archive.org/web/20221207193942/https://onsitego.com/blog/xiaomi-quietly-discontinues-mi-credit-mi-pay-india/"],"removal":"delete","type":"misc"},{"id":"com.microsoft.appmanager","label":"Link to Windows","description":"Microsoft app for synchronising your phone with a Windows PC.","web":["https://play.google.com/store/apps/details?id=com.microsoft.appmanager"],"removal":"replace","type":"misc"},{"id":"com.microsoft.office.excel","label":"Microsoft Excel","description":"Spreadsheets, business collaboration, charts and data analysis tool from Microsoft","web":["https://play.google.com/store/apps/details?id=com.microsoft.office.excel"],"removal":"replace","type":"misc"},{"id":"com.microsoft.office.officehub","label":"Microsoft Office Mobile","description":"Includes the complete Word, PowerPoint, and Excel apps to offer a convenient office experience on the go.","removal":"replace","type":"misc"},{"id":"com.microsoft.office.officehubhl","label":"Office Mobile hub","description":"Office Mobile hub (on Samsung Phone)\nIncludes the complete Word, PowerPoint, and Excel apps to offer a convenient office experience on the go.","removal":"replace","type":"misc"},{"id":"com.microsoft.office.officehubrow","label":"Microsoft 365 (Office)","description":"Word, Excel, PowerPoint, PDF scanner/editor, Cloud services...all in one app","web":["https://play.google.com/store/apps/details?id=com.microsoft.office.officehubrow"],"removal":"replace","type":"misc"},{"id":"com.microsoft.office.onenote","label":"Microsoft OneNote","description":"Note taking app from Microsoft.\nThis app has a lot of permissions. For example it has access to phone state, including the phone number of the device, current cellular network information, the status of any ongoing calls...","web":["https://play.google.com/store/apps/details?id=com.microsoft.office.onenote"],"removal":"replace","suggestions":"note_taking_apps","type":"misc"},{"id":"com.microsoft.office.outlook","label":"Microsoft Outlook","description":"Microsoft email application","web":["https://play.google.com/store/apps/details?id=com.microsoft.office.outlook"],"removal":"replace","suggestions":"email_clients","type":"misc"},{"id":"com.microsoft.office.powerpoint","label":"Microsoft PowerPoint","description":"Microsoft presentation and slides app","web":["https://play.google.com/store/apps/details?id=com.microsoft.office.outlook"],"removal":"replace","type":"misc"},{"id":"com.microsoft.office.word","label":"Microsoft Word","description":"Create, edit, share microsoft Word documents much like you do on your PC","web":["https://play.google.com/store/apps/details?id=com.microsoft.office.word"],"removal":"replace","type":"misc"},{"id":"com.microsoft.skydrive","label":"Microsoft OneDrive","description":"Cloud storage app from Microsoft","web":["https://play.google.com/store/apps/details?id=com.microsoft.skydrive"],"removal":"replace","suggestions":"cloud_services","type":"misc"},{"id":"com.microsoft.translator","label":"Microsoft Translator","description":"Translate text, voice, conversations, camera photos and screenshots etc with microsoft translator","web":["https://play.google.com/store/apps/details?id=com.microsoft.translator"],"removal":"replace","suggestions":"translators","type":"misc"},{"id":"com.monotype.android.font.applemint","description":"Font","removal":"delete","type":"misc"},{"id":"com.monotype.android.font.chococooky","label":"ChocoEUKor font","description":"Font","removal":"delete","type":"misc"},{"id":"com.monotype.android.font.cooljazz","label":"CoolEUKor font","description":"Font","removal":"delete","type":"misc"},{"id":"com.monotype.android.font.foundation","label":"Foundation font","description":"Font","removal":"delete","type":"misc"},{"id":"com.monotype.android.font.rosemary","label":"RoseEUKor font","description":"Font","removal":"delete","type":"misc"},{"id":"com.monotype.android.font.tinkerbell","description":"Font","removal":"delete","type":"misc"},{"id":"com.mtk.telephony","description":"SimRecoveryTestTool","removal":"delete","type":"misc"},{"id":"com.netflix.mediaclient","label":"Netflix","description":"Popular TV shows and movies streaming application","web":["https://play.google.com/store/apps/details?id=com.netflix.mediaclient"],"removal":"replace","suggestions":"streaming_apps","type":"misc"},{"id":"com.netflix.partner.activation","label":"PartnerNetflixActivation","description":"Apk file name: By_3rd_NetflixActivationOverSeas\nSome form of activation of Netflix account, subscription or app? Might be what puts the Netflix app icon on the homescreen. Not sure.\nNetflix app works without this.","removal":"delete","type":"misc"},{"id":"com.nextradioapp.nextradio","label":"NextRadio","description":"Adware FM radio\n3rd-party app which lets you experience live and local FM radio on your smartphone.","web":["https://play.google.com/store/apps/details?id=com.nextradioapp.nextradio"],"removal":"replace","suggestions":"radios","type":"misc"},{"id":"com.niksoftware.snapseed","label":"Snapseed","description":"Google photo editor","web":["https://play.google.com/store/apps/details?id=com.niksoftware.snapseed"],"removal":"delete","type":"misc"},{"id":"com.nuance.swype.input","label":"Swype","description":"Swype keyboard by Nuance","web":["https://www.nuance.com/mobile/mobile-applications/swype/android.html","https://en.wikipedia.org/wiki/Swype"],"removal":"replace","suggestions":"keyboards","type":"misc"},{"id":"com.oem.rftoolkit","label":"RfToolkit","description":"Testing things like Wi-Fi, lots of Chinese words","removal":"delete","type":"misc"},{"id":"com.omusic.gPhone","description":"friDay music\nA treasure house of global music, thousands of situational playlists, complete classification of Chinese, Western, Japanese and Korean, classical, and original soundtracks, allowing you to easily travel the digital music world.\nhttps://play.google.com/store/apps/details?id=com.omusic.gPhone","removal":"delete","type":"misc"},{"id":"com.opera.branding","label":"Opera Branding Provider","description":"Don't know what it really does.","removal":"delete","type":"misc"},{"id":"com.opera.branding.news","label":"Opera News Branding Provider","description":"Don't know what it really does.","removal":"delete","type":"misc"},{"id":"com.opera.max.oem","label":"Opera Max","description":"System-wide data-saving proxy that funnell all app data through Opera’s servers to compress images and videos (discontinued)","removal":"delete","type":"misc"},{"id":"com.opera.max.preinstall","label":"Opera Max","description":"System-wide data-saving proxy that funnell all app data through Opera’s servers to compress images and videos (discontinued)","removal":"delete","type":"misc"},{"id":"com.opera.mini.native","label":"Opera Mini","description":"Tracks your online activities, which are linked to your unique ID.\nIt has built-in VPN for 'free', which Restore Privacy describe as a 'data collection tool in disguise'.","web":["https://play.google.com/store/apps/details?id=com.opera.mini.native","https://www.opera.com/privacy","https://restoreprivacy.com/vpn/reviews/opera-vpn/","https://privacytests.org/android.html"],"removal":"replace","suggestions":"browsers","type":"misc"},{"id":"com.opera.preinstall","label":"Opera Preinstall Data","description":"Generates utm tracking stuff","removal":"delete","type":"misc"},{"id":"com.opos.cs","label":"com.opos.cs","description":"Hot Apps\nGenerate app folders on home screen that recommended sponsored apps and games.","removal":"delete","type":"misc"},{"id":"com.pandora.android","label":"Pandora","description":"Very intrusive music and podcasts app.\nExodus report: 17 permissions and 14 trackers","web":["https://play.google.com/store/apps/details?id=com.pandora.android","https://reports.exodus-privacy.eu.org/reports/com.pandora.android/latest/"],"removal":"replace","suggestions":"music_apps","type":"misc"},{"id":"com.particlenews.newsbreak","label":"NewsBreak","description":"News provided by NewsBreak (https://www.newsbreak.com/)","web":["https://play.google.com/store/apps/details?id=com.particlenews.newsbreak","https://reports.exodus-privacy.eu.org/en/reports/com.particlenews.newsbreak/latest/"],"removal":"replace","suggestions":"rss_readers","type":"misc"},{"id":"com.phonepe.app","label":"PhonePe","description":"PhonePe is a payment app that allows indian users to use BHIM UPI, your credit card and debit card or wallet to recharge your mobile phone.\nPay your utility bills and also make instant payments at offline and online stores.","web":["https://play.google.com/store/apps/details?id=com.phonepe.app","https://en.wikipedia.org/wiki/PhonePe"],"removal":"delete","type":"misc"},{"id":"com.pinsight.dw","label":"App Stack","description":"Force-installed app by Sprint. Pinsight is an advertising company\nNote: Sprint sold Pinsight to InMobi in 2018.","web":["https://pinsightmedia.com/","https://www.fiercewireless.com/wireless/sprint-sells-mobile-ad-biz-pinsight-media-to-inmobi"],"removal":"delete","type":"misc"},{"id":"com.pinsight.v1","label":"com.pinsight.v1","description":"App Spotlight\nMakes you discover new apps from the Google Play store. The selection criteria is unknown.","removal":"delete","type":"misc"},{"id":"com.pivotmobile.android.metrics","label":"com.pivotmobile.android.metrics","description":"Pivot Mobile is a popular game developer famously known for its Swimmy Turtle game.\nSub-downloaded by third-party apps; used for 'data consumption & ad' tracking.\nCommonly found in Lenovo & Motorola devices.","web":["https://howtofixapp.com/com-pivotmobile-android-metrics/","https://basicknowledgehub.com/com-pivotmobile-android-metrics/","https://beta.pithus.org/report/dd41b027c2999fb9d5b023e4171c238b6f0b9edfb9087703e9d64c3f6148530d"],"removal":"delete","type":"misc"},{"id":"com.playphone.gamestore","label":"com.playphone.gamestore","description":"Playphone Gamestore (https://www.playphone.com/)\n\"Helps\" you discover the \"best\" Android games and connects you to a global gaming community. Sounds Amazing !","removal":"delete","type":"misc"},{"id":"com.playphone.gamestore.loot","label":"com.playphone.gamestore.loot","description":"Loot \nPremium service from playphone ?","removal":"delete","type":"misc"},{"id":"com.pure.indosat.care","label":"myIM3","description":"App provided by Indosat Ooredoo, an Internet provider from Indonesia.\nEnables Indosat users to manage prepaid and postpaid numbers and check their credit and payments, purchase data packs, calls, SMS...","web":["https://play.google.com/store/apps/details?id=com.pure.indosat.care"],"removal":"delete","type":"misc"},{"id":"com.qti.confuridialer","label":"Conference URI dialer","description":"Conference call service for digital signal (SIP/VoIP).\nIt's hidden and no apps use it.","removal":"replace","type":"misc"},{"id":"com.qti.dcf","description":"I found only: DCF Allows an application to share content using Bluetooth.\nThe application is only allowed to broadcast the content as it already has access to remote devices. These things are not available for users.\nQualcomm only said these things, but where is the code?\nThis app is without code so it's safe to remove.","removal":"delete","type":"misc"},{"id":"com.qti.diagservices","label":"com.qti.diagservices","description":"Starts process when plugged into a PC (with debugging on, haven't tried off) and then runs until stopped.\nDiagnostic services Presumably tests to collect hardware data.\nsize of this package is 12 KB and have Diag_OnBoot, QTIDiagServices.\nHas permission: RECEIVE_BOOT_COMPLETED.","removal":"delete","type":"misc"},{"id":"com.qti.dpmserviceapp","label":"com.qti.dpmserviceapp","description":"Data Power Manager for the radio?\nUsed to improve energy efficiency?\nIn code I found something like this:\ndpm hal server, read procid\nit's still unknown.","removal":"caution","type":"misc"},{"id":"com.qti.ltebc","label":"LTE Broadcast Manager","description":"Runs on boot, but not in the background beyond that.","removal":"caution","type":"misc"},{"id":"com.qti.pasrservice","description":"Has powersaving things, device idle mode changer to screen on/off.","removal":"caution","type":"misc"},{"id":"com.qti.phone","description":"dialer, dialing service, for phone calls.\nHas IMS things, optimizations?, its weird that calling works after remove, has code related to 'com.qualcomm.qcrilmsgtunnel',\nChina Mobile Communications Corporation(China Mobile) SIM card things.","removal":"caution","type":"misc"},{"id":"com.qti.powermodule","description":"it's for powersaving? have more debugging stuff than powersaving.","removal":"caution","type":"misc"},{"id":"com.qti.qcc","description":"QCC\nHave a lot of stuff about logs, testing framework for Android with Robolectric, LTE Broadcast.\nIntroduced in android 13.\nHave png file qdma = Qualcomm Device Management and Analytics.\nSo it's only spyware.","removal":"delete","type":"misc"},{"id":"com.qti.qualcomm.datastatusnotification","label":"com.qti.qualcomm.datastatusnotification","description":"Sends you a message when you reach a specified data limit?\nContains a service, but I've never it run. But I've also never run out of data or used the Android data warning system.","web":["https://www.qualcomm.com/news/onq/2016/05/02/qualcomm-trupalette-brings-your-phones-display-life"],"removal":"caution","warning":"On HyperOS, disabling this package results in the mobile networks section to break as well as SIMs showing no service.","type":"misc"},{"id":"com.qti.qualcomm.deviceinfo","label":"Device Info","description":"Hidden device info activity not available for users.\nIt's safe to remove and you will not loose device info in settings.","removal":"delete","type":"misc"},{"id":"com.qti.qualcomm.mstatssystemservice","description":"Useless network statistics, package usage.","removal":"delete","type":"misc"},{"id":"com.qti.service.colorservice","label":"com.qti.service.colorservice","description":"Allows the application to directly affect the device's display paramter.\nAFAIK no apps use it.","removal":"delete","type":"misc"},{"id":"com.qti.slaservice","description":"Service Level Agreement (SLA) protocol that analyzes network performance, quality of service, \nand provides users with various parameters of network quality of service, \nsuch as: jitter delay, file transfer rate, TCP latency. BUT it's placebo.\nuses permission com.miui.analytics.onetrack.TRACK_EVENT also have a lot chinese stuff.","removal":"delete","type":"misc"},{"id":"com.qti.snapdragon.qdcm_ff","label":"QDCM-FF","description":"Qualcomm Display Color Management tool\nAttempts to \"make colors look vibrant and true to life\". No idea if it actually does something useful or if it's only some garbage dynamic color tuning (they tend to destroy colors).\nContains a service, but I've never seen it run on my Oneplus 9. Could be tied to color \"improvement\" settings in Settings->Display (all of which are off for me).","removal":"delete","type":"misc"},{"id":"com.qti.xdivert","label":"Smart-Divert","description":"If enabled, diverts your calls to another number.\nYou can choose to divert all calls, divert on no reply or divert when the line is busy.\nWhere can you enable/disable this feature?","removal":"delete","type":"misc"},{"id":"com.qualcomm.atfwd","label":"com.qualcomm.atfwd","description":"Used to send AT command messages from/to the modem\nAttention commands commands are a collection of short-string commands developed in the early 1980s \nthat were designed to be transmitted via phone lines and control modems. Different AT command strings can be merged together \nto tell a modem to dial, hang up, or change connection parameters. \nSmartphones include a basic modem component inside them, which allows the smartphone to connect to the Internet \nvia its telephony function.\nThis can be abused. It's been known for many years that Android devices are vulnerable to attacks carried out via AT commands.","web":["https://www.bleepingcomputer.com/news/security/smartphones-from-11-oems-vulnerable-to-attacks-via-hidden-at-commands/"],"removal":"delete","type":"misc"},{"id":"com.qualcomm.atfwd2","description":"This is the same app as the 'com.qualcomm.atfwd'.","removal":"delete","type":"misc"},{"id":"com.qualcomm.cabl","label":"Content Adaptive Backlight Settings","description":"This app will try to adjust the image being displaye by changing the contrast/quality/image backlight depending on \nthe content on the screen.\nDownside to this is loss of dynamic range which results in some colors being washed out/clipped.\nCABL != Auto brightness (which doesn't change the content of the screen, only the brightness)\nNOTE: You may want to remove this. It does not work very well on many phones.","web":["https://mobileinternist.com/disable-adaptive-brightness-android"],"removal":"delete","type":"misc"},{"id":"com.qualcomm.embms","label":"com.qualcomm.embms","description":"Runs on boot, but not in the background beyond that?\nAdds support for eMBMS(evolved Multimedia Broadcast Multicast Service), also known as: LTE Broadcast\nEnables carriers to send content using multicast/broadcast (same content to many users at the same time) instead of unicast(to a single user).\nIt's a more efficient use of network resources compared to users receiving the same content individually.\nProbably safe to disable if you don't care about multi/broad-casts.","web":["https://en.wikipedia.org/wiki/Multimedia_Broadcast_Multicast_Service"],"removal":"delete","type":"misc"},{"id":"com.qualcomm.fastdormancy","label":"com.qualcomm.fastdormancy","description":"Provide Fast Dormancy feature/setting in the dialer (reduce battery consumption and network utilization during periods of data inactivity)","web":["https://en.wikipedia.org/wiki/Fast_Dormancy"],"removal":"replace","type":"misc"},{"id":"com.qualcomm.location","label":"LocationServices","description":"Runs in the background as part of the system. Runs even if disabled, so probably pointless to disable.\nPeriodically sends a unique software ID, location (lat, long, alt, and their uncertainty), nearby cellular towers and Wi-Fi hotspots and their signal strength to Qualcomm servers.","web":["https://www.qualcomm.com/site/privacy/services"],"removal":"delete","type":"misc"},{"id":"com.qualcomm.location.XT","description":"Qualcomm IZat. it's hidden XT Activity. Location for Qualcomm. It's a bad idea to keep it.\nPeriodically sends a unique software ID, location (lat, long, alt, and their uncertainty), nearby cellular towers and Wi-Fi hotspots and their signal strength to Qualcomm servers.\nhttps://www.qualcomm.com/site/privacy/services","removal":"delete","type":"misc"},{"id":"com.qualcomm.location.XT.setup","description":"Qualcomm IZat\nIts hidden Setup XT location for Qualcomm. It's a bad idea to keep it.\nPeriodically sends a unique software ID, location (lat, long, alt, and their uncertainty), nearby cellular towers and Wi-Fi hotspots and their signal strength to Qualcomm servers.\nhttps://www.qualcomm.com/site/privacy/services","removal":"delete","type":"misc"},{"id":"com.qualcomm.qcom_qmi","description":"qchat logo png founded also Hello World! and app_name Qchat_QMI\nthis app has no code and no activities so safe to remove.","removal":"delete","type":"misc"},{"id":"com.qualcomm.qcrilmsgtunnel","label":"com.qualcomm.qcrilmsgtunnel","description":"Message receiving channel (secondary card can't turn on 5g).\nIf you don't use dual-sim it's safe to remove.","removal":"caution","warning":"Breaks calls after a reboot on some phones","type":"misc"},{"id":"com.qualcomm.qti.accesscache","description":"CarrierAccessCacheService\nWell it's only name app and no code.","removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.auth.fidocryptoservice","label":"FidoCryptoService","description":"Qualcomm FIDO implementation.\nFido is a set of open technical specifications for mechanisms of authenticating users to online services that do not depend on passwords.","web":["https://en.wikipedia.org/wiki/FIDO_Alliance","https://fidoalliance.org/specs/u2f-specs-1.0-bt-nfc-id-amendment/fido-glossary.html","https://fidoalliance.org/specs/fido-v2.0-rd-20170927/fido-overview-v2.0-rd-20170927.html"],"removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.auth.sampleauthenticatorservice","description":"it's only Sample,\nnothing important found in the code","removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.auth.sampleextauthservice","description":"it's only Sample,\nnothing important found in the code","removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.auth.secureextauthservice","description":"it's only Sample,\nnothing important found in the code","removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.auth.securesampleauthservice","description":"it's only Sample,\nnothing important found in the code","removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.autoregistration","label":"com.qualcomm.qti.autoregistration","description":"Collects device activation data to remotely activate phone warranty.\nIn 2019 this package sent private data (IMEI, CELLID, CCID) in clear-text to zzhc.vnet.cn (chinese server). According to HMD (Nokia) it was a mistake.","web":["https://www.androidauthority.com/nokia-7-plus-user-info-967901/"],"removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.biometrics.fingerprint.service","label":"com.qualcomm.qti.biometrics.fingerprint.service","description":"Fingerprint authentication not used by any app, maybe only china.\nTests temperature, debug, logs, fingerprint biometrics.","removal":"caution","type":"misc"},{"id":"com.qualcomm.qti.callenhancement","label":"CallEnhancement","description":"Supposed to enhance call quality (I'll let you test if it really does)\nThis can record your phone calls. A vulnerability was found in 2019, allowing unauthorized microphone audio recording by 3rd-party apps.","web":["https://nvd.nist.gov/vuln/detail/CVE-2019-15472"],"removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.callfeaturessetting","label":"CallFeatureSetting","description":"Hidden call forwarding, call waiting settings. Not available for users.","removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.cne","label":"CneApp","description":"Connectivity Engine that runs in the background as part of the System.\nEnables seamless hand-off between mobile data and Wi-Fi networks. Can also dynamically measure network performance to prioritize using the best one (I think that's part of \"Intelligently select the best Wi-Fi\" in settings).\nProbably worth keeping on; I noticed connection reliability getting worse when I disabled it.","web":["https://www.qualcomm.com/news/onq/2013/07/02/qualcomms-cne-bringing-smarts-3g4g-wi-fi-seamless-interworking","https://programmersought.com/article/35091829299/"],"removal":"caution","type":"misc"},{"id":"com.qualcomm.qti.confdialer","label":"ConfDialer","description":"LTE Conferencing Service.","removal":"caution","type":"misc"},{"id":"com.qualcomm.qti.devicestatisticsservice","label":"com.qualcomm.qti.devicestatisticsservice","description":"Device statistics service Statistics of the phone's usage: data, Wifi, battery, the use of various software, number of SMS and emails sent, etc.\nIt's not very useful, can be removed.","removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.dynamicddsservice","label":"com.qualcomm.qti.dynamicddsservice","description":"Dynamic DDS Service\nDDS = Direct Digital Synthesizer. Supposedly useful for testing, communication and frequency sweep applications. Some apps may use this for local communication between devices? I'm guessing this is related to sending data through audio(a bunch of rapid beeps outside of the range of human hearing), which I believe Google Home used(still uses?) at one point as an option to connect to a Chromecast.","web":["https://www.qualcomm.com/news/releases/1996/05/07/qualcomm-introduces-new-high-speed-dual-direct-digital-synthesizer","https://www.allaboutcircuits.com/technical-articles/direct-digital-synthesis/"],"removal":"caution","type":"misc"},{"id":"com.qualcomm.qti.gpudrivers.kalama.api33","description":"Adreno Graphics Drivers","removal":"unsafe","type":"misc"},{"id":"com.qualcomm.qti.gpudrivers.kona.api30","description":"Adreno Graphics Drivers\nGPU drivers for Snapdragon 865 and 870.","removal":"unsafe","type":"misc"},{"id":"com.qualcomm.qti.gpudrivers.lahaina.api30","description":"Adreno Graphics Drivers\nGPU drivers for Snapdragon 888.","removal":"unsafe","type":"misc"},{"id":"com.qualcomm.qti.gpudrivers.lito.api30","description":"Adreno Graphics Drivers\nGPU drivers for Snapdragon 765G.","removal":"unsafe","type":"misc"},{"id":"com.qualcomm.qti.gpudrivers.sm6150.api30","description":"Adreno Graphics Drivers\nGPU drivers for Snapdragon 675.\nBut actually I have Snapdragon 732G\nit's weird but keep it.","removal":"unsafe","type":"misc"},{"id":"com.qualcomm.qti.gpudrivers.taro.api31","description":"Adreno Graphics Drivers\nGPU drivers","removal":"unsafe","type":"misc"},{"id":"com.qualcomm.qti.improvetouch.service","label":"com.qualcomm.qti.improvetouch.service","description":"A improve touch things was found in the code, but it is not available to users, only to developers.\nThe size of the app is 36.40 KB, so why would this improve touch?\nThis app does not exist on some xiaomi phones, and the touch works the same.","removal":"caution","type":"misc"},{"id":"com.qualcomm.qti.ims","label":"com.qualcomm.qti.ims","description":"Hidden IMS Settings activity not available for users.\nIn this app, you can only turn on this option: Auto reject incoming calls.","removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.loadcarrier","description":"CarrierLoadService\nHave only things like a: Find carrier, carrier switch.\nit's useless.","removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.lpa","label":"com.qualcomm.qti.lpa","description":"lpa = Local Profile Assistants\nRuns on boot, but not in the background beyond that.\nCode has a lot of references to UIM(User Identity Module, which is SIM-related)\nOnly useful if you use an eSIM? (electronic SIM)\nAllows users to choose and change their subscription data when switching between network operators/carriers.","web":["https://developer.qualcomm.com/blog/rise-esims-and-isims-and-their-impact-iot\nhttps://source.android.com/devices/tech/connect/esim-overview"],"removal":"replace","type":"misc"},{"id":"com.qualcomm.qti.modemtestmode","description":"Modem test","removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.networksetting","label":"Network operators","description":"A hidden settings menu that lets you select network modes like GSM only, WCDMA only, LTE only etc, toggle VoLTE On/Off...","removal":"caution","type":"misc"},{"id":"com.qualcomm.qti.optinoverlay","label":"com.qualcomm.qti.optinoverlay","description":"Useless frameworks for com.qualcomm.location.XT it's probably dialogues.\nAnyway it's only legal notices.","removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.perfdump","label":"Perfdump","description":"Performance dump (logging)\nEnable a more accurate overview of the running services (and maybe how much power/RAM they take?)","removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.performancemode","label":"Performance Mode","description":"Hidden Performance Mode activity not available for users.\nIt's on Xiaomi, OnePlus.\nRemoval does not brick performance mode.","removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.poweroffalarm","label":"PowerOffAlarm","description":"Probably what enables alarms to start the device from an off state.\nRuns on boot and when you open a clock app.","removal":"caution","type":"misc"},{"id":"com.qualcomm.qti.powersavemode","description":"It have hidden power saving modes.\nUsers cant use it.","removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.qccauthmgr","label":"QCC-AUTHMGR","description":"It has something to with (com.qualcomm.qti.smq).\nFeedback stuff, something about key: blacklist, download or whatever. All of them look like telemetry, and it's all not available for users.","removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.qccnetstat","description":"useless network statistics also QCC in name qualcomm app is for analytics.","removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.qcolor","label":"QColor","description":"QTI enhanced color mode? I found a png file, color service, it's not needed for any apps.","removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.qcom_accesslogkit","description":"Theres nothing about logs. I only found in receivers (c Broadcast Receiver)\nand some useless png, text files. Useless.","removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.qdcmtpg","description":"layout but not sure for what. a lot useless code, and internet permission","removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.qdma","label":"QDMA","description":"QDMA = Qualcomm Device Management and Analytics.\nA lot of data collections: logs, dropbox, network.","removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.qmmi","label":"QMMI","description":"QMMI is a test app made by Qualcomm. It is used by service center to test the working of the various device components.","web":["https://community.phones.nokia.com/discussion/52566/android-10-on-nokia-8-1/p19\nUseless for end-users."],"removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.qms.service.connectionsecurity","label":"com.qualcomm.qti.qms.service.connectionsecurity","description":"Telemetry service\nqms = quality management service\nBackground-Connection to tls.telemetry.swe.quicinc.com (Host/Domain belongs to Qualcomm)","removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.qms.service.credentials","description":"QMS = Quality Management Service. In code I found logs and QMS spying things.","removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.qms.service.telemetry","label":"Qualcomm Mobile Security","description":"Telemetry service. Obviously phones to Qualcomm.","removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.qms.service.trustzoneaccess","description":"QMS always spying. Trust Zone in this app means nothing more than logs.","removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.qtisettings","label":"com.qualcomm.qti.qtisettings","description":"In code found getting info about device Ram Size, Rom Size, get Wifi Mac Address, is Settings Task Done.\nAlso in app found qtisystemservice code.\nIt looks like the app is for developers only, so it may be removed.","removal":"caution","type":"misc"},{"id":"com.qualcomm.qti.qtisystemservice","label":"com.qualcomm.qti.qtisystemservice","description":"Seems to only log stuff related to telephony?\nA user removed this without noticing any issues.","removal":"replace","type":"misc"},{"id":"com.qualcomm.qti.qwes.AndroidService","description":"qms, qwes it's used for collecting user data.","removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.rcsimsbootstraputil","label":"com.qualcomm.qti.rcsimsbootstraputil","description":"RCS Service\nRCS = Rich Communication Services.\nRCS is a communication protocol between mobile telephone carriers and between phone and carrier, aiming at replacing SMS.\nUses IP protocol so it needs an internet connection.\nIt's a hot mess right now. It aims at being universal but only exists in Samsung Messages and Google Messages, because Google hasn't released a public API yet, so 3rd-party apps can't support it.\nIn a lot of countries messages go through Google's Jibe servers.\nCan anybody check if this is needed for VolTE/VoWifi?","web":["https://en.wikipedia.org/wiki/Rich_Communication_Services","https://jibe.google.com/policies/terms/","https://pocketnow.com/why-you-should-probably-avoid-googles-rcs-text-messaging-chat-feature"],"removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.remoteSimlockAuth","label":"com.qualcomm.qti.remoteSimlockAuth","description":"Enable you to lock/unlock your eSIM remotely.\nSeems more of a security risk to me than anything else.","removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.ridemodeaudio","description":"Ridemode Recording list\nHidden app, not available for users that should be the audio playback of the drive mode.","removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.roamingsettings","label":"Roaming Settings","description":"Hidden settings menu for tweaking roaming settings? How exactly do you access this menu?","removal":"caution","type":"misc"},{"id":"com.qualcomm.qti.seccamservice","label":"SecCamService","description":"Only collects data about camera.","removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.seemp.service","description":"Useless code and it's probably to verify packages.","removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.server.qtiwifi","description":"it's made for analytics.\nThis service can be used to have oem specific feature development. \nCurrently this service is being used to collect CSI data from cfrtool \nvia hidl and then pass the data to application(user level).","removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.server.wigig.tethering.rro","description":"Only found dhcp range? Only the specified lists were found: 192.168. ...\nIt have a lot random of this.\nLooks very unused.","removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.services.secureui","label":"Secure UI Service","description":"It collects your data about screen:gravity, orientation, id, layout_width.\nIt has a Wifi Display Service. QC_WFD\nDoes not run in the background.","removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.services.systemhelper","label":"System Helper Service","description":"Runs \"SysHelperService\" in the background as part of the system.\nPermissions: DEVICE_POWER, READ_PHONE_STATE, READ_PRIVILEGED_PHONE_STATE, RECEIVE_BOOT_COMPLETED, WRITE_SETTINGS, WAKE_LOCK and ACCESS_SURFACE_FLINGER.\nAndroid simple network firewall utility service. On a RedMi Note 13 running HyperOS 2.6.0, no application is running after uninstalling this package.","removal":"caution","type":"misc"},{"id":"com.qualcomm.qti.simcontacts","label":"SimContacts","description":"Hidden Qualcomm sim contacts editor app.","removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.simsettings","label":"com.qualcomm.qti.simsettings","description":"Related to SIM settings? Exact nature is unclear.","removal":"caution","type":"misc"},{"id":"com.qualcomm.qti.smcinvokepkgmgr","description":"This app have nothing.","removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.smq","label":"com.qualcomm.qti.smq","description":"QTR (Qualcomm Technology Reporting)\nRuns on boot.\nSeems like a telemetry package, supposedly sending hardware & software type, configuration and performance data.\nContains a \"QtiFeedbackActivity\" called \"Hardware Feedback\". When that hidden activity is launched through Activity Launcher you get a screen showing just a checkbox and this text:\n\"Collecting hardware and software type, configuration, and performance data helps Qualcomm improve next generation device battery life, security, and performance. Untick to disable.\"\nUnticking isn't remembered; it's ticked again next time you enter. There's also a \"Learn More\" link that leads to: http://reporting.qti.qualcomm.com/learnmore_en.html which doesn't load for me.","removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.telephony.vodafonepack","label":"com.qualcomm.qti.telephony.vodafonepack","description":"Related to Vodafone Prepaid Recharge Plan\nIf you're not a Vodafone client but still has this package on your phone you can delete it.\nFor Vodafone client, I don't know what this package does.","web":["https://en.wikipedia.org/wiki/Vodafone"],"removal":"caution","type":"misc"},{"id":"com.qualcomm.qti.telephonyservice","description":"Sound processing during phonecalls.\nRuns in the background.\nVital package for making calls.","removal":"unsafe","type":"misc"},{"id":"com.qualcomm.qti.uceshimservice","label":"uceShimService","description":"UCE shim service\nUCE = User Capability Exchange. A shim is basically a compatibility layer for an API, that makes sure anything that uses the API does so correctly.\nUsed for RCS. Provides a discovery service for users to see the capabilities of other users.\nUCE is based on SIP PUBLISH and SIP SUBSCRIBE/NOTIFY.\nDevices PUBLISH their capabilities to a presence server, when another device wants to find out what the other party supports, the device sends a SUBSCRIBE to the presence server which then returns a NOTIFY of what the other party supports.","web":["https://fr.wikipedia.org/wiki/Session_Initiation_Protocol"],"removal":"replace","type":"misc"},{"id":"com.qualcomm.qti.uim","label":"com.qualcomm.qti.uim","description":"Runs \"RemoteSimLockService\" in the background.\nThis might be the only remote SIM lock service, just called UIM because R-UIM(Removeable-UserIdentityModule) is a variant of SIM commonly used in Asia.","removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.uimGbaApp","label":"com.qualcomm.qti.uimGbaApp","description":"Contains a \"GbaService\", related to uim card (R-UIM is a type of SIM card mainly used in Asia)","removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.workloadclassifier","label":"com.qualcomm.qti.workloadclassifier","description":"Runs \"WLCService\" in the background.\nI assume this has to do with CPU scheduling. Probably important for efficiency, if not basic operation.\nIt's for performance and security? It categorizes apps maybe for optimization.\nit's named workloadclassifier so it should do that. Reads the list of installed applications, storage space.\nit's still unknown.","removal":"caution","type":"misc"},{"id":"com.qualcomm.qti.xrcb","description":"Receive xrcb network signals for radio side. \nit's about emergency alerts, weather alerts, public announcements, and other information.","removal":"delete","type":"misc"},{"id":"com.qualcomm.qti.xrvd.service","label":"XRVD","description":"The real name of this app is XRVDTest. It has accessibility testing,\nCollects some data? It's something for developers probably.","removal":"delete","type":"misc"},{"id":"com.qualcomm.qtil.aptxacu","description":"Hidden aptxals Audio Bluetooth sample improvement. Useless 96kHz sample.","removal":"delete","type":"misc"},{"id":"com.qualcomm.qtil.aptxals","label":"com.qualcomm.qtil.aptxals","description":"Hidden aptxals Audio Bluetooth sample improvement test.","removal":"delete","type":"misc"},{"id":"com.qualcomm.qtil.aptxalsOverlay","label":"com.qualcomm.qtil.aptxalsOverlay","description":"Overlay for hidden aptxals Audio Bluetooth sample improvement. Useless 96kHz sample.","removal":"delete","type":"misc"},{"id":"com.qualcomm.qtil.aptxui","label":"Bluetooth","description":"Hidden aptxals Audio Bluetooth sample improvement. Useless 96kHz sample.","removal":"delete","type":"misc"},{"id":"com.qualcomm.qtil.btdsda","description":"Hidden bluetooth: answering call, hanging up call, hanging up conference call.\nIf you have dual-sim then you will lose these things? NOTE: disabling this causes audio to come from earpiece regardless of the option selected (Speaker, Earpiece, Bluetooth)","removal":"replace","type":"misc"},{"id":"com.qualcomm.shutdownlistner","description":"uses library com.qualcomm.qcrilhook and logs shutdown phone but it's not needed.","removal":"delete","type":"misc"},{"id":"com.qualcomm.simcontacts","label":"Sim Contacts","description":"Probably handles syncing(exporting/importing) contacts to/from the SIM card. Usually not a feature anybody cares about.","removal":"delete","type":"misc"},{"id":"com.qualcomm.svi","label":"Sunlight Visibility Improvement","description":"SVI Settings. Hidden display enhancement colors not available to users.","removal":"delete","type":"misc"},{"id":"com.qualcomm.timeservice","label":"Qualcomm Time Service","description":"Updates time-services user time offset when user changes time of the day and Android sends a TIME_CHANGED or DATE_CHANGED intents.\nTime-services restores the time of the day after reboot.","web":["https://github.com/bcyj/android_tools_leeco_msm8996/blob/master/time-services/src/com/qualcomm/timeservice/TimeServiceBroadcastReceiver.java"],"removal":"caution","type":"misc"},{"id":"com.qualcomm.uimremoteclient","label":"com.qualcomm.uimremoteclient","description":"Related to uim card (R-UIM is a type of SIM card mainly used in Asia)","removal":"delete","type":"misc"},{"id":"com.qualcomm.uimremoteserver","label":"com.qualcomm.uimremoteserver","description":"Related to uim card (R-UIM is a type of SIM card mainly used in Asia)","removal":"delete","type":"misc"},{"id":"com.qualcomm.wfd.service","label":"Wfd Service","description":"Provides a way to cast your screen to a TV (Miracast). Or is it WiFi Direct?","web":["https://en.wikipedia.org/wiki/Miracast"],"removal":"caution","type":"misc"},{"id":"com.quicinc.cne.CNEService","label":"com.quicinc.cne.CNEService","description":"Qualcomm Connectivity Engine\nEnables seamless hand-off between mobile data and Wi-Fi networks. Can also dynamically measure network performance to prioritize using the best one (I think that's part of \"Intelligently select the best Wi-Fi\" in settings).\nProbably worth keeping on; I noticed connection reliability getting worse when I disabled it.","web":["https://www.qualcomm.com/news/onq/2013/07/02/qualcomms-cne-bringing-smarts-3g4g-wi-fi-seamless-interworking","https://programmersought.com/article/35091829299/"],"removal":"caution","type":"misc"},{"id":"com.quicinc.fmradio","label":"FM Radio","description":"FM Radio app by Qualcomm\nquicinc = Qualcomm Innovation Center","removal":"replace","suggestions":"radios","type":"misc"},{"id":"com.quicinc.voice.activation","label":"Qualcomm Voice Assist","description":"Always-on voice detection, so obviously always runs in the background.\nProbably worth keeping enabled for battery savings if you use Google Assistant regularly while your screen is off.","web":["https://play.google.com/store/apps/details?id=com.quicinc.voice.activation"],"removal":"delete","type":"misc"},{"id":"com.realvnc.android.remote","label":"com.realvnc.android.remote","description":"Remote controle service by Realvnc\nNot sure having a remote control app installed as a system app is a good idea\nNo longer maintained.","web":["https://en.wikipedia.org/wiki/RealVNC","https://www.realvnc.com/en/legal/#privacy"],"removal":"delete","type":"misc"},{"id":"com.redteamobile.roaming","label":"ORoaming","dependencies":["com.redteamobile.roaming.deamon"],"description":"Lets you buy RedTeaMobile data plan to access Internet in foreign country with a virtual SIM card","web":["https://support.oppo.com/uk/answer/?aid=neu9139","https://beta.pithus.org/report/d017d4f6623bf8e71456e6bffe551ef6f3ff3095c62cef3df6d968354898c097"],"removal":"delete","type":"misc"},{"id":"com.redteamobile.roaming.deamon","label":"ORoaming","required_by":["com.redteamobile.roaming"],"description":"Redtea Roaming service deamon for com.redteamobile.roaming","removal":"delete","type":"misc"},{"id":"com.remotefairy4","label":"AnyMote Universal Remote + Wifi Smart Home Control","description":"IR Remote control app\nIt has lots of trackers and permissions","web":["https://play.google.com/store/apps/details?id=com.remotefairy4","https://reports.exodus-privacy.eu.org/en/reports/com.remotefairy4/latest/"],"removal":"delete","type":"misc"},{"id":"com.republicwireless.tel","label":"Republic","description":"Lets you manage your Republic wireless account.\nRepublic Wireless is an american mobile virtual network operator","web":["https://play.google.com/store/apps/details?id=com.republicwireless.tel&hl","https://en.wikipedia.org/wiki/Republic_Wireless"],"removal":"delete","type":"misc"},{"id":"com.rhapsody","label":"Napster","description":"Napster music streaming app","web":["https://play.google.com/store/apps/details?id=com.rhapsody","https://en.wikipedia.org/wiki/Napster","http://napster.com/privacy"],"removal":"replace","suggestions":"music_apps","type":"misc"},{"id":"com.rhapsody.vpl","label":"Napster Music","description":"Napster music streaming app","web":["https://play.google.com/store/apps/details?id=com.rhapsody","https://en.wikipedia.org/wiki/Napster","http://napster.com/privacy"],"removal":"replace","suggestions":"music_apps","type":"misc"},{"id":"com.rlk.weathers","label":"Weather","description":"Weather app with ads and trackers. Can access phone calls and SMS.","web":["https://play.google.com/store/apps/details?id=com.rlk.weathers","https://beta.pithus.org/report/c3fa30c66192c458f93456401421d3c74f9122191b561781af142c42c24fe603"],"removal":"replace","type":"misc"},{"id":"com.roaming.android.gsimbase","label":"com.roaming.android.gsimbase","description":"gsim = Global SIM? (SIM = Subscriber Identity Module, as in SIM-card)\nConsidering the \"roaming\" context that's my best guess.","removal":"replace","type":"misc"},{"id":"com.roaming.android.gsimcontentprovider","label":"com.roaming.android.gsimcontentprovider","description":"gsim = Global SIM? (SIM = Subscriber Identity Module, as in SIM-card)\nConsidering the \"roaming\" context that's my best guess.","removal":"replace","type":"misc"},{"id":"com.s.antivirus","label":"AVG Protection","description":"AVG Antivirus for Sony Xperia.","web":["https://play.google.com/store/apps/details?id=com.s.antivirus"],"removal":"delete","type":"misc"},{"id":"com.sec.app.samsungprintservice","description":"Samsung Print Service Plugin.\nPublished by HP, see:\nhttps://play.google.com/store/apps/details?id=com.sec.app.samsungprintservice","removal":"delete","type":"misc"},{"id":"com.sem.factoryapp","label":"com.sem.factoryapp","description":"SEMFactoryApp\nCall home (172.217.168.14 --> Google IP). Needs NFC permission.\nThis package is maybe used to test NFC.","removal":"delete","type":"misc"},{"id":"com.servicemagic.consumer","label":"Angi","description":"HomeAdvisor: Contractors for Home Improvement\nHelps you find local contractors from the service Home Advisor network\nHomeAdvisor collects users data when a request is made and then sells that data to local contractors in exchange for money.","web":["https://play.google.com/store/apps/details?id=com.servicemagic.consumer","https://en.wikipedia.org/wiki/HomeAdvisor#Critism"],"removal":"delete","type":"misc"},{"id":"com.setk.widget","label":"Bizz","description":"Recommands you stuff to do/buy in your nearby area","web":["https://play.google.com/store/apps/details?id=com.setk.widget"],"removal":"delete","type":"misc"},{"id":"com.sharecare.askmd","label":"AskMD","description":"Symptom checker app provided by Sharecare [Discontinued]\nLets you see what might be causing your symptoms and helps you find a nearby physician","web":["https://en.wikipedia.org/wiki/Sharecare#Criticisms"],"removal":"delete","type":"misc"},{"id":"com.shopee.br","label":"Shopee","description":"An e-commerce online shopping platform for Brazil.","web":["https://play.google.com/store/apps/details?id=com.shopee.br"],"removal":"delete","type":"misc"},{"id":"com.shopee.id","label":"Shopee","description":"An e-commerce online shopping platform for Indonesia.","web":["https://play.google.com/store/apps/details?id=com.shopee.id"],"removal":"delete","type":"misc"},{"id":"com.skype.m2","label":"Skype Lite","description":"Lite version of Skype\nVoice & video calling app","removal":"replace","suggestions":"meeting_apps","type":"misc"},{"id":"com.skype.raider","label":"Skype","description":"Voice & video calling app","web":["https://play.google.com/store/apps/details?id=com.skype.raider"],"removal":"replace","suggestions":"meeting_apps","type":"misc"},{"id":"com.slacker.radio","label":"LiveOne","description":"Music and Live Events streaming app","web":["https://play.google.com/store/apps/details?id=com.slacker.radio","https://www.liveone.com/privacy"],"removal":"replace","suggestions":"music_apps","type":"misc"},{"id":"com.smithmicro.netwise.director.comcast.oem","label":"XFINITY WiFi Settings","description":"Auto-connects you to XFINITY WiFi hotspot.\nXFINITY is a subsidiary of the Comcast Corporation","web":["https://play.google.com/store/apps/details?id=com.smithmicro.netwise.director.comcast.oem","https://en.wikipedia.org/wiki/Xfinity"],"removal":"delete","type":"misc"},{"id":"com.snapchat.android","label":"Snapchat","description":"Snapchat is a social media app that allows users to share photos and videos that disappear after a short period of time.","web":["https://play.google.com/store/apps/details?id=com.snapchat.android"],"removal":"delete","type":"misc"},{"id":"com.spotify.music","label":"Spotify","description":"Popular music streaming app","web":["https://play.google.com/store/apps/details?id=com.spotify.music","https://www.spotify.com/privacy/"],"removal":"replace","suggestions":"music_apps","type":"misc"},{"id":"com.staplegames.blocksClassicSGGP","label":"Blocks","description":"Preinstalled game on some Samsung phones.\nExodus report: 9 permissions, 26 trackers","web":["https://play.google.com/store/apps/details?id=com.staplegames.blocksClassicSGGP","https://reports.exodus-privacy.eu.org/reports/com.staplegames.blocksClassicSGGP/latest/"],"removal":"delete","type":"misc"},{"id":"com.swiftkey.swiftkeyconfigurator","label":"SwiftKey factory settings","description":"Used by commercial swiftkey partners to configure the SwiftKey app.\nSwiftkey is a keyboard developed by TouchType, a Microsoft subsidiary","web":["https://en.wikipedia.org/wiki/SwiftKey"],"removal":"delete","type":"misc"},{"id":"com.talpa.hibrowser","label":"Hi Browser","description":"Awful browser with embedded trackers and ads.","web":["https://play.google.com/store/apps/details?id=com.talpa.hibrowser","https://reports.exodus-privacy.eu.org/fr/reports/com.talpa.hibrowser/latest/"],"removal":"replace","suggestions":"browsers","type":"misc"},{"id":"com.talpa.share","label":"XShare Mini","description":"File Sharing App (via Bluetooth) with Google and Facebook trackers.\nAsks for a lot of permissions including ACCESS_FINE_LOCATION, REQUEST_INSTALL_PACKAGES.","web":["https://beta.pithus.org/report/949bf802e335ad0db47b1551cde46af2b2ef13da4b38be969c60c9439b94f05b"],"removal":"delete","type":"misc"},{"id":"com.telenav.app.android.cingular","label":"AT&T Navigator","description":"Crappy GPS app provided by Telenav and rebranded by AT&T.","web":["https://play.google.com/store/apps/details?id=com.telenav.app.android.cingular","https://www.telenav.com/legal/policies-privacy-policy"],"removal":"replace","suggestions":"maps","type":"misc"},{"id":"com.telenav.app.android.scout_us","label":"Scout","description":"Bad GPS with on top of that bad chat features (USA only)","web":["https://play.google.com/store/apps/details?id=com.telenav.app.android.scout_us","https://www.scoutgps.com/"],"removal":"replace","suggestions":"maps","type":"misc"},{"id":"com.til.timesnews","label":"Newspoint","description":"India specific news app","web":["https://play.google.com/store/apps/details?id=com.til.timesnews","http://www.newspointapp.com/policy.cms"],"removal":"replace","suggestions":"rss_readers","type":"misc"},{"id":"com.tiqiaa.remote","label":"Zaza Remote","description":"A Universal infrared control app full of trackers and with unecessary permissions.","web":["https://play.google.com/store/apps/details?id=com.tiqiaa.remote","https://beta.pithus.org/report/93eed47a45c00998f2111907afc26b5697aaf7fb19c0efb6b42d46addf0e297c"],"removal":"delete","type":"misc"},{"id":"com.tmobile.echolocate","label":"T-Mobile Diagnostics","description":"More info needed","removal":"delete","type":"misc"},{"id":"com.touchtype.swiftkey","label":"Swiftkey","description":"Keyboard app by TouchType, a Microsoft subsidiary.\n4 Trackers + 11 Permissions","web":["https://en.wikipedia.org/wiki/SwiftKey","https://play.google.com/store/apps/details?id=com.touchtype.swiftkey","https://reports.exodus-privacy.eu.org/en/reports/com.touchtype.swiftkey/latest/"],"removal":"replace","warning":"Default keyboard on some Nokia and Huawei phones. Make sure you have another keyboard app before disabling this.","suggestions":"keyboards","type":"misc"},{"id":"com.touchtype.swiftkey.res.overlay","label":"com.touchtype.swiftkey.res.overlay","description":"Some overlay for Swiftkey? Overlays are usually themes, but not sure about this one.","removal":"caution","type":"misc"},{"id":"com.tracker.t","label":"com.tracker.t","description":"Given its name I think you can take the risk to delete it.","removal":"delete","type":"misc"},{"id":"com.tripadvisor.tripadvisor","label":"Tripadvisor","description":"Travel Guidance app","web":["https://play.google.com/store/apps/details?id=com.tripadvisor.tripadvisor","https://en.wikipedia.org/wiki/Tripadvisor#Controversies","https://nypost.com/2016/03/01/why-you-should-never-ever-trust-tripadvisor/"],"removal":"delete","type":"misc"},{"id":"com.tripledot.solitaire","label":"Solitaire","description":"Preinstalled game on some Samsung phones. 30 permissions, 23 trackers","web":["https://play.google.com/store/apps/details?id=com.tripledot.solitaire","https://reports.exodus-privacy.eu.org/reports/com.tripledot.solitaire/latest/"],"removal":"delete","type":"misc"},{"id":"com.tripledot.woodoku","label":"Woodoku","description":"A wood block puzzle game\nPreinstalled game on some Samsung phones. 28 permissions, 24 trackers","web":["https://play.google.com/store/apps/details?id=com.tripledot.woodoku","https://reports.exodus-privacy.eu.org/reports/com.tripledot.woodoku/latest/"],"removal":"delete","type":"misc"},{"id":"com.trustonic.teeservice","label":"TeeService","description":"TEE = Trusted Execution Environment","removal":"caution","type":"misc"},{"id":"com.trustonic.tuiservice","label":"Trusted User Interface","description":"A security layer by Trustonic.\nAllows a \"Trusted App\" to interact directly with the user, completely isolated from the device OS.\nIt's closed source and normal devs can't use it for their apps.\nMainly used by OEM apps like Samsung Pay and for DRM.\nGoogle implemented their own TUI in Android Pie: https://android-developers.googleblog.com/search/label/Trusted%20User%20Interface\nhttps://www.trustonic.com/news/blog/benefits-trusted-user-interface/\nDisabling will break \"Trusted Apps\".","web":["https://stackoverflow.com/questions/16909576/how-to-make-use-of-arm-trust-zone-in-android-application","https://en.wikipedia.org/wiki/Trusted_execution_environment","https://en.wikipedia.org/wiki/ARM_architecture#Security_extensions","https://googleprojectzero.blogspot.com/2017/07/trust-issues-exploiting-trustzone-tees.html","https://medium.com/@nimronagy/arm-trustzone-on-android-975bfe7497d2","https://www.synacktiv.com/posts/exploit/kinibi-tee-trusted-application-exploitation.html","https://blog.quarkslab.com/introduction-to-trusted-execution-environment-arms-trustzone.html","https://medium.com/taszksec/unbox-your-phone-part-i-331bbf44c30c","nhttps://www.gsd.inesc-id.pt/~nsantos/papers/pinto_acsur19.pdf"],"removal":"caution","type":"misc"},{"id":"com.turner.cnvideoapp","label":"CN","description":"Award winning Cartoon Network App","web":["https://play.google.com/store/apps/details?id=com.turner.cnvideoapp","http://www.cartoonnetwork.com/legal/privacy/mobile.html"],"removal":"delete","type":"misc"},{"id":"com.ubercab","label":"Uber","description":"Uber does not protect personal user data and has a questionable ethic.","web":["https://play.google.com/store/apps/details?id=com.ubercab","https://en.wikipedia.org/wiki/Uber#Criticism"],"removal":"delete","type":"misc"},{"id":"com.ubercab.driver","label":"Uber - Driver","description":"Uber does not protect personal user data and has a questionable ethic.","web":["https://play.google.com/store/apps/details?id=com.ubercab.driver","https://en.wikipedia.org/wiki/Uber#Criticism"],"removal":"delete","type":"misc"},{"id":"com.ubercab.eats","label":"Uber Eats","description":"Uber does not protect personal user data and has a questionable ethic.","web":["https://play.google.com/store/apps/details?id=com.ubercab.eats","https://en.wikipedia.org/wiki/Uber#Criticism"],"removal":"delete","type":"misc"},{"id":"com.ume.browser.northamerica","label":"UME Web Browser","description":"It has trackers and a LOT of unnecessary permissions.","web":["https://play.google.com/store/apps/details?id=com.ume.browser.northamerica","https://reports.exodus-privacy.eu.org/en/reports/com.ume.browser.cust/latest/"],"removal":"replace","suggestions":"browsers","type":"misc"},{"id":"com.wb.goog.got.conquest","label":"Game of Thrones: Conquest","description":"Official GOT strategy building game","web":["https://play.google.com/store/apps/details?id=com.wb.goog.got.conquest"],"removal":"delete","type":"misc"},{"id":"com.whatsapp","label":"WhatsApp","description":"Popular messaging app from Meta. Requires Google Play Services to receive messages in the background.","web":["https://play.google.com/store/apps/details?id=com.whatsapp","https://en.wikipedia.org/wiki/WhatsApp#Controversies_and_criticism"],"removal":"replace","type":"misc"},{"id":"com.wing.wtsarcontrol","description":"Sar control, Chinese App, not available for normal users.","removal":"delete","type":"misc"},{"id":"com.wingtech.sartest","description":"SarTest\nSecret code: 332.","removal":"delete","type":"misc"},{"id":"com.yahoo.mobile.client.android.liveweather","label":"Weather","description":"All of their services are crappy so it's not so difficult to boycott","web":["https://play.google.com/store/apps/details?id=com.yahoo.mobile.client.android.weather","https://en.wikipedia.org/wiki/Criticism_of_Yahoo!"],"removal":"replace","type":"misc"},{"id":"com.yellowpages.android.ypmobile","label":"Yellow Pages","description":"Business related app","web":["https://play.google.com/store/apps/details?id=com.yellowpages.android.ypmobile"],"removal":"delete","type":"misc"},{"id":"com.yelp.android","label":"Yelp","description":"Yelp lets users post reviews and rate businesses.","web":["https://play.google.com/store/apps/details?id=com.yelp.android","https://en.wikipedia.org/wiki/Yelp#Controversy_and_litigation"],"removal":"delete","type":"misc"},{"id":"com.zaz.translate","label":"Hi Translate","description":"Bloated translation app with a lot of trackers and permissions\nIt uses the Google Translate API for the translations.","web":["https://play.google.com/store/apps/details?id=com.zaz.translate","https://beta.pithus.org/report/fdd787d96c3e069f983320c84c32fc6b8cdf205df17244d190b181edf0c14f68"],"removal":"replace","suggestions":"translators","type":"misc"},{"id":"com.zhiliaoapp.musically","label":"TikTok","description":"The TikTok app","web":["https://play.google.com/store/apps/details?id=com.zhiliaoapp.musically","https://en.wikipedia.org/wiki/TikTok#Privacy_and_security_concerns"],"removal":"delete","type":"misc"},{"id":"de.axelspringer.yana.zeropage","label":"upday","description":"News app preinstalled on some smartphones","web":["https://play.google.com/store/apps/details?id=de.axelspringer.yana.zeropage"],"removal":"replace","suggestions":"rss_readers","type":"misc"},{"id":"flipboard.app","label":"Flipboard","description":"Flipboard News App","web":["https://play.google.com/store/apps/details?id=flipboard.app","https://about.flipboard.com/privacy-policy/","https://privacy.commonsense.org/privacy-report/Flipboard"],"removal":"replace","suggestions":"rss_readers","type":"misc"},{"id":"flipboard.boxer.app","label":"Briefing","description":"Briefing app from Flipboard","web":["https://play.google.com/store/apps/details?id=flipboard.boxer.app","https://about.flipboard.com/privacy-policy/"],"removal":"replace","suggestions":"rss_readers","type":"misc"},{"id":"id.co.babe","label":"BaBe","description":"Indonesian news app","web":["https://play.google.com/store/apps/details?id=id.co.babe"],"removal":"replace","suggestions":"rss_readers","type":"misc"},{"id":"in.amazon.mShop.android.shopping","label":"Amazon","description":"Amazon shopping app for India","web":["https://play.google.com/store/apps/details?id=in.amazon.mShop.android.shopping"],"removal":"delete","type":"misc"},{"id":"in.mohalla.sharechat","label":"ShareChat","description":"Multilingual India specific social media platform","web":["https://play.google.com/store/apps/details?id=in.mohalla.sharechat","https://en.wikipedia.org/wiki/ShareChat","https://help.sharechat.com/policies/privacy-policy/"],"removal":"delete","type":"misc"},{"id":"in.playsimple.tripcross","label":"CrossWord Jam","description":"Preinstalled game on some Samsung phones. 12 permissions, 25 trackers","web":["https://play.google.com/store/apps/details?id=in.playsimple.tripcross","https://reports.exodus-privacy.eu.org/reports/in.playsimple.tripcross/latest/"],"removal":"delete","type":"misc"},{"id":"in.playsimple.wordtrip","label":"Word Trip","description":"Word Count & word streak puzzle game. 19 trackers, 11 permissions","web":["https://play.google.com/store/apps/details?id=in.playsimple.wordtrip","https://reports.exodus-privacy.eu.org/en/reports/in.playsimple.wordtrip/latest/"],"removal":"delete","type":"misc"},{"id":"jp.co.omronsoft.openwnn","label":"jp.co.omronsoft.openwnn","description":"Japanese keyboard/IME (don't know why it's pre-installed on US/european devices)\nNote : IME = input method editor","removal":"replace","warning":"Make sure you've another one installed before you disable.","suggestions":"keyboards","type":"misc"},{"id":"jp.gocro.smartnews.android","label":"SmartNews","description":"Delivers the top trending news stories from other publishers (NBC News, The Verges etc...)\nIncludes 7 Trackers + 10 permissions","web":["https://play.google.com/store/apps/details?id=jp.gocro.smartnews.android","https://reports.exodus-privacy.eu.org/en/reports/jp.gocro.smartnews.android/latest/"],"removal":"replace","suggestions":"rss_readers","type":"misc"},{"id":"msgplus.jibe.sca.vpl","label":"Messaging Plus","description":"Messings using the RCS protocol\nRelated to Google Jibe (https://jibe.google.com/)","web":["https://en.wikipedia.org/wiki/Rich_Communication_Services"],"removal":"delete","type":"misc"},{"id":"net.bat.store","label":"AHA Games","description":"Mobile game store.\nFull of trackers and has CAMERA and RECORD_AUDIO permissions. Displays intrusive game ads on HIOS launcher and random popups.","web":["https://beta.pithus.org/report/f5346d1388aff293bc84b481c3a9823cc3bf76ffc241fcf455754b86028f22b9"],"removal":"delete","type":"misc"},{"id":"net.fetnet.fetvod","description":"Streaming service\nRegistered members can watch the movies, dramas, variety shows and animations\nhttps://play.google.com/store/apps/details?id=net.fetnet.fetvod","removal":"delete","type":"misc"},{"id":"net.sharewire.parkmobilev2","label":"ParkMobile","description":"Smarter way to park and reserve your spot ahead of time","web":["https://play.google.com/store/apps/details?id=net.sharewire.parkmobilev2"],"removal":"delete","type":"misc"},{"id":"net.supertreat.solitaire","label":"Solitaire","description":"Preinstalled game on some Samsung phones. 8 permissions, 17 trackers","web":["https://play.google.com/store/apps/details?id=net.supertreat.solitaire","https://reports.exodus-privacy.eu.org/reports/net.supertreat.solitaire/latest/"],"removal":"delete","type":"misc"},{"id":"org.codeaurora.bluetooth","label":"Bluetooth extensions","description":"More info needed","web":["https://source.codeaurora.org/quic/la/platform/vendor/qcom-opensource/bluetooth"],"removal":"caution","type":"misc"},{"id":"org.codeaurora.gps.gpslogsave","label":"org.codeaurora.gps.gpslogsave","description":"More info needed","removal":"delete","type":"misc"},{"id":"org.codeaurora.ims","label":"org.codeaurora.ims","description":"IMS(Ip Multimedia Subsystem) is an open industry standard for voice and multimedia communications over packet-based IP networks (VoLTE/VoIP/Wifi calling).\nRuns in the background as part of the system, with Google's IMS(com.google.android.ims, \"Carrier Services\") disabled, I haven't checked if it'd run with Carrier Services enabled.","removal":"caution","type":"misc"},{"id":"org.simalliance.openmobileapi.service","label":"SmartcardService","description":"The SmartCard API is a reference implementation of the SIMalliance Open Mobile API specification that enables Android applications to communicate with Secure Elements, (SIM card, embedded Secure Elements, Mobile Security Card or others)\nSafe to remove if you think you don't need this","web":["https://github.com/seek-for-android/pool/wiki/SmartcardAPI"],"removal":"caution","type":"misc"},{"id":"org.simalliance.openmobileapi.uicc1terminal","description":"Open Mobile API (\"interface\") to access UICC secure elements\nUICC stands for Universal Integrated Circuit Card.\nIt is a the physical and logical platform for the USIM and may contain additional USIMs and other applications.\n(U)SIM is an application on the UICC.\nhttps://bluesecblog.wordpress.com/2016/11/18/uicc-sim-usim/\nGood read: https://arxiv.org/ftp/arxiv/papers/1601/1601.03027.pdf\nNote2: The term SIM is widely used in the industry and especially with consumers to mean both SIMs and UICCs.\nhttps://www.justaskgemalto.com/us/what-uicc-and-how-it-different-sim-card/\n","removal":"replace","type":"misc"},{"id":"org.simalliance.openmobileapi.uicc2terminal","label":"org.simalliance.openmobileapi.uicc2terminal","description":"Open Mobile API (\"interface\") to access UICC secure elements.\nUICC stands for Universal Integrated Circuit Card.\nIt is the physical and logical platform for the USIM and may contain additional USIMs and other applications.\n(U)SIM is an application on the UICC.\nNote: The term SIM is widely used in the industry and especially with consumers to mean both SIMs and UICCs.","web":["https://bluesecblog.wordpress.com/2016/11/18/uicc-sim-usim/","https://arxiv.org/ftp/arxiv/papers/1601/1601.03027.pdf","https://www.justaskgemalto.com/us/what-uicc-and-how-it-different-sim-card/"],"removal":"caution","type":"misc"},{"id":"pl.zdunex25.updater","label":"pl.zdunex25.updater","description":"Updater for the zdnex25's theme","web":["https://www.deviantart.com/zdunex25/gallery/26889741/themes"],"removal":"delete","type":"misc"},{"id":"tv.fubo.mobile.vpl","label":"fuboTV","description":"Lets you Watch live Sports, TV Shows, Movies & News","web":["https://play.google.com/store/apps/details?id=tv.fubo.mobile"],"removal":"replace","suggestions":"streaming_apps","type":"misc"},{"id":"tv.peel.app","label":"Peel Universal Smart TV Remote Control","description":"Lets you remotely control devices like your TV, DVD or Blu-ray player (Discontinued)","removal":"delete","type":"misc"},{"id":"vendor.qti.data.txpwradmin","description":"This app is from qualcomm, in androidmanifest.xml I found things like com.qualcomm.qti.qmsdataservices, qms is spyware,\npermissions uses: access wifi state, quary all packages, package usage stats.\nSo hidden network stats, more about it is this code is just logs, takes apm, wifi, bt status.","removal":"replace","type":"misc"},{"id":"vendor.qti.hardware.cacert.server","label":"CACertApp","description":"Occasionally runs in the background.\nHandles CACert certificates?\nCACert is a community-driven CA that issues certificates to the public at large for free. CA = Certificate Authority, an entity that certifies the ownership of a public key that can be used for secure communications.","web":["http://www.cacert.org/","https://en.wikipedia.org/wiki/CAcert.org"],"removal":"unsafe","warning":"Probably a bad idea to disable; could mess with device security.","type":"misc"},{"id":"vendor.qti.imsdatachannel","description":"Needed for IMS.","removal":"replace","type":"misc"},{"id":"vendor.qti.imsrcs","description":"IMS(Ip Multimedia Subsystem) is an open industry standard for voice and multimedia communications over packet-based IP networks (VoLTE/VoIP/Wifi calling). Also have RCS.","removal":"caution","type":"misc"},{"id":"vendor.qti.iwlan","description":"Used for VoLTE/VoWifi (Wifi-calling)\nIwLAN = Interworking wLAN.\nSupport for mobile data offloading (use of complementary network technologies for delivering data originally targeted for cellular networks)\nIt means your phone will use the Wi-Fi connection instead of the cellular data connection.\nhttps://en.wikipedia.org/wiki/Mobile_data_offloading","removal":"caution","type":"misc"},{"id":"vendor.qti.qesdk.sysservice","description":"It's for debugging and logs. But in the code I see VDebug. Vivo probably uses Qualcomm for some features.","removal":"delete","type":"misc"},{"id":"zpub.res","label":"zpub.res","description":"Third-party app pre-installed on ZTE phones.","removal":"delete","type":"misc"},{"id":"android.autoinstalls.config.Nothing.Pong","description":"PlayAutoInstalls\nAutoInstalls a set of OEM apps on device setup (first boot/factory reset).","removal":"delete","type":"oem"},{"id":"android.autoinstalls.config.TCL.PAI","description":"AutoInstalls a set of OEM apps on device setup (first boot/factory reset).","removal":"delete","type":"oem"},{"id":"android.autoinstalls.config.Xiaomi.cactus","description":"Cactus is the device codename.\nAutoInstalls a set of OEM apps on device setup (first boot/factory reset).","removal":"delete","type":"oem"},{"id":"android.autoinstalls.config.Xiaomi.cepheus","description":"Cepheus is the device codename.\nAutoInstalls a set of OEM apps on device setup (first boot/factory reset).","removal":"delete","type":"oem"},{"id":"android.autoinstalls.config.Xiaomi.cereus","description":"Cepheus is the device codename.\nAutoInstalls a set of OEM apps on device setup (first boot/factory reset). For example, Personal Activity Intelligence.\nAn algorithm that determines a personal activity index based on resting heart rate, heart condition during exercise, gender, weight, and age. Requires smart bracelet to work.","removal":"delete","type":"oem"},{"id":"android.autoinstalls.config.Xiaomi.daisy","description":"Daisy is the device codename.\nAutoInstalls a set of OEM apps on device setup (first boot/factory reset).","removal":"delete","type":"oem"},{"id":"android.autoinstalls.config.Xiaomi.dipper","description":"Dipper is the device codename.\nAutoInstalls a set of OEM apps on device setup (first boot/factory reset).","removal":"delete","type":"oem"},{"id":"android.autoinstalls.config.Xiaomi.land","description":"land is the device codename.\nAutoInstalls a set of OEM apps on device setup (first boot/factory reset).","removal":"delete","type":"oem"},{"id":"android.autoinstalls.config.Xiaomi.model","description":"PlayAutoInstalls\nAutoInstalls a set of OEM apps on device setup (first boot/factory reset).","removal":"delete","type":"oem"},{"id":"android.autoinstalls.config.Xiaomi.qssi","description":"PlayAutoInstalls\nAutoInstalls a set of OEM apps on device setup (first boot/factory reset).","removal":"delete","type":"oem"},{"id":"android.autoinstalls.config.Xiaomi.willow","description":"PlayAutoInstalls\nAutoInstalls a set of OEM apps on device setup (first boot/factory reset).","removal":"delete","type":"oem"},{"id":"android.autoinstalls.config.asus.pai","description":"PlayAutoInstalls\nAutoInstalls a set of OEM apps on device setup (first boot/factory reset).","removal":"delete","type":"oem"},{"id":"android.autoinstalls.config.google.sabrina","description":"AutoInstalls a set of OEM apps on device setup (first boot/factory reset).","removal":"delete","type":"oem"},{"id":"android.autoinstalls.config.lge.device","description":"PlayAutoInstalls\nAutoInstalls a set of OEM apps on device setup (first boot/factory reset).","removal":"delete","type":"oem"},{"id":"android.autoinstalls.config.motorola.layout","label":"Device configuration","description":"AutoInstalls a set of OEM apps on device setup (first boot/factory reset).\nA layout?","removal":"delete","type":"oem"},{"id":"android.autoinstalls.config.oneplus","label":"Device configuration","description":"Device configuration\nAutoInstalls a set of OEM apps on device setup (first boot/factory reset).","removal":"delete","type":"oem"},{"id":"android.autoinstalls.config.oppo","description":"PlayAutoInstalls\nAutoInstalls a set of OEM apps on device setup (first boot/factory reset).","removal":"delete","type":"oem"},{"id":"android.autoinstalls.config.samsung","description":"AutoInstalls a set of OEM apps on device setup (first boot/factory reset).","removal":"delete","type":"oem"},{"id":"android.autoinstalls.config.transsion.device","description":"PlayAutoInstalls\nAutoInstalls a set of OEM apps on device setup (first boot/factory reset).","removal":"delete","type":"oem"},{"id":"android.autoinstalls.config.vivo.devices","description":"PlayAutoInstalls\nAutoInstalls a set of OEM apps on device setup (first boot/factory reset).","removal":"delete","type":"oem"},{"id":"android.autoinstalls.config.xioami.mibox3","description":"AutoInstalls a set of OEM apps on device setup (first boot/factory reset).","removal":"delete","type":"oem"},{"id":"android.connectivity.base.overlay","description":"CaptivePortalLogin overlay.\nOverlays work by mapping resources defined in the overlay package to resources defined in the target package. When an app attempts to resolve the value of a resource in the target package, the value of the overlay resource the target resource is mapped to is returned instead.\nhttps://source.android.com/docs/core/runtime/rros","removal":"caution","type":"oem"},{"id":"android.ext.services","description":"Looks like useless BUT you risk bootloop phone.","removal":"unsafe","type":"oem"},{"id":"android.framework.res.rro.oneplus","description":"A runtime resource overlay (RRO) is a package that changes the resource values of a target package at runtime. For example, an app installed on the system image might change its behavior based upon the value of a resource. Rather than hardcoding the resource value at build time, an RRO installed on a different partition can change the values of the app's resources at runtime.\nRROs can be enabled or disabled. You can programmatically set the enable/disable state to toggle an RRO's ability to change resource values. RROs are disabled by default (however, static RROs are enabled by default).\nhttps://source.android.com/docs/core/runtime/rros","removal":"caution","type":"oem"},{"id":"android.frameworkres.overlay.Network","description":"Network overlay.\nOverlays work by mapping resources defined in the overlay package to resources defined in the target package. When an app attempts to resolve the value of a resource in the target package, the value of the overlay resource the target resource is mapped to is returned instead.\nhttps://source.android.com/docs/core/runtime/rros","removal":"unsafe","type":"oem"},{"id":"android.frameworkres.overlay.display.product","description":"A runtime resource overlay (RRO) is a package that changes the resource values of a target package at runtime. For example, an app installed on the system image might change its behavior based upon the value of a resource. Rather than hardcoding the resource value at build time, an RRO installed on a different partition can change the values of the app's resources at runtime.\nRROs can be enabled or disabled. You can programmatically set the enable/disable state to toggle an RRO's ability to change resource values. RROs are disabled by default (however, static RROs are enabled by default).\nhttps://source.android.com/docs/core/runtime/rros","removal":"caution","type":"oem"},{"id":"android.miui.home.launcher.res","description":"Config to default icons/placeholder widgets in MIUI launcher.\nIt's used only first time run MIUI launcher app like com.mi.globallayout.","removal":"replace","type":"oem"},{"id":"android.miui.overlay","description":"Refers to a specific package within the MIUI overlay,\nThe package contains various resources and components that are used to customize the appearance and behavior of the MIUI interface.\nhttps://source.android.com/docs/core/runtime/rros","removal":"unsafe","type":"oem"},{"id":"android.miui.poco.launcher.res","description":"Config to default icons/placeholder widgets in MIUI launcher.\nIt's used only first time run MIUI launcher app like com.mi.globallayout.","removal":"replace","type":"oem"},{"id":"android.overlay.common.all_devices","description":"This overlay has got useless configs (?)","removal":"caution","type":"oem"},{"id":"android.overlay.common.pong","description":"Brightness, screen configs. Better don't risk","removal":"unsafe","type":"oem"},{"id":"android.overlay.common.spacewar","description":"Brightness, screen configs. Better don't risk","removal":"unsafe","type":"oem"},{"id":"android.overlay.dynamicNavBar","description":"Found only in the bools.xml code, which shows the 'config Support dynamic Navigation Bar' 'true'.","removal":"caution","type":"oem"},{"id":"android.overlay.gms.region.all","description":"Useless overlay to setupwizard things","removal":"delete","type":"oem"},{"id":"android.overlay.gms.region.eea","description":"Useless overlay to setupwizard things","removal":"delete","type":"oem"},{"id":"android.overlay.gms.region.ind","description":"Useless overlay to setupwizard things","removal":"delete","type":"oem"},{"id":"android.overlay.gms.region.row","description":"Useless overlay to setupwizard things","removal":"delete","type":"oem"},{"id":"android.overlay.multiuser","description":"Maximum multi user config.","removal":"caution","type":"oem"},{"id":"android.overlay.navbar","description":"Show Navigation Bar Config.","removal":"caution","type":"oem"},{"id":"android.overlay.vivoresrro","description":"This overlay has only: strings.xml: select_cancel.","removal":"delete","type":"oem"},{"id":"android.overlay.vrro","description":"This overlay has display configs","removal":"unsafe","type":"oem"},{"id":"android.romstats","description":"Misleading package name. This is a Xiaomi-only package.\nCan someone provide the .apk?\nTelemetry stuff\n","removal":"delete","type":"oem"},{"id":"android.rro_product","description":"Better don't touch it.","removal":"unsafe","type":"oem"},{"id":"android.telephony.overlay.cmcc","description":"Likely overlay themes from China Mobile Communications Corporation(CMCC) or China Telecom(CT).","removal":"caution","type":"oem"},{"id":"androidhwext","description":"EMUI Graphical User Interface:\nhttps://fixyourandroid.com/about/androidhwext\nThe framework responsible for the themes (even the standard one) of EMUI to launch. Also, it's the core for the Themes app. REMOVING IT CAUSES A BOOTLOOP!","removal":"unsafe","type":"oem"},{"id":"androidx.camera.extensions.impl","description":"Camera extensions for Huawei like Tags,HDR effects\nand other things to video, photos.","removal":"replace","type":"oem"},{"id":"aon.frameworkres.overlay.display.product","description":"A runtime resource overlay (RRO) is a package that changes the resource values of a target package at runtime. For example, an app installed on the system image might change its behavior based upon the value of a resource. Rather than hardcoding the resource value at build time, an RRO installed on a different partition can change the values of the app's resources at runtime.\nRROs can be enabled or disabled. You can programmatically set the enable/disable state to toggle an RRO's ability to change resource values. RROs are disabled by default (however, static RROs are enabled by default).\nhttps://source.android.com/docs/core/runtime/rros","removal":"caution","type":"oem"},{"id":"asusims.overlay.aosp.documentsui","description":"Not needed overlay for hide documentsui icon. No effects after remove.","removal":"delete","type":"oem"},{"id":"asusims.overlay.common","description":"It have important configs better dont risk.\nImportant configs to android.","removal":"unsafe","type":"oem"},{"id":"asusims.overlay.documentsui","description":"Not needed overlay for hide documentsui icon. No effects after remove.","removal":"delete","type":"oem"},{"id":"blibli.mobile.commerce","description":"Blibli Belanja Online Mall\nChinese payment blipay\nhttps://play.google.com/store/apps/details?id=blibli.mobile.commerce","removal":"delete","type":"oem"},{"id":"cn.nubia.accounts","description":"ZAccountService\nIt's for Nubia Account.","removal":"delete","type":"oem"},{"id":"cn.nubia.advanced","description":"Hidden advanced test bluetooth, WiFi","removal":"delete","type":"oem"},{"id":"cn.nubia.aftersale","description":"Has location permissions and it's for debugging, logs.","removal":"delete","type":"oem"},{"id":"cn.nubia.antivirus","description":"We don't need antivirus if we have Play Protect.","removal":"delete","type":"oem"},{"id":"cn.nubia.applockmanager","description":"App Lock, Add Eyeprint","removal":"delete","type":"oem"},{"id":"cn.nubia.apptimelock","description":"Healthy time management\nApp time limit.","removal":"delete","type":"oem"},{"id":"cn.nubia.autoagingtest","description":"Hidden camera, WiFi tests.","removal":"delete","type":"oem"},{"id":"cn.nubia.behaviordetection","description":"Chinese useless frameworks.\nUsed to collect application behavior data to understand users.","removal":"delete","type":"oem"},{"id":"cn.nubia.bluetooth.opp","description":"Needed for Bluetooth","removal":"caution","type":"oem"},{"id":"cn.nubia.bootanimationinfo","description":"Boot Animation Info\nProbably it's unsafe to remove.","removal":"unsafe","type":"oem"},{"id":"cn.nubia.browser","description":"Browser\nStock Nubia Chinese browser.","removal":"delete","type":"oem"},{"id":"cn.nubia.calendar.preset","description":"Calendar\nStock Calendar app","removal":"replace","type":"oem"},{"id":"cn.nubia.clock.widget.preset","description":"ClockWidget\nIt has more permissions than it should have and DaemonService","removal":"delete","type":"oem"},{"id":"cn.nubia.cloud","description":"Nubia Cloud Manager","removal":"delete","type":"oem"},{"id":"cn.nubia.contacts","description":"Contacts\nStock Nubia Contacts","removal":"replace","type":"oem"},{"id":"cn.nubia.controlcenter","description":"control center\nProvides a control panel for controlling the switching of\nimportant functions in the system, such as data services, WiFi and other switches.","removal":"unsafe","type":"oem"},{"id":"cn.nubia.contrycode","description":"Useless logs. Useless frameworks.","removal":"delete","type":"oem"},{"id":"cn.nubia.databackup","description":"Backup\nIt's not the best choice for backup. Read Nubia privacy policy before.","removal":"delete","type":"oem"},{"id":"cn.nubia.deskclock.preset","description":"Clock\nStock Clock app for manage alarms.","removal":"replace","type":"oem"},{"id":"cn.nubia.diyaod","description":"It can be found in settings","removal":"delete","type":"oem"},{"id":"cn.nubia.doubleapp","description":"App duplicator\nit's duplicating app feature that can be found in settings","removal":"delete","type":"oem"},{"id":"cn.nubia.dynamicwallpaper","description":"Needed for video wallpapers","removal":"replace","type":"oem"},{"id":"cn.nubia.edge","description":"Smart Service Card\nSmart Service Card, Smart sidebar.","removal":"delete","type":"oem"},{"id":"cn.nubia.entertainmenttoolbox","description":"Useless logs, tracking. Useless frameworks.","removal":"delete","type":"oem"},{"id":"cn.nubia.error.dialog","description":"It's just a system error dialog. It doesn't send data anywhere.","removal":"delete","type":"oem"},{"id":"cn.nubia.extcard","description":"Nubia Chinese pay","removal":"delete","type":"oem"},{"id":"cn.nubia.faceid","description":"Face unlock lock screen.","removal":"delete","type":"oem"},{"id":"cn.nubia.factory","description":"Hidden hardware tests.","removal":"delete","type":"oem"},{"id":"cn.nubia.fan","description":"It has cooling fan activities.","removal":"caution","type":"oem"},{"id":"cn.nubia.fastpair","description":"Ultra low latency? It's something for Nubia earphones.","removal":"delete","type":"oem"},{"id":"cn.nubia.filebrowser","description":"File browser\nIt's for file browser probably. Has Chinese components.","removal":"replace","type":"oem"},{"id":"cn.nubia.fileobserver","description":"FileObserver\nFileObserver? Has Chinese components.","removal":"replace","type":"oem"},{"id":"cn.nubia.forbid.selfstart.provider","description":"Mainly used to manage application self-startup, convenient for users to adjust the policy of application self-startup.","removal":"replace","type":"oem"},{"id":"cn.nubia.gallery3d","description":"Gallery\nStock gallery with Chinese trackers.","removal":"delete","type":"oem"},{"id":"cn.nubia.gallerylockscreen","description":"Wallpapers are displayed on the lock screen.","removal":"replace","type":"oem"},{"id":"cn.nubia.gamehelpmodule","description":"It's a macro for the game. I think it's very useful if you're playing games.","removal":"caution","type":"oem"},{"id":"cn.nubia.gamehighlights","description":"Red Magic Moment game recorder\nI think it's very useful if you're playing games.","removal":"caution","type":"oem"},{"id":"cn.nubia.gamelauncher","description":"Game Space Nubia\nI think it's very useful if you're playing games.","removal":"caution","type":"oem"},{"id":"cn.nubia.gamenotes","description":"Game Notes\nGame Notes? Probably an optional thing.","removal":"replace","type":"oem"},{"id":"cn.nubia.gamepi","description":"Needed for game optimization probably and also has something for Game Space","removal":"caution","type":"oem"},{"id":"cn.nubia.garbagecleanwidget","description":"Widget One-tap Cleanup\nUseless Widget One-tap Cleanup. Useless frameworks.","removal":"delete","type":"oem"},{"id":"cn.nubia.gif","description":"It's for creating GIFs","removal":"delete","type":"oem"},{"id":"cn.nubia.gpu.drivers","description":"It has GPU settings so better keep that.","removal":"caution","type":"oem"},{"id":"cn.nubia.harassintercept","description":"Mainly used to intercept harassing and fraudulent phone calls and text messages,\nwhile the user can also customize the way to intercept.","removal":"delete","type":"oem"},{"id":"cn.nubia.heartrate","description":"Hidden? Heart Rate Detection","removal":"delete","type":"oem"},{"id":"cn.nubia.hybrid","description":"Quick Apps Preview for ads 3d. Has a lot of Chinese components.","removal":"delete","type":"oem"},{"id":"cn.nubia.identity","description":"It's for personalized recommendations.","removal":"delete","type":"oem"},{"id":"cn.nubia.jobdispatcher","description":"Mainly through the network to modify the basic data and functions of the self-developed application\nmodule part of the cell phone system in the background. Also has neopush.","removal":"caution","type":"oem"},{"id":"cn.nubia.keymapcenter","description":"It's for gaming right? Useless frameworks.","removal":"delete","type":"oem"},{"id":"cn.nubia.launcher","description":"Launcher\nStock Nubia Launcher.","removal":"caution","type":"oem"},{"id":"cn.nubia.monitor","description":"Better keep this for gaming.","removal":"caution","type":"oem"},{"id":"cn.nubia.musicpicker.preset","description":"Ringtones\nMusic Picker for nubia phones.","removal":"caution","type":"oem"},{"id":"cn.nubia.myfile","description":"File Manager\nNubia File Manager.","removal":"replace","type":"oem"},{"id":"cn.nubia.neopush","description":"Not needed for notifications. Has a lot of permissions.","removal":"delete","type":"oem"},{"id":"cn.nubia.neostore","description":"App Store\nNubia App Store","removal":"delete","type":"oem"},{"id":"cn.nubia.owlsystem","description":"Nubia data collection. It has a lot of permissions and it's spyware!","removal":"delete","type":"oem"},{"id":"cn.nubia.packageoptimization","description":"Nubia Package Optimization\nNubia Package Optimization? Cleans cache and more","removal":"replace","type":"oem"},{"id":"cn.nubia.paycomponent","description":"Chinese pay Nubia","removal":"delete","type":"oem"},{"id":"cn.nubia.persist","description":"PersistService\nPersistService? In code I found it's for logs.","removal":"delete","type":"oem"},{"id":"cn.nubia.phonemanualintegrate.preset","description":"Useless User Guide","removal":"delete","type":"oem"},{"id":"cn.nubia.photoeditor","description":"Photo Editor\nPhoto Editor. It has Baidu location so better turn off network for this app.","removal":"replace","type":"oem"},{"id":"cn.nubia.powersaving","description":"Better keep this for power saving?","removal":"caution","type":"oem"},{"id":"cn.nubia.processmanager","description":"Useless frameworks","removal":"delete","type":"oem"},{"id":"cn.nubia.quickappwebserver","description":"Another component of Quick Apps Preview for ads 3d.","removal":"delete","type":"oem"},{"id":"cn.nubia.quicksearchbox","description":"Useless quick search box","removal":"delete","type":"oem"},{"id":"cn.nubia.ramdisk","description":"Probably it's for optimization","removal":"caution","type":"oem"},{"id":"cn.nubia.sar","description":"PersistService\nPersistService? In code I found it's for logs.","removal":"delete","type":"oem"},{"id":"cn.nubia.security2","description":"It's not only security but it has a lot of useful settings.\nBetter keep this.","removal":"caution","type":"oem"},{"id":"cn.nubia.sensor","description":"Hidden sensor calibration.","removal":"delete","type":"oem"},{"id":"cn.nubia.setupwizard","description":"Setup Wizard\nYou don't need it after first-boot setup.","removal":"delete","type":"oem"},{"id":"cn.nubia.share","description":"share video\nNubia share video. You can share video by this app probably.","removal":"replace","type":"oem"},{"id":"cn.nubia.smartrecognition","description":"It's for smart things","removal":"delete","type":"oem"},{"id":"cn.nubia.supersnap","description":"Super Snap Shot, Screenshot","removal":"replace","type":"oem"},{"id":"cn.nubia.systemupdate","description":"System Update\nProvides system updates for nubia","removal":"caution","type":"oem"},{"id":"cn.nubia.theme.apply","description":"Needed for theme apply","removal":"replace","type":"oem"},{"id":"cn.nubia.thememanager","description":"Theme Manager\nStock Nubia Theme Manager","removal":"replace","type":"oem"},{"id":"cn.nubia.touping","description":"Needed for screen cast","removal":"replace","type":"oem"},{"id":"cn.nubia.upgradeservice","description":"Needed for upgrade service.","removal":"caution","type":"oem"},{"id":"cn.nubia.video","description":"Video Player\nNubia Video Player","removal":"replace","type":"oem"},{"id":"cn.nubia.virtualgamehandle","description":"Virtual joystick\nVirtual joystick? Useless logs, tracking. Useless frameworks.","removal":"delete","type":"oem"},{"id":"cn.nubia.voiceassist","description":"Nubia Voice\nAssistant things.","removal":"delete","type":"oem"},{"id":"cn.nubia.woodpecker","description":"Another tool for logs.","removal":"delete","type":"oem"},{"id":"cn.nubia.yulorepage","description":"Yulorepage\nHas only notifications thing.\nNot very useful. It's something for China like yellow pages.","removal":"delete","type":"oem"},{"id":"cn.nubia.zappdatabackup","description":"Backup App, Restore App","removal":"delete","type":"oem"},{"id":"cn.nubia.ziconunity","description":"Icon handling\nIcon handling? It's for theme icons probably","removal":"caution","type":"oem"},{"id":"cn.oneplus.nvbackup","description":"NVBackupUI\nRuns in the background on some phones.\nHandles things related to OTA system updates?\nSafe to disable, but might break OTA updates.","removal":"caution","type":"oem"},{"id":"cn.oneplus.oem_tcma","description":"TCMA = Tiered Contention Multiple Access\nRuns on boot.\nA form of CSMA/CA, a cellular traffic management protocol. TCMA schedules transmission of different types of traffic based on urgency.\nChina-only? (the \"cn\" in cn.oneplus is China's country code)\nhttps://en.wikipedia.org/wiki/Carrier-sense_multiple_access_with_collision_avoidance\nhttps://patents.google.com/patent/US20020163933A1","removal":"replace","type":"oem"},{"id":"cn.oneplus.oemtcma","description":"TCMA = Tiered Contention Multiple Access\nRuns on boot.\nA form of CSMA/CA, a cellular traffic management protocol. TCMA schedules transmission of different types of traffic based on urgency.\nChina-only? (the \"cn\" in cn.oneplus is China's country code)\nhttps://en.wikipedia.org/wiki/Carrier-sense_multiple_access_with_collision_avoidance\nhttps://patents.google.com/patent/US20020163933A1","removal":"replace","type":"oem"},{"id":"cn.oneplus.opmms","label":"OPMmsLocation","description":"Determines your location when sending SMS/MMS?\nChina-only? (\"cn\" is China's country code)","removal":"delete","type":"oem"},{"id":"cn.oneplus.photos","label":"Shot On OnePlus","description":"Accessible through the Wallpapers selection menu.\nProvides photos uploaded by OnePlus users, allowing you to set them as your wallpaper.\nEach day, one new photo appears within the application.","removal":"delete","type":"oem"},{"id":"cn.wps.moffice.lite.meizu","description":"WPS Office\nChinese WPS Office","removal":"delete","type":"oem"},{"id":"cn.wps.moffice_eng","description":"WPS Office\nChinese WPS Office pre-installed on Xiaomi devices.","removal":"delete","type":"oem"},{"id":"cn.wps.moffice_eng.xiaomi.lite","description":"Chinese WPS Office","removal":"delete","type":"oem"},{"id":"cn.wps.xiaomi.abroad.lite","label":"Mi Doc Viewer","description":"Allows users to view various types of documents on their Android devices like documents (*.doc/docx, *.ppt/pptx, *.xls/xlsx, *.pdf, *.wps, and *.txt) and it's powered by WPS Office\nFYI: WPS is a Chinese closed-source software. It's as bad as Microsoft Office (privacy-wise)","removal":"replace","type":"oem"},{"id":"cn.zte.recorder","description":"ZTE Voice Recorder with... 33 permissions and talking with Baidu servers. Pithus analysis: https://beta.pithus.org/report/bab47d32f5b93cdf4d3a3cb082d1d0e7ba3e323356391b2d46e63617c1d15324","removal":"delete","type":"oem"},{"id":"com.Qunar","description":"Chinese partner app 'Qunar Travel'.","removal":"delete","type":"oem"},{"id":"com.UCMobile","description":"Chinese UC Mobile.","removal":"delete","type":"oem"},{"id":"com.achievo.vipshop","description":"Chinese some shop, normal user can uninstall it.","removal":"delete","type":"oem"},{"id":"com.activate.activatephone","description":"Useless frameworks.","removal":"delete","type":"oem"},{"id":"com.adaptivebrightnessgo","description":"Not related to adaptive brightness in the sliding bar. The APK is called `CameraLightSensor`, so I guess it's for the camera.","removal":"replace","type":"oem"},{"id":"com.agui.app.imei","description":"IMEI Tool: Change MEID's & IMEI's of both SIM's\nEnter *#*#08#*#* in the dial pad to access\n","removal":"caution","type":"oem"},{"id":"com.agui.app.memtester","description":"Memory tester\nHidden test menu. Used in diagnostics, normally invoked by MMI(Man-Machine Interface) Codes\n","removal":"delete","type":"oem"},{"id":"com.agui.appblock","description":"App blocker\nSettings > Intelligent assistance: App blocker\n Unihertz power management service killing background apps to improve battery performance.","removal":"caution","type":"oem"},{"id":"com.agui.batterystatsdumper","description":"Battery Stats Dumper\nLets you check and clear battery usage statistics.\nEnter *#*#010#*#* in the dial pad to access this hidden menu.","removal":"replace","type":"oem"},{"id":"com.agui.game","label":"Game mode","description":"Blocks calls & notifications when selected APP's are open","removal":"delete","type":"oem"},{"id":"com.agui.newsos","description":"SOS\nNotify emergency contacts. When triggered, will also put the phone in a low comsumption mode\n","removal":"delete","type":"oem"},{"id":"com.agui.nfc","description":"NFC card emulation: simulates various types of unencrypted entrance cards.\n","removal":"delete","type":"oem"},{"id":"com.agui.providers.pedometer","description":"Toolbox > \"Pedometer\" Pedometer/step counter.\nBecause of a feature that integrates with the lock sceen the System UI crashes when removed.","removal":"unsafe","type":"oem"},{"id":"com.agui.screenshot","description":"Screenshot\nScreenshot utility triggered when double tapping the Red Button\n","removal":"replace","type":"oem"},{"id":"com.agui.studentmodel","label":"Student Mode","description":"Locks down your phone to reduce distractions","removal":"delete","type":"oem"},{"id":"com.agui.update","description":"\"Wireless Update\" Settings > About Phone : Sytem update.\nRemoving will prevent Automatic Wireless Updates\n","removal":"caution","required_by":["com.agui.update.overlay"],"type":"oem"},{"id":"com.agui.update.overlay","description":"Overlay for com.agui.update. Overlay are usually themes.\n","removal":"caution","dependencies":["com.agui.update"],"type":"oem"},{"id":"com.agui.usbcamera","description":"Toolbox > \"USB Camera\" Only usefull if you want to use a USB Camera\n","removal":"delete","type":"oem"},{"id":"com.aiunit.aon","description":"AONService. ColorOS Smart Features.\nApp run with AI and phone sensors to recognize what best scenario for run privacy protection and the smart features OPPO offer.\n 1. Smart Spying Prevention, 2. Smart Air Control, 3. Smart Rotation, 4. Smart Always On Display.\nhttps://community.oppo.com/thread/1446120575440257025","removal":"caution","type":"oem"},{"id":"com.alam.overlay.android","description":"It looks like the unused stuff has been translated into Indonesian.","removal":"delete","type":"oem"},{"id":"com.alam.overlay.android.systemui","description":"It looks like the unused stuff has been translated into Indonesian.","removal":"delete","type":"oem"},{"id":"com.alcatel.mcrm","description":"enjoy.now\nIt's an app for debugging and requires Google Play services.","removal":"delete","type":"oem"},{"id":"com.amap.android.location","description":"Chinese location. Not needed for location.","removal":"delete","type":"oem"},{"id":"com.amazon.amazonvideo.livingroom","description":"Prime video\nhttps://play.google.com/store/apps/details?id=com.amazon.amazonvideo.livingroom&hl=en&gl=US","removal":"delete","type":"oem"},{"id":"com.android.BBKClock","description":"Clock\nVivo clock app. App for manage alarms.","removal":"replace","type":"oem"},{"id":"com.android.BBKCrontab","description":"Task timer\nIt's for scheduling tasks.","removal":"delete","type":"oem"},{"id":"com.android.DeviceAsWebcam","label":"Webcam Service","description":"For using smartphone as a webcam. A dependency for Private space on Pixel devices.","web":["https://source.android.com/docs/security/features/private-space"],"removal":"caution","type":"oem"},{"id":"com.android.LGSetupWizard","description":"The first time you turn your device on, a Welcome screen is displayed. It guides you through the basics of setting up your device.\nIt's the setup for LG services.\n","removal":"delete","type":"oem"},{"id":"com.android.SystemUIResOverlay","description":"Threekey volume guide pictures.","removal":"caution","type":"oem"},{"id":"com.android.VideoPlayer","description":"i Video\nVideo player app for vivo phones.","removal":"delete","type":"oem"},{"id":"com.android.WingFactoryCamera","description":"MiCaliTool\nCamera Calibration.","removal":"delete","type":"oem"},{"id":"com.android.angle","description":"Have only empty main activity. it's not a joke.","removal":"delete","type":"oem"},{"id":"com.android.backup","description":"Xiaomi Backup and Restore feature (mislead package name).\nThis package was replaced by 'com.miui.backup' on newer models.\n","removal":"delete","type":"oem"},{"id":"com.android.bbk.lockscreen3","description":"Lock\nNeeded for lockscreen?","removal":"caution","type":"oem"},{"id":"com.android.bbklog","description":"Log Collection\nLogs everything.","removal":"delete","type":"oem"},{"id":"com.android.bbkmusic","description":"i Music\nMusic app with Chinese trackers.","removal":"replace","type":"oem"},{"id":"com.android.bips.auto_generated_rro_product__","description":"Unused auto generated code: color icon code to print service.","removal":"delete","type":"oem"},{"id":"com.android.bips.overlay.pixel","description":"it's overlay only to icon color.","removal":"delete","type":"oem"},{"id":"com.android.bluetooth.auto_generated_rro_product__","description":"Random generated code app, in TECNO Phone found headphones images in the app to Settings.","removal":"replace","type":"oem"},{"id":"com.android.bluetooth.bthelper","description":"BtHelper\nit's only for Apple Air Pods. it's used to check the battery level of AirPods headphones.","removal":"delete","type":"oem"},{"id":"com.android.bluetooth.oplus.overlay","description":"Overlay to 'com.android.bluetooth'.","removal":"unsafe","type":"oem"},{"id":"com.android.bluetooth.overlay","description":"Not a standard package provided by the Android framework.\nIt seems to be a custom package specific to a particular Android device or custom ROM.\nBased on the search results, there is no official documentation or information available about in the Android documentation or developer resources.\nIt is possible that this package contains custom overlays or modifications related to the Bluetooth functionality on a specific device or ROM.\nOverlays are used to customize the behavior or appearance of the Android system without modifying the core framework.\nThese overlays can be provided by device manufacturers or custom ROM developers to add or modify features specific to their devices.","removal":"unsafe","type":"oem"},{"id":"com.android.bluetooth.tempow","description":"Implementation of a improved bluetooth protocol (developed by french company Tempow)\nhttps://www.tempow.com/tap\nNOTE: This is NOT an AOSP package. It is OEMs who choose to implement this procotol or not.\nFor now, only TCL has this.\n","removal":"unsafe","type":"oem"},{"id":"com.android.camera.overlay","description":"Unused? In code found some sounds to camera app\nand random images that looks like a calibration.","removal":"replace","type":"oem"},{"id":"com.android.camera2","description":"Xiaomi Camera (I don't know why they kept this package name. It's really confusing.)\nIt's a proprietary app based on the AOSP sources:\nhttps://android.googlesource.com/platform/packages/apps/Camera2/+/master/src/com/android/camera\n","removal":"replace","type":"oem"},{"id":"com.android.camerabigballconfig.overlay","description":"Configures default Qr Code component\ncom.google.android.gms .mlkit.barcode.ui.PlatformBarcodeScanningActivityProxy. Not needed to camera or scan QR Code.","removal":"delete","type":"oem"},{"id":"com.android.cameraextensions","description":"CameraExtensionsProxy.\nCamera-related third-party apps can call Android camera extensions such as Portrait, Night Mode, and HDR, which doesn't seem to work significantly on MIUI.","removal":"delete","type":"oem"},{"id":"com.android.captiveportallogin.overlay","description":"it's overlay only to icon color.","removal":"delete","type":"oem"},{"id":"com.android.carrierconfig.overlay.product","description":"Better keep this for LTE network.","removal":"unsafe","type":"oem"},{"id":"com.android.cellbroadcast.overlay","description":"Unused overlay without code. Only for Test message.","removal":"delete","type":"oem"},{"id":"com.android.cellbroadcastreceiver.overlay.base.s600ww","description":"Nokia theme overlay for com.android.cellbroadcastreceiver","removal":"caution","dependencies":["com.android.cellbroadcastreceiver"],"type":"oem"},{"id":"com.android.cellbroadcastreceiver.overlay.pixel","description":"Useless code to cellbroadcastreceiver","removal":"delete","type":"oem"},{"id":"com.android.cellbroadcastservice.overlay","description":"Unused overlay. Cross-SIM duplicate detection false.","removal":"delete","type":"oem"},{"id":"com.android.cellbroadcastservice.overlay.pixel","description":"Useless code to cellbroadcastservice","removal":"delete","type":"oem"},{"id":"com.android.compos.payload","description":"CompOS\nHas something to 'com.android.compos'.","removal":"caution","type":"oem"},{"id":"com.android.connectivity.resources.nt.overlay","description":"WiFi configs. Better don't risk","removal":"unsafe","type":"oem"},{"id":"com.android.connectivity.resources.overlay","description":"Cause bootloop?\nFound in TECNO phone, in overlay there's a notification saying 'No Internet As Dialog When High Priority'.\nCan someone test it?","removal":"unsafe","type":"oem"},{"id":"com.android.dlna.service","description":"Screen cast OPPO.","removal":"replace","type":"oem"},{"id":"com.android.dreams.overlay.basic","description":"Overlay to 'com.android.dreams.basic' has icons png.","removal":"replace","type":"oem"},{"id":"com.android.dreams.overlay.phototable","description":"Overlay to 'com.android.dreams.phototable' has icons png.","removal":"replace","type":"oem"},{"id":"com.android.email.partnerprovider.overlay","description":"Theme overlay for partnerprovider?","removal":"delete","type":"oem"},{"id":"com.android.fileexplorer","description":"Xiaomi/Mi File Explorer (Again it's a really poor choice for a package name considering it is not the AOSP File explorer)\nIt's a Closed-source app based on the AOSP version.\n","removal":"replace","type":"oem"},{"id":"com.android.flyme.bridge.softsim","description":"it's for softsim? No activities, looks like a useless framework.","removal":"delete","type":"oem"},{"id":"com.android.frameworkhwext","description":"There is not much info but others seem to have disabled this and similar, see:\nhttps://forum.xda-developers.com/t/guide-list-of-bloatware-safe-to-remove-how-to-do-it.3866647.","removal":"caution","type":"oem"},{"id":"com.android.frameworkhwnext.honor","description":"Framework necessary for Themes app to launch","removal":"caution","type":"oem"},{"id":"com.android.gallery3d.refocus","description":"","removal":"caution","type":"oem"},{"id":"com.android.globalFileexplorer","label":"File Manager","description":"Xiaomi File Manager.","removal":"delete","type":"oem"},{"id":"com.android.hbmsvmanager","description":"Its app for calibration screen and debugging.\nHBM SV is unknown.","removal":"replace","type":"oem"},{"id":"com.android.hbmsvmanager.auto_generated_rro_product__","description":"unused png files, calibration. it's from (com.android.hbmsvmanager)","removal":"delete","type":"oem"},{"id":"com.android.huawei.HiMediaEngine","description":"It's for debugging camera. Has all camera features and report.","removal":"delete","type":"oem"},{"id":"com.android.huawei.projectmenu","description":"ProjectMenu\nHidden settings not available for users. it's useless.","removal":"delete","type":"oem"},{"id":"com.android.hwmirror","description":"Mirror\nLets you use your phone as a mirror...\n","removal":"delete","type":"oem"},{"id":"com.android.imedia.syncplay","description":"Party Mode\nParty Mode lets you sync music playback across multiple devices.","removal":"replace","type":"oem"},{"id":"com.android.imsserviceentitlement","description":"It's needed for WiFi calling.","removal":"caution","type":"oem"},{"id":"com.android.incallui","label":"Phone","description":"Xiaomi (and OnePlus) Phone dialer.\nFetches APN lists on some phones. Package name is highly misleading.","removal":"caution","type":"oem"},{"id":"com.android.incallui.overlay","description":"App without code and safe to remove.","removal":"delete","type":"oem"},{"id":"com.android.inputmethod.pinyin","description":"It is the built in software keyboard for LDPlayer. Getting rid of this breaks any keyboard input including from PC hardware keyboard.","removal":"caution","type":"oem"},{"id":"com.android.inputsettings.overlay.miui","description":"Keyboard demo found. It's something to miinput unused Chinese!","removal":"delete","type":"oem"},{"id":"com.android.keyguard","label":"HUAWEI magazine unlock","description":"It's a proprietary app based on the AOSP package called com.android.keyguard. Lets you customize your lock screen wallpapers.\nIf you have EMUI 10 or older, check the AOSP file, as Huawei uses AOSP package name for their own app.","web":["https://consumer.huawei.com/en/support/content/en-us00206571/","https://forum.xda-developers.com/honor-6x/how-to/guide-list-bloat-software-emui-safe-to-t3700814","https://forum.xda-developers.com/huawei-p40-pro/how-to/adb-debloating-t4088633","https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/issues/330"],"removal":"caution","warning":"Breaks home and recents button on Xiaomi Mi Pad.","type":"oem"},{"id":"com.android.launcher","description":"OPPO Launcher\nBreaks swipe up gestures after remove.","removal":"caution","type":"oem"},{"id":"com.android.launcher3.tv","description":"TV\nNot sure if it's useless frameworks or for the launcher or TV launcher.","removal":"caution","type":"oem"},{"id":"com.android.managedprovisioning.overlay","description":"Random useless code to com.android.managedprovisioning","removal":"delete","type":"oem"},{"id":"com.android.mediacenter","description":"Huawei music app. (Yeah they messed up with the package name)\n","removal":"delete","type":"oem"},{"id":"com.android.messaging","description":"Android default messaging app","removal":"replace","type":"oem"},{"id":"com.android.microdroid.empty_payload","description":"empty app that has permission to MANAGE VIRTUAL MACHINE.","removal":"delete","type":"oem"},{"id":"com.android.midrive","description":"Mi Drive \nMisleading package name. It is indeed a closed-source Xiaomi application.\nAllow for cloud storage (on Mi Cloud) and syncing across multiple Android devices.\n","removal":"delete","type":"oem"},{"id":"com.android.mmi","description":"Not an AOSP package at all\nHidden MMI test app \nMMI = Man Machine Interface ?\n","removal":"unsafe","type":"oem"},{"id":"com.android.mmifm","description":"FM Radio_wrh\nIt looks like unused FM Radio stuff without activity, adding favorite channels.","removal":"delete","type":"oem"},{"id":"com.android.mmisubtest","description":"P-Sensor Calibration\nIt's useless and hidden.","removal":"delete","type":"oem"},{"id":"com.android.mmitest","description":"Hardware Testing\nHidden testing hardware things.","removal":"delete","type":"oem"},{"id":"com.android.mms.overlay.cmcc","description":"Likely overlay themes from China Mobile Communications Corporation(CMCC) or China Telecom(CT).","removal":"caution","type":"oem"},{"id":"com.android.mms.overlay.ct","description":"Likely overlay themes from China Mobile Communications Corporation(CMCC) or China Telecom(CT).","removal":"caution","type":"oem"},{"id":"com.android.mms.service.plugin.UaProfUrl","description":"Useless plugin for logs","removal":"delete","type":"oem"},{"id":"com.android.mms.service.plugin.UserAgent","description":"Useless plugin for logs. Collects data.","removal":"delete","type":"oem"},{"id":"com.android.modemnotifier","description":"Modem Info\nHidden Modem Info? Used for logs.","removal":"delete","type":"oem"},{"id":"com.android.networksettings.overlay.ct","description":"Likely overlay themes from China Mobile Communications Corporation(CMCC) or China Telecom(CT).","removal":"caution","type":"oem"},{"id":"com.android.nfc.auto_generated_rro_product__","description":"Random useless code to com.android.nfc","removal":"delete","type":"oem"},{"id":"com.android.nfc.auto_generated_rro_vendor__","description":"Random useless code to com.android.nfc","removal":"delete","type":"oem"},{"id":"com.android.omadm.service","description":"Oma Device Management, There are carrier tools, no firmware update found.\nThe code also includes Subscription Index and configures FOTA.","removal":"replace","type":"oem"},{"id":"com.android.omadm.service.auto_generated_rro_product__","description":"Useless config metrics code","removal":"delete","type":"oem"},{"id":"com.android.omadm.service.auto_generated_rro_vendor__","description":"Useless devinfo model","removal":"delete","type":"oem"},{"id":"com.android.opasuwintegrationsample","description":"Optimized Setup Wizard Integration Sample code.\nCan be used to create a custom setup wizard, such as one created by a manufacturer that runs after the initial Android Setup Wizard.\nDoes not appear to actually run. It is sample code and should be able to be removed safely.","removal":"delete","type":"oem"},{"id":"com.android.overlay.calendar","description":"Overlay to 'com.android.calendar' logo calendar icons.","removal":"replace","type":"oem"},{"id":"com.android.overlay.camera2","description":"Overlay to 'com.android.camera2' png icons.","removal":"replace","type":"oem"},{"id":"com.android.overlay.contacts","description":"Overlay to 'com.android.contacts' png icons.","removal":"replace","type":"oem"},{"id":"com.android.overlay.dialer","description":"Overlay to 'com.android.dialer' png icons.","removal":"replace","type":"oem"},{"id":"com.android.overlay.dynamiciconconfig","description":"Overlay to 'com.android.launcher3' calendar, clock component name.","removal":"caution","type":"oem"},{"id":"com.android.overlay.email","description":"Overlay to 'com.android.email' icons png.","removal":"delete","type":"oem"},{"id":"com.android.overlay.emergency","description":"Overlay to 'com.android.emergency' icons png.","removal":"replace","type":"oem"},{"id":"com.android.overlay.fmradio","description":"Overlay to 'com.android.fmradio' icons png.","removal":"replace","type":"oem"},{"id":"com.android.overlay.gallery3d","description":"Overlay to 'com.android.gallery3d' icons png.","removal":"replace","type":"oem"},{"id":"com.android.overlay.gmscontactprovider","description":"Checked in miui 14.\nIncorrect named thing to sync metadata contacts or gms?\nmetadata_sync_pacakge com.google.android.gms\nNo effects after remove.\n(detected on miui 14 with android 13 phones)\nit's an unused overlay.","removal":"delete","type":"oem"},{"id":"com.android.overlay.gmssettingprovider","description":"Checked in miui 14.\nHave some stuff about google gms: backup.BackupTransportService.\nUseless backup things, without it backup and cloud work.\nMaybe it's used for backup settings, but probably not, and if it is, I don't think you need it.\n(detected on miui 14 with android 13 phones)\nIt's an unused overlay.","removal":"delete","type":"oem"},{"id":"com.android.overlay.gmssettings","description":"Unused overlay to 'com.google.android.apps.safetyhub'.","removal":"delete","type":"oem"},{"id":"com.android.overlay.gmstelecomm","description":"Overlay app to 'com.google.android.dialer', 'com.android.incallui'.","removal":"delete","type":"oem"},{"id":"com.android.overlay.gmstelephony","description":"Overlay app to 'com.google.android.dialer', 'com.google.android.gms'.","removal":"delete","type":"oem"},{"id":"com.android.overlay.launcher3","description":"Overlay to 'com.android.launcher3' icons png.","removal":"caution","type":"oem"},{"id":"com.android.overlay.livepicker","description":"Overlay to 'com.android.wallpaper.livepicker' icons png.","removal":"delete","type":"oem"},{"id":"com.android.overlay.logmanager","description":"Overlay to 'com.android.logmanager' icons png.","removal":"delete","type":"oem"},{"id":"com.android.overlay.messaging","description":"Overlay to 'com.android.messaging' icons png.","removal":"replace","type":"oem"},{"id":"com.android.overlay.music","description":"Overlay to 'com.android.music' icons png.","removal":"delete","type":"oem"},{"id":"com.android.overlay.quicksearchbox","description":"Overlay to 'com.android.quicksearchbox' icons png.","removal":"replace","type":"oem"},{"id":"com.android.overlay.settings","description":"Overlay to 'com.android.settings' icons png.","removal":"caution","type":"oem"},{"id":"com.android.overlay.soundrecorder","description":"Overlay to 'com.android.soundrecorder' icons png.","removal":"delete","type":"oem"},{"id":"com.android.overlay.stk","description":"Overlay to 'com.android.stk' icons png.","removal":"replace","type":"oem"},{"id":"com.android.overlay.wallpaper","description":"Overlay to 'com.android.wallpaper' icons png.","removal":"replace","type":"oem"},{"id":"com.android.partnerbrowsercustomizations.btl.s600ww.overlay","description":"Theme overlay for some browser customization?","removal":"caution","type":"oem"},{"id":"com.android.pedometer","description":"Pedometer\nIt's for displaying steps probably.","removal":"delete","type":"oem"},{"id":"com.android.phone.auto_generated_rro_product__","description":"Configs to phone auto generated but better keep.","removal":"unsafe","type":"oem"},{"id":"com.android.phone.auto_generated_rro_vendor__","description":"Configs to phone auto generated but better keep.","removal":"unsafe","type":"oem"},{"id":"com.android.phone.injection","description":"Possibly safe, or useless as removal doesn't seem to change anything.\nIt's a very small APK, it's a single class which only assigns a constant numeric value to a variable called 'telephony_injection'","removal":"replace","type":"oem"},{"id":"com.android.phone.overlay.carriersettings","description":"Overlay to (com.google.android.carrier)\nAnyway, this app doesn't exist on your phone I guess.","removal":"caution","type":"oem"},{"id":"com.android.phone.overlay.miui","description":"Contains specific modifications and enhancements made by MIUI to the phone app's user interface and functionality.","removal":"unsafe","type":"oem"},{"id":"com.android.phone.overlay.motcommon","description":"Related to Motorola's custom overlay for Phone and other UI elements.","removal":"replace","type":"oem"},{"id":"com.android.poweroffhandlerapp","description":"I found only lockDevice and Shutdown Phone Animation","removal":"delete","type":"oem"},{"id":"com.android.powertouch","description":"Moto Power Touch\nlets you customize the double tap power button gesture to launch an app or shortcut of your choice.","removal":"delete","type":"oem"},{"id":"com.android.providers.calendar.overlay.base.s600ww","description":"Some overlay for a content provider package. Overlays are usually themes.","removal":"caution","type":"oem"},{"id":"com.android.providers.contacts.auto_generated_rro_product__","description":"Incorrect named thing to sync metadata gms?\nmetadata_sync_pacakge com.google.android.gms\nNo effects after remove.","removal":"delete","type":"oem"},{"id":"com.android.providers.contacts.overlay.pixel","description":"in overlay found: metadata_sync_pacakge: com.google.android.gms. Not needed.","removal":"delete","type":"oem"},{"id":"com.android.providers.media.overlay.pixel","description":"Useless code to photos cloudpicker.","removal":"delete","type":"oem"},{"id":"com.android.providers.privacyprotection","description":"It has permission protect and logs.","removal":"delete","type":"oem"},{"id":"com.android.providers.settings.auto_generated_rro_product__","description":"Useless overlay to Backup google. Backup works without it.","removal":"delete","type":"oem"},{"id":"com.android.providers.settings.auto_generated_rro_vendor__","description":"rro = Runtime Resources Overlay.\nChanges values of a package config, based in the overlay definitions (heavily used by OEMs to customize the look and feel of Android).","removal":"replace","type":"oem"},{"id":"com.android.providers.settings.btl.s600ww.overlay","description":"Some overlay for a content provider package. Overlays are usually themes.","removal":"caution","type":"oem"},{"id":"com.android.providers.settings.overlay.base.s600ww","description":"Some overlay for a content provider package. Overlays are usually themes.","removal":"caution","type":"oem"},{"id":"com.android.providers.settings.overlay.common","description":"Better keep this for acceleration? Better don't risk.","removal":"unsafe","type":"oem"},{"id":"com.android.providers.tctdatahubprovider","description":"","removal":"caution","type":"oem"},{"id":"com.android.providers.telephony.auto_generated_rro_product__","description":"Configs to providers telephony auto generated but better keep.","removal":"unsafe","type":"oem"},{"id":"com.android.providers.telephony.overlay.carriersettings","description":"Overlay to (com.google.android.carrier)\nAnyway, this app doesn't exist on your phone I guess.","removal":"caution","type":"oem"},{"id":"com.android.providers.tv","description":"TV Storage\nProvides TV listings.","removal":"unsafe","type":"oem"},{"id":"com.android.qns","description":"Apn service, emergency calling, not sure how useful this app is.","removal":"caution","type":"oem"},{"id":"com.android.retaildemo.overlay.base.s600ww","description":"Theme overlay for Retail demonstration mode?\nhttps://en.wikipedia.org/wiki/Demo_mode","removal":"caution","type":"oem"},{"id":"com.android.role.notes.enabled","label":"Notes Role enabled","description":"Unknown overlay, sets default notes.","removal":"caution","warning":"Removing the package breaks seamless disabling and enabling of the developer options on Google Pixels.","type":"oem"},{"id":"com.android.safetycenter.resources.overlay","description":"Safety Center overlay.\nOverlays work by mapping resources defined in the overlay package to resources defined in the target package. When an app attempts to resolve the value of a resource in the target package, the value of the overlay resource the target resource is mapped to is returned instead.\nSafety Center provides redirection entries so that users can access specific security and privacy settings. Safety Center also identifies issues that users can fix on their devices or accounts, by combining dynamic data received from multiple sources. This data provides users with a general safety status with specific recommendations.\nhttps://source.android.com/docs/core/runtime/rros\nhttps://source.android.com/docs/security/safety-center/overview","removal":"replace","type":"oem"},{"id":"com.android.safetycenter.styles.overlay","description":"Safety Center styles overlay.\nOverlays work by mapping resources defined in the overlay package to resources defined in the target package. When an app attempts to resolve the value of a resource in the target package, the value of the overlay resource the target resource is mapped to is returned instead.\nSafety Center provides redirection entries so that users can access specific security and privacy settings. Safety Center also identifies issues that users can fix on their devices or accounts, by combining dynamic data received from multiple sources. This data provides users with a general safety status with specific recommendations.\nhttps://source.android.com/docs/core/runtime/rros\nhttps://source.android.com/docs/security/safety-center/overview","removal":"delete","type":"oem"},{"id":"com.android.safetyregulatoryinfo","description":"Unused? This is what you can find probably in settings:\nsetSavePassword, setSaveFormData, setBlockNetworkLoads, setJavaScriptEnabled.","removal":"replace","type":"oem"},{"id":"com.android.safetyregulatoryinfo.auto_generated_rro_product__","description":"Useless unused Chinese images.","removal":"delete","type":"oem"},{"id":"com.android.screenshot","description":"Default android screenshot tool","removal":"replace","type":"oem"},{"id":"com.android.sdm.plugins.connmo","description":"ConnMO\nNotifications for video calling?\nAlso Apn Service.","removal":"replace","type":"oem"},{"id":"com.android.sdm.plugins.dcmo","description":"Carrier OMADM\nAnother thing to VoLTE.","removal":"replace","type":"oem"},{"id":"com.android.sdm.plugins.diagmon","description":"diagnostic plugin\nit's app for hidden diagnostics.","removal":"delete","type":"oem"},{"id":"com.android.secretcode","description":"SecretCode\nHidden app shows some secret codes.","removal":"delete","type":"oem"},{"id":"com.android.server.deviceconfig.resources","description":"DeviceConfigServiceResources (Server*?). Part of com.android.configinfrastructure. Resources for the device's default server config.\nhttps://cs.android.com/android/platform/superproject/main/+/main:packages/modules/ConfigInfrastructure/service/ServiceResources/Android.bp","removal":"caution","type":"oem"},{"id":"com.android.server.telecom.auto_generated_rro_product__","description":"Configs to server telecom auto generated but better keep.","removal":"unsafe","type":"oem"},{"id":"com.android.server.telecom.overlay.miui","description":"Manage calls\nit's used for manage calls ui.\nit's important overlay to calling.","removal":"unsafe","type":"oem"},{"id":"com.android.service.ims","description":"It's for subscribing to the presence information using RCS.","removal":"replace","type":"oem"},{"id":"com.android.settingaccessibility","description":"Accessibility settings\nIt should be important for settings.","removal":"caution","type":"oem"},{"id":"com.android.settings.auto_generated_rro_product__","description":"Configs to settings auto generated but better keep.","removal":"unsafe","type":"oem"},{"id":"com.android.settings.auto_generated_rro_vendor__","description":"Configs to settings auto generated but better keep.","removal":"unsafe","type":"oem"},{"id":"com.android.settings.os.overlay","description":"Looks unused, but not confirmed. Found in app 'icon file download', Vibration switch. Icon user found, overlay to 'com.android.settings'.","removal":"caution","type":"oem"},{"id":"com.android.settings.overlay.SettingsFuture","description":"Configs to settings auto generated but better keep.","removal":"unsafe","type":"oem"},{"id":"com.android.settings.overlay.cmcc","description":"Likely overlay themes from China Mobile Communications Corporation(CMCC) or China Telecom(CT).","removal":"caution","type":"oem"},{"id":"com.android.settings.overlay.common","description":"Color mode config you can find it in settings\nit's something about: natural, boosted, saturated, adaptive.","removal":"unsafe","type":"oem"},{"id":"com.android.settings.overlay.ct","description":"Likely overlay themes from China Mobile Communications Corporation(CMCC) or China Telecom(CT).","removal":"caution","type":"oem"},{"id":"com.android.settings.overlay.filesgoogle","description":"Useless overlay to Google Files.","removal":"delete","type":"oem"},{"id":"com.android.settings.overlay.g1azg","description":"Configs to settings auto generated but better keep.","removal":"unsafe","type":"oem"},{"id":"com.android.settings.overlay.gb17l","description":"Configs to settings auto generated but better keep.","removal":"unsafe","type":"oem"},{"id":"com.android.settings.overlay.gb62z","description":"Configs to settings auto generated but better keep.","removal":"unsafe","type":"oem"},{"id":"com.android.settings.overlay.gx7as","description":"Configs to settings auto generated but better keep.","removal":"unsafe","type":"oem"},{"id":"com.android.settings.overlay.miui","description":"This app is without code and safe to remove.","removal":"delete","type":"oem"},{"id":"com.android.settings.overlay.personalsafety","description":"Useless overlay to personalsafety, safetyhub","removal":"delete","type":"oem"},{"id":"com.android.settings.overlay.pixel2021","description":"Configs to settings auto generated but better keep.","removal":"unsafe","type":"oem"},{"id":"com.android.settings.overlay.product","description":"Useless overlay to images phone white and black, it's for camera MP info.","removal":"delete","type":"oem"},{"id":"com.android.settings.overlay.turbo","description":"Useless overlay to smart battery images, videos. And it's probably unused.","removal":"delete","type":"oem"},{"id":"com.android.settings.resoverlay","description":"Found in the code \"Declaration of Conformity\" on the TECNO phone, is probably unused, as it cannot be found in the Settings or anywhere else.\nIt can be safely removed, the Settings still work without this app.","removal":"replace","type":"oem"},{"id":"com.android.settingsaccessibility","description":"Accessibility settings (tools for vision, hearing and physical impairments)\nSometimes, third-party apps may require special permissions in the accessibility settings in order to work properly.","removal":"caution","type":"oem"},{"id":"com.android.setupwizard.overlay","description":"It's needed only on first-boot setup.","removal":"delete","type":"oem"},{"id":"com.android.setupwizard.overlay.ontim","description":"Useless overlay to welcome image First-boot setup.","removal":"delete","type":"oem"},{"id":"com.android.simappdialog.auto_generated_rro_product__","description":"Useless overlay to simappdialog.","removal":"delete","type":"oem"},{"id":"com.android.soundpicker.auto_generated_rro_product__","description":"Has colors, dialog things, autogenerated code, but someone can confirm that app is safe to remove?","removal":"caution","type":"oem"},{"id":"com.android.stk.overlay.miui","description":"'SIM Toolkit' name app only found.","removal":"delete","type":"oem"},{"id":"com.android.store","description":"An app called TCL Mobile that you cannot disable and just opens up a link to the OEM in the browser.","removal":"delete","type":"oem"},{"id":"com.android.supl","description":"SUPL20Service\nGPS still works without it. Probably needed to location in China.","removal":"delete","type":"oem"},{"id":"com.android.systemui.auto_generated_rro_product__","description":"Configs to systemui auto generated but better keep.","removal":"unsafe","type":"oem"},{"id":"com.android.systemui.auto_generated_rro_vendor__","description":"Configs to systemui auto generated but better keep.","removal":"unsafe","type":"oem"},{"id":"com.android.systemui.feature_cover","description":"It looks like a very important app.\nImportant configs to android?","removal":"unsafe","type":"oem"},{"id":"com.android.systemui.miui.optimization.overlay","description":"In the code founded: navigationcolor to red.\nUnused overlay.","removal":"delete","type":"oem"},{"id":"com.android.systemui.notch.overlay","description":"It have something to Display Cutout. Better dont touch it.\nImportant configs to display.","removal":"unsafe","type":"oem"},{"id":"com.android.systemui.os.overlay","description":"Icon user found, overlay to 'com.android.systemui'.","removal":"caution","type":"oem"},{"id":"com.android.systemui.overlay.charging.anim.alita_supervooc2","description":"Charging phone animation from a lot of PNG files","removal":"delete","type":"oem"},{"id":"com.android.systemui.overlay.charging.anim.siphon_wireless","description":"Wireless charging animation for OnePlus phones.","removal":"replace","type":"oem"},{"id":"com.android.systemui.overlay.cmcc","description":"Likely overlay themes from China Mobile Communications Corporation(CMCC) or China Telecom(CT).","removal":"caution","type":"oem"},{"id":"com.android.systemui.overlay.ct","description":"Likely overlay themes from China Mobile Communications Corporation(CMCC) or China Telecom(CT).","removal":"caution","type":"oem"},{"id":"com.android.systemui.overlay.fingerprint.anim.ccyh","description":"Fingerprint animation from a lot of PNG files","removal":"delete","type":"oem"},{"id":"com.android.systemui.overlay.fingerprint.anim.cosmos","description":"Fingerprint Animation Cosmos when unlocking phone.","removal":"replace","type":"oem"},{"id":"com.android.systemui.overlay.fingerprint.anim.fireworks","description":"Fingerprint Animation Fireworks when unlocking phone.","removal":"replace","type":"oem"},{"id":"com.android.systemui.overlay.fingerprint.anim.fy","description":"Fingerprint Animation Fy when unlocking phone.","removal":"replace","type":"oem"},{"id":"com.android.systemui.overlay.fingerprint.anim.jhsy","description":"Fingerprint animation from a lot of PNG files","removal":"delete","type":"oem"},{"id":"com.android.systemui.overlay.fingerprint.anim.jslz","description":"Fingerprint animation from a lot of PNG files","removal":"delete","type":"oem"},{"id":"com.android.systemui.overlay.fingerprint.anim.lgsy","description":"Fingerprint animation from a lot of PNG files","removal":"delete","type":"oem"},{"id":"com.android.systemui.overlay.fingerprint.anim.nlgs","description":"Fingerprint animation from a lot of PNG files","removal":"delete","type":"oem"},{"id":"com.android.systemui.overlay.fingerprint.anim.none","description":"Fingerprint Animation None when unlocking phone.","removal":"replace","type":"oem"},{"id":"com.android.systemui.overlay.fingerprint.anim.ripple","description":"Fingerprint Animation Ripple when unlocking phone.","removal":"replace","type":"oem"},{"id":"com.android.systemui.overlay.fingerprint.anim.stripe","description":"Fingerprint Animation Stripe when unlocking phone.","removal":"replace","type":"oem"},{"id":"com.android.systemui.overlay.fingerprint.anim.sw","description":"Fingerprint Animation Sw when unlocking phone.","removal":"replace","type":"oem"},{"id":"com.android.systemui.overlay.fingerprint.anim.tyjw","description":"Fingerprint animation from a lot of PNG files","removal":"delete","type":"oem"},{"id":"com.android.systemui.overlay.fingerprint.anim.xklc","description":"Fingerprint animation from a lot of PNG files","removal":"delete","type":"oem"},{"id":"com.android.systemui.overlay.gms","description":"Useless overlay to systemui gms","removal":"delete","type":"oem"},{"id":"com.android.systemui.overlay.miui","description":"App without code and safe to remove.","removal":"delete","type":"oem"},{"id":"com.android.systemui.overlay.pong","description":"Better keep this for config temperature? Better don't risk.","removal":"unsafe","type":"oem"},{"id":"com.android.systemui.overlay.spacewar","description":"Better keep this for config temperature? Better don't risk.","removal":"unsafe","type":"oem"},{"id":"com.android.theme.color.amethyst","description":"Android color accent only for Google Pixel or AOSP or Motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.color.aquamarine","description":"Android color accent only for Google Pixel or AOSP or Motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.color.black","description":"Android color accent only for google pixel or aosp or motorola","removal":"delete","type":"oem"},{"id":"com.android.theme.color.cinnamon","description":"Android color accent only for google pixel or aosp or motorola","removal":"delete","type":"oem"},{"id":"com.android.theme.color.darklake","description":"Android color accent only for Google Pixel or AOSP or Motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.color.dorange","description":"Android color accent only for Google Pixel or AOSP or Motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.color.dpurple","description":"Android color accent only for Google Pixel or AOSP or Motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.color.green","description":"Android color accent only for google pixel or aosp or motorola","removal":"delete","type":"oem"},{"id":"com.android.theme.color.lgreen","description":"Android color accent only for Google Pixel or AOSP or Motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.color.ocean","description":"Android color accent only for google pixel or aosp or motorola","removal":"delete","type":"oem"},{"id":"com.android.theme.color.orchid","description":"Android color accent only for google pixel or aosp or motorola","removal":"delete","type":"oem"},{"id":"com.android.theme.color.parasailing","description":"Android color accent only for Google Pixel or AOSP or Motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.color.purple","description":"Android color accent only for google pixel or aosp or motorola","removal":"delete","type":"oem"},{"id":"com.android.theme.color.saffron","description":"Android color accent only for Google Pixel or AOSP or Motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.color.sand","description":"Android color accent only for Google Pixel or AOSP or Motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.color.slate","description":"Android color accent only for Google Pixel or AOSP or Motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.color.space","description":"Android color accent only for google pixel or aosp or motorola","removal":"delete","type":"oem"},{"id":"com.android.theme.color.tangerine","description":"Android color accent only for Google Pixel or AOSP or Motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.font.ArchivoSemiBold","description":"Font ArchivoSemiBold","removal":"delete","type":"oem"},{"id":"com.android.theme.font.Exo2Regular","description":"Font Exo2Regular","removal":"delete","type":"oem"},{"id":"com.android.theme.font.RobotoSlabRegular","description":"Font RobotoSlabRegular","removal":"delete","type":"oem"},{"id":"com.android.theme.font.RookeryRegular","description":"Font RookeryRegular","removal":"delete","type":"oem"},{"id":"com.android.theme.icon.round","description":"Android icon shape only for Google Pixel or AOSP or Motorola","removal":"delete","type":"oem"},{"id":"com.android.theme.icon.roundedrect","description":"Android color accent only for google pixel or aosp or motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.icon.squircle","description":"Android color accent only for google pixel or aosp or motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.icon.teardrop","description":"Android color accent only for google pixel or aosp or motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.icon_pack.circular.android","description":"Android icon pack only for google pixel or aosp or motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.icon_pack.circular.launcher","description":"Android icon pack only for google pixel or aosp or motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.icon_pack.circular.settings","description":"Android icon pack only for google pixel or aosp or motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.icon_pack.circular.systemui","description":"Android icon pack only for google pixel or aosp or motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.icon_pack.circular.themepicker","description":"Android icon pack only for google pixel or aosp or motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.icon_pack.filled.android","description":"Android icon pack only for google pixel or aosp or motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.icon_pack.filled.launcher","description":"Android icon pack only for google pixel or aosp or motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.icon_pack.filled.settings","description":"Android icon pack only for google pixel or aosp or motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.icon_pack.filled.systemui","description":"Android icon pack only for google pixel or aosp or motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.icon_pack.filled.themepicker","description":"Android icon pack only for google pixel or aosp or motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.icon_pack.kai.android","description":"Android icon pack only for Google Pixel or AOSP or Motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.icon_pack.kai.launcher","description":"Android icon pack only for Google Pixel or AOSP or Motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.icon_pack.kai.settings","description":"Android icon pack only for Google Pixel or AOSP or Motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.icon_pack.kai.systemui","description":"Android icon pack only for Google Pixel or AOSP or Motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.icon_pack.kai.themepicker","description":"Android icon pack only for Google Pixel or AOSP or Motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.icon_pack.rounded.android","description":"Android icon pack only for google pixel or aosp or motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.icon_pack.rounded.launcher","description":"Android icon pack only for google pixel or aosp or motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.icon_pack.rounded.settings","description":"Android icon pack only for google pixel or aosp or motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.icon_pack.sam.android","description":"Android icon pack only for Google Pixel or AOSP or Motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.icon_pack.sam.launcher","description":"Android icon pack only for Google Pixel or AOSP or Motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.icon_pack.sam.settings","description":"Android icon pack only for Google Pixel or AOSP or Motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.icon_pack.sam.systemui","description":"Android icon pack only for Google Pixel or AOSP or Motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.icon_pack.sam.themepicker","description":"Android icon pack only for Google Pixel or AOSP or Motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.icon_pack.victor.android","description":"Android icon pack only for Google Pixel or AOSP or Motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.icon_pack.victor.launcher","description":"Android icon pack only for Google Pixel or AOSP or Motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.icon_pack.victor.settings","description":"Android icon pack only for Google Pixel or AOSP or Motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.icon_pack.victor.systemui","description":"Android icon pack only for Google Pixel or AOSP or Motorola","removal":"replace","type":"oem"},{"id":"com.android.theme.icon_pack.victor.themepicker","description":"Android icon pack only for Google Pixel or AOSP or Motorola","removal":"replace","type":"oem"},{"id":"com.android.thememanager","label":"Xioami Themes","description":"Formerly, MIUI Themes.\nXiaomi seems to love confusing package names.\nLets you select and apply themes provided by Xiaomi.\nHas a lot trackers. Running in the background.","removal":"caution","warning":"Disabling will break the ability to change the lock-screen wallpaper and ringtones in the OEM clock app.","type":"oem"},{"id":"com.android.thememanager.gliobal_config.config.overlay","description":"Blacklist of some wallpaper names. Useless.\nThis app is not exist on miui china rom.","removal":"delete","type":"oem"},{"id":"com.android.thememanager.module","description":"Something related to Xiaomi's theme manager?","removal":"caution","type":"oem"},{"id":"com.android.traceur.auto_generated_rro_product__","description":"useless configs to traceur auto generated.","removal":"delete","type":"oem"},{"id":"com.android.traceur.auto_generated_rro_vendor__","description":"useless configs to traceur auto generated.","removal":"delete","type":"oem"},{"id":"com.android.traceur.overlay.pixel","description":"Unused colors to 5G","removal":"delete","type":"oem"},{"id":"com.android.tv.frameworkpackagestubs","description":"Activity Stub\nHard to say by code, has browser provider and SQLite content.","removal":"caution","type":"oem"},{"id":"com.android.tv.settings","description":"Needed for settings.","removal":"unsafe","type":"oem"},{"id":"com.android.unisoc.telephony.server","description":"Unisoc 4G and 5G drivers.","web":["https://xdaforums.com/t/oukitel-wp23-pro-unlocking-bootloader-rooting-gsi.4642483/#post-89239693"],"removal":"caution","warning":"Removal will break incoming calls.","type":"oem"},{"id":"com.android.updater","description":"Mi Updater\nProvides system updates\nREMOVING THIS WILL BOOTLOOP YOUR DEVICE! Doesn't bootloop on MIUI 13 and above.","removal":"caution","type":"oem"},{"id":"com.android.vendors.bridge.softsim","description":"Needed for virtual SIM.","removal":"delete","type":"oem"},{"id":"com.android.vivo.tws.vivotws","description":"TWS\nIt's for Vivo TWS earphones.","removal":"delete","type":"oem"},{"id":"com.android.voicemailomtp","description":"Voicemail\nVoicemail? I think no one uses that.","removal":"delete","type":"oem"},{"id":"com.android.wallpaper","description":"Styles editor\nNeeded for editing style UI","removal":"replace","type":"oem"},{"id":"com.android.watermark","description":"Hidden IMEI tests.","removal":"delete","type":"oem"},{"id":"com.android.wifi.resources.overlay.WifiRes6GhzEnable","description":"Config to support wifi 6 Ghz? Any wifi have this?\nIf you don't have 6 Ghz wifi then it's safe to disable.","removal":"delete","type":"oem"},{"id":"com.android.wifi.resources.overlay.WifiResDualStaApEnable","description":"5 GHz WiFi support.","removal":"unsafe","type":"oem"},{"id":"com.android.wifi.resources.overlay.WifiResSoftap80211axEnable","description":"Important configs to wifi.","removal":"unsafe","type":"oem"},{"id":"com.android.wifi.resources.overlay.common","description":"System Wi-Fi resources Theme pack\nGuessing it's a pack of themes for some Wi-Fi related system UI, based on the name.","removal":"unsafe","type":"oem"},{"id":"com.android.wifi.resources.overlay.kalama","description":"Needed for WiFi.","removal":"unsafe","type":"oem"},{"id":"com.android.wifi.resources.overlay.motCommon","description":"Related to Motorola's custom overlay for Wi-Fi connections.","removal":"replace","type":"oem"},{"id":"com.android.wifi.resources.overlay.oplus","description":"Wi-Fi configs","removal":"unsafe","type":"oem"},{"id":"com.android.wifi.resources.overlay.pineapple","description":"A runtime resource overlay (RRO) is a package that changes the resource values of a target package at runtime. For example, an app installed on the system image might change its behavior based upon the value of a resource. Rather than hardcoding the resource value at build time, an RRO installed on a different partition can change the values of the app's resources at runtime.\nRROs can be enabled or disabled. You can programmatically set the enable/disable state to toggle an RRO's ability to change resource values. RROs are disabled by default (however, static RROs are enabled by default).\nhttps://source.android.com/docs/core/runtime/rros","removal":"unsafe","type":"oem"},{"id":"com.android.wifi.resources.overlay.target","description":"In code founded wifi configs.\nImportant configs to WiFi.","removal":"unsafe","type":"oem"},{"id":"com.android.wifi.resources.overlay.taro","description":"Better keep this for config wifi? Better don't risk.","removal":"unsafe","type":"oem"},{"id":"com.android.wifi.resources.xiaomi","description":"Have some configs to network about: disconnected scan interval schedule\nwifi11axsupportoverride, config_wifi_tcp_buffers\nyou should keep this because it's maybe from settings.","removal":"unsafe","type":"oem"},{"id":"com.android.wm.shell","description":"Profile installer? App looks like a 'com.android.shell'.","removal":"unsafe","type":"oem"},{"id":"com.android.yadayada","description":"Disclaimer show to user, feedback, first boot setup.","removal":"delete","type":"oem"},{"id":"com.android.ztescreenshot","description":"ScreenCapture\nNeeded for screenshots.","removal":"caution","type":"oem"},{"id":"com.ape.cleanassist","label":"Smart Assist","description":"Provides junk cleaner feature for Wiko mobile","removal":"replace","suggestions":"cleaners","type":"oem"},{"id":"com.ape.cleanassistoverlay","description":"Overlay app for com.ape.cleanassist.\nIf you uninstall it, this package should also be removed.","required_by":["com.ape.cleanassist"],"removal":"delete","type":"oem"},{"id":"com.ape.easymode1","label":"Simple Mode","description":"Provides easy mode/quick access for Wiko mobile","removal":"delete","type":"oem"},{"id":"com.ape.factory","description":"Checks Build.SERIAL and ro.build.type\nAll permissions and activities (camera, mic, battery, radio, led, sim, gps, nfc). Reads/writes to External Storage","removal":"replace","type":"oem"},{"id":"com.ape.fmradio","label":"FM Radio","description":"FM Radio app for Wiko mobile","removal":"replace","suggestions":"radios","type":"oem"},{"id":"com.ape.fmradiooverlay","description":"Overlay app for com.ape.fmradio.\nIf you uninstall it, this package should also be removed.","required_by":["com.ape.fmradio"],"removal":"delete","type":"oem"},{"id":"com.ape.gamemode","description":"Provides game mode feature for Wiko mobile.","removal":"delete","type":"oem"},{"id":"com.ape.massageexpress","description":"Provides Wiko screen message feature","removal":"delete","type":"oem"},{"id":"com.ape.mtbf","description":"Named 'MTBF Tools'\nDialer Code: 68238665. Uses SQLite Database. Reads/writes to External Storage. All sorts of permissions (camera, mic, battery, radio, led, sim, gps, nfc).\nFound more details at https://twitter.com/dbauduin/status/940126704261099520 (archive: https://web.archive.org/web/20240330025312/https://twitter.com/dbauduin/status/940126704261099520)\nHardcoded URLs (an api and a book): http://www.andykhan.com/jexcelapi\nhttp://www.amazon.co.uk/exec/obidos/ASIN/0571058086/qid=1099836249/sr=1-3/ref=sr_1_11_3/202-6017285-1620664\nhttps://www.amazon.co.uk/exec/obidos/ASIN/0571058086qid=1099836249/sr=1-3/ref=sr_1_11_3/202-6017285-1620664","removal":"replace","type":"oem"},{"id":"com.ape.oneclean","label":"Oneclean","description":"Provides memory cleaner feature for Wiko mobile","removal":"replace","suggestions":"cleaners","type":"oem"},{"id":"com.ape.smartgesture","description":"Provide smart gesture feature for Wiko mobile","removal":"delete","type":"oem"},{"id":"com.ape.soundrecorder","description":"Sound recorder app for Wiko mobile","removal":"replace","suggestions":"audio_recorders","type":"oem"},{"id":"com.ape.soundrecorderoverlay","description":"Overlay app for com.ape.soundrecorder.\nIf you uninstall it, this package should be removed also.","required_by":["com.ape.soundrecorder"],"removal":"delete","type":"oem"},{"id":"com.ape.walkfit","label":"Wiko Health","description":"Walk counter and health for Wiko mobile","removal":"replace","type":"oem"},{"id":"com.ape.weatherlive","label":"Weather Live","description":"Weather app for Wiko mobile","removal":"replace","suggestions":"weather_apps","type":"oem"},{"id":"com.ape.weatherliveoverlay","description":"Overlay app for com.ape.weatherlive. If you uninstall it, this package should also be removed.","required_by":["com.ape.weatherlive"],"removal":"delete","type":"oem"},{"id":"com.ape.wikolegal","description":"Provides legal for first time setup in Wiko mobile","removal":"delete","type":"oem"},{"id":"com.ape.wikosetupwizard","description":"Provides first time app install for Wiko mobile","removal":"delete","type":"oem"},{"id":"com.apple.atve.sony.appletv","description":"Apple Tv app on android Tv. Can be removed without any problem","removal":"delete","type":"oem"},{"id":"com.arcsoft.app.humanavatar","description":"My Avatar\nAvatar activities useless.","removal":"delete","type":"oem"},{"id":"com.arcsoft.lg_avatar_resource","description":"LGAvatarReource\nIt's probably for arcamera and it's for avatars. People removed that.","removal":"delete","type":"oem"},{"id":"com.arcsoft.magicshotstudio","description":"it's app for photos","removal":"delete","type":"oem"},{"id":"com.asu","description":"ZF10 Live Wallpaper\nLive Wallpaper","removal":"delete","type":"oem"},{"id":"com.asus.UpdateLauncher","description":"Updater Launcher\nRemove if you dont need it Launcher updates.","removal":"delete","type":"oem"},{"id":"com.asus.alwayson","description":"Always-on Display\nAOD will be not available after remove.","removal":"delete","type":"oem"},{"id":"com.asus.alwayson.res.overlay","description":"Not needed gif to Always-on Display","removal":"delete","type":"oem"},{"id":"com.asus.appupdater","description":"ASUS Config Updater\nIt may have something to do with app updater but no activities.","removal":"caution","type":"oem"},{"id":"com.asus.asusbacktap","description":"Back tap\nit's for Accessibility component feature.\nBack tap will not be available in settings after remove.","removal":"delete","type":"oem"},{"id":"com.asus.asusoptiflex","description":"ASUS Opti Flex\nit's something for settings? Still unknown.","removal":"caution","type":"oem"},{"id":"com.asus.asussettingsbackuphelper","description":"Settings ASUS Backup\nCan be removed if ASUS Backup are not used.","removal":"delete","type":"oem"},{"id":"com.asus.atd.smmitest","description":"SMMI TEST\nTesting hardware things.","removal":"delete","type":"oem"},{"id":"com.asus.audiowizard","description":"ASUS AudioWizard\nNeeded for sound effects.","removal":"delete","type":"oem"},{"id":"com.asus.calculator","description":"Calculator - unit converter (https://play.google.com/store/apps/details?id=com.asus.calculator)\nHas more permissions than a Calculator app reasonably should have.\nConnects to a few Google and currency exchange-rate servers.\nhttps://beta.pithus.org/report/817514371bbdb76ec52da4c8456bbc116deec179603099deabbe6fcce6f6ccdb","removal":"delete","type":"oem"},{"id":"com.asus.camera","description":"Stock ASUS camera app. It has Google Analytics, so better disable internet for this app.","removal":"replace","type":"oem"},{"id":"com.asus.cellbroadcastreceiver.overlay","description":"useless overlay for cellbroadcast","removal":"delete","type":"oem"},{"id":"com.asus.cellbroadcastservice.overlay","description":"useless overlay for cellbroadcast","removal":"delete","type":"oem"},{"id":"com.asus.configupdater","description":"ASUS Config Updater\nSome people have removed this. Needed for ASUS autoupdate apps probably, also found debugging code.","removal":"delete","type":"oem"},{"id":"com.asus.contacts","description":"ASUS Contacts\nStock ASUS Contacts app. Contains Google trackers.","removal":"replace","type":"oem"},{"id":"com.asus.deskclock","description":"ASUS Digital Clock & Widget\nContains google analytics.","removal":"replace","type":"oem"},{"id":"com.asus.dialer","description":"ASUS Dialer\nContains Google trackers.","removal":"replace","type":"oem"},{"id":"com.asus.dm","description":"System update\nSystem updates for ASUS","removal":"caution","type":"oem"},{"id":"com.asus.easylauncher","description":"Asus Easy Mode (https://play.google.com/store/apps/details?id=com.asus.easylauncher)\nAlternative launcher with bigger icons and simpler interface","removal":"delete","type":"oem"},{"id":"com.asus.ephotoburst","description":"ASUS Burst shot viewer\nASUS Photo viewer, Trim Service. Contains Google Analytics.","removal":"delete","type":"oem"},{"id":"com.asus.faceunlockservice","description":"ASUS Face Unlock Service\nNeeded for face unlock.\nFace unlock will not work after remove.","removal":"replace","type":"oem"},{"id":"com.asus.filemanager","description":"File Manager\nStock ASUS file manager app.","removal":"replace","type":"oem"},{"id":"com.asus.focusapplistener","description":"Focus app\nNeeded for optimization, probably.","removal":"caution","type":"oem"},{"id":"com.asus.gallery","description":"Stock Gallery app. Contains Google Analytics.","removal":"delete","type":"oem"},{"id":"com.asus.gamewidget","description":"Game genie\nProbably useful for gaming.\nhttps://play.google.com/store/apps/details?id=com.asus.gamewidget","removal":"caution","type":"oem"},{"id":"com.asus.gamewidget.service","description":"Game genie service\nGame genie service needed for (`com.asus.gamewidget`)\nIt's probably useful for gaming.","removal":"caution","type":"oem"},{"id":"com.asus.hardwarestub","description":"HardwareStub Services\nHas components for optimization game.","removal":"caution","type":"oem"},{"id":"com.asus.hbm","description":"High Brightness Mode\nIt can be found probably in settings. Still unknown.","removal":"caution","type":"oem"},{"id":"com.asus.ia.asusapp","description":"My Asus (https://play.google.com/store/apps/details?id=com.asus.ia.asusapp)\nAsus service center (support + store)","removal":"delete","type":"oem"},{"id":"com.asus.imagesearch","description":"Image search\nit's so bloated.","removal":"delete","type":"oem"},{"id":"com.asus.ims.benchmarkblocker","description":"Founded debugs and benchmark blocker.","removal":"delete","type":"oem"},{"id":"com.asus.ims.brightnessservice","description":"Not needed, unused frameworks","removal":"delete","type":"oem"},{"id":"com.asus.ims.configmanager","description":"Not needed, unused frameworks","removal":"delete","type":"oem"},{"id":"com.asus.ims.devicepolicymanager","description":"Not needed, unused frameworks","removal":"delete","type":"oem"},{"id":"com.asus.ims.evtlog","description":"Not needed, unused frameworks","removal":"delete","type":"oem"},{"id":"com.asus.ims.extdispctrl","description":"Not needed, unused frameworks","removal":"delete","type":"oem"},{"id":"com.asus.ims.forcedark","description":"Not needed, unused frameworks","removal":"delete","type":"oem"},{"id":"com.asus.ims.freeform","description":"Not needed, unused frameworks","removal":"delete","type":"oem"},{"id":"com.asus.ims.observer","description":"Not needed, unused frameworks","removal":"delete","type":"oem"},{"id":"com.asus.ims.packageinstallerproxy","description":"Not needed, unused frameworks","removal":"delete","type":"oem"},{"id":"com.asus.ims.phykeyctrl","description":"Not needed, unused frameworks","removal":"delete","type":"oem"},{"id":"com.asus.ims.pointerproxy","description":"Not needed, unused frameworks","removal":"delete","type":"oem"},{"id":"com.asus.ims.powerctrl","description":"Not needed, unused frameworks","removal":"delete","type":"oem"},{"id":"com.asus.ims.rogproxy","description":"Not needed, unused frameworks","removal":"delete","type":"oem"},{"id":"com.asus.ims.smartread","description":"Not needed, unused frameworks","removal":"delete","type":"oem"},{"id":"com.asus.ims.twinapps","description":"Not needed, unused frameworks","removal":"delete","type":"oem"},{"id":"com.asus.ims.watermark","description":"Not needed, unused frameworks","removal":"delete","type":"oem"},{"id":"com.asus.inadvertentTouch","description":"Not needed 'Do not cover the top of the screen'","removal":"delete","type":"oem"},{"id":"com.asus.key_status","description":"KeyStatusTool\nKeyStatusTool? All of this code means nothing.","removal":"delete","type":"oem"},{"id":"com.asus.launcher","description":"ASUS Launcher\nContains Google Firebase Analytics.","removal":"caution","type":"oem"},{"id":"com.asus.livedemoservice","description":"Demo Mode\nEnables retail demonstration mode. it's not a feature for normal users.","removal":"delete","type":"oem"},{"id":"com.asus.lockscreen2","description":"Lockscreen\nIt should be important for lockscreen.","removal":"caution","type":"oem"},{"id":"com.asus.loguploader","description":"Log Tool\nCaptured logs, Generate Log, Report Log.","removal":"delete","type":"oem"},{"id":"com.asus.loguploaderproxy","description":"Bug Reporter Proxy\nLogs without activity.\nNot needed another logs from loguploader.","removal":"delete","type":"oem"},{"id":"com.asus.mlmodel","description":"Unused frameworks? AI optimization?\nResources have vision_model and something encrypted.\nThis app is large (338MB).","removal":"caution","type":"oem"},{"id":"com.asus.mobilemanager","description":"Mobile Manager\nNot sure to keep it or not.\nhttps://play.google.com/store/apps/details?id=com.asus.mobilemanager","removal":"caution","type":"oem"},{"id":"com.asus.mobilemanagerservice","description":"Mobile Manager Service\nLooks like a unused framework, but not sure.","removal":"caution","type":"oem"},{"id":"com.asus.nextapp","description":"NextApp\nUnused frameworks? It has something to training AI? everything is encrypted.","removal":"caution","type":"oem"},{"id":"com.asus.nextappcore","description":"NextAppCore\nUseless frameworks? It has something to training AI? everything is encrypted.","removal":"caution","type":"oem"},{"id":"com.asus.openbeta2","description":"OpenBeta2\nBeta program server that collects a lot of data.","removal":"delete","type":"oem"},{"id":"com.asus.powersaver","description":"Power Master\nIt's for power-saving and may be important.\nImportant app to powersaving.","removal":"unsafe","type":"oem"},{"id":"com.asus.sarprotection","description":"SAR Protection\nSecurity thing.","removal":"delete","type":"oem"},{"id":"com.asus.screenrecorder","description":"Screen recorder\nApp for screen recording.","removal":"replace","type":"oem"},{"id":"com.asus.setupwizard","description":"Setup Wizard\nIt's needed only for first-boot setup.","removal":"delete","type":"oem"},{"id":"com.asus.smartcover","description":"SmartCover\nUsed for smart features.","removal":"delete","type":"oem"},{"id":"com.asus.smartkey","description":"Smart key\nUsed for smart features.","removal":"delete","type":"oem"},{"id":"com.asus.smartreading","description":"Smart Reading\nIt's app for smart reading","removal":"delete","type":"oem"},{"id":"com.asus.soundrecorder","description":"ASUS Sound recorder (https://play.google.com/store/apps/details?id=com.asus.soundrecorder)\nConnects to Google Analytics and some Asus servers, which is a bit sketchy for a sound recording app..\nhttps://beta.pithus.org/report/f4cf38e1c35a04c3579fa198d2abd3ef1ff7be79633d6d3f2bc69c8a69164e1d","removal":"delete","type":"oem"},{"id":"com.asus.splendid","description":"Asus Splendid\nOptional app for adjust your screen for your own viewing pleasure.\nhttps://play.google.com/store/apps/details?id=com.asus.splendid","removal":"delete","type":"oem"},{"id":"com.asus.stitchimage.service","description":"Stitch image\nIt's for the stock gallery app.","removal":"delete","type":"oem"},{"id":"com.asus.sysmonitor","description":"Device Health\nDevice Health? It collects your data.","removal":"delete","type":"oem"},{"id":"com.asus.system.api","description":"AsusBoost\nIt's probably an important app. It has booster services and memory cleaner service.","removal":"caution","type":"oem"},{"id":"com.asus.systemcolor","description":"System color scheme\nNeeded for changing wallpapers?","removal":"caution","type":"oem"},{"id":"com.asus.taskwidget","description":"ASUS Task Manager widget\nTask manager widget to System optimization","removal":"delete","type":"oem"},{"id":"com.asus.teleserv.overlay.odm","description":"overlay to default config ims.","removal":"caution","type":"oem"},{"id":"com.asus.theme.color.asusui","description":"Theme AsusUI\nStock System Theme\nSystem may not work after removing it.","removal":"unsafe","type":"oem"},{"id":"com.asus.theme.color.rog","description":"Rog Theme\nStock System Theme\nSystem may not work after removing it.","removal":"unsafe","type":"oem"},{"id":"com.asus.themeservice","description":"Needed for change theme apps?","removal":"caution","type":"oem"},{"id":"com.asus.tips","description":"Tips\nTips for smart features and more.","removal":"delete","type":"oem"},{"id":"com.asus.twinapps","description":"Twin Apps for duplicate apps.","removal":"delete","type":"oem"},{"id":"com.asus.twinappsservice","description":"Twin Apps Service\nNeeded for Twin Apps for duplicate apps.","removal":"delete","type":"oem"},{"id":"com.asus.userfeedback","description":"ZenUI Help (https://play.google.com/store/apps/details?id=com.asus.userfeedback)\nCustomer service app that provides FAQs, Mobile care service, user feedback, and public forums.\nLots of telemetry (insecure on top of that):\nhttps://beta.pithus.org/report/e80a1fa70adc097fc9817720b5c8c81cfd156a76e6d062759b2bc3d6937a97e7","removal":"delete","type":"oem"},{"id":"com.asus.visualmaster","description":"Tru2Life\nit's app for color modes.","removal":"caution","type":"oem"},{"id":"com.asus.weathertime","description":"ASUS Weather\nit's for weather. Have Firebase Analytics.","removal":"delete","type":"oem"},{"id":"com.asus.wifi.resources.overlay","description":"overlay to wifi configs better dont risk.\nImportant configs to WiFi.","removal":"unsafe","type":"oem"},{"id":"com.asus.zenmotion","description":"Gestures\nNeeded for Gestures.","removal":"delete","type":"oem"},{"id":"com.aura.oobe.kbdi","description":"Appcloud\nPersistent notification until you click on it and agree to install games. Sort of game cloud pre-installed in some Xiaomi phones\nSafe to remove.","removal":"delete","type":"oem"},{"id":"com.autonavi.minimap","description":"高德地图 (Yeah no english translation) (https://play.google.com/store/apps/details?id=com.autonavi.minimap)\nXiaomi GPS\n","removal":"delete","type":"oem"},{"id":"com.baidu.BaiduMap","description":"Chinese Baidu Map.","removal":"delete","type":"oem"},{"id":"com.baidu.duersdk.opensdk","description":"Duer stuff from Baidu \nDuer is a virtual AI assistant.\n","removal":"delete","type":"oem"},{"id":"com.baidu.input_huawei","description":"Woh! 51 permissions! \nHuawei chinese stock input keyboard. You probably shouldn't trust this closed-source keyboard with this much permissions...NOTE: Make sure to have another keyboard installed before removing this package!\n","removal":"delete","type":"oem"},{"id":"com.baidu.input_mi","description":"Baidu IME (Baidu keyboard)\nYOU SHOULD NEVER USE A CLOSED-SOURCE KEYBOARD ! \nhttps://www.techrepublic.com/blog/asian-technology/japanese-government-warns-baidu-ime-is-spying-on-users/\nArchive : https://web.archive.org/save/https://www.techrepublic.com/blog/asian-technology/japanese-government-warns-baidu-ime-is-spying-on-users/\nNOTE: Make sure you have installed another keyboard before removing this package.\n","removal":"replace","type":"oem"},{"id":"com.baidu.input_vivo","description":"Default keyboard (Baidu IME customized for Vivo devices).\nThe number of requested permissions for this keyboard is terrifying. You really should use another keyboard. Pithus analysis: https://beta.pithus.org/report/d4cdf8fedcd94436ade720cb8df9b4ef32aca6c7822cae6c8698937d68e20363","removal":"replace","type":"oem"},{"id":"com.baidu.location.fused","description":"FusedLocation\nChinese baidu location.","removal":"delete","type":"oem"},{"id":"com.baidu.map.location","description":"Chinese Network location baidu. Only for China.","removal":"delete","type":"oem"},{"id":"com.baidu.searchbox","description":"百度 (https://play.google.com/store/apps/details?id=com.baidu.searchbox)\nBaidu App search engine.\n","removal":"delete","type":"oem"},{"id":"com.bbk.SuperPowerSave","description":"Super Battery Saver\nNot sure if it's useful or not. Super power save.","removal":"delete","type":"oem"},{"id":"com.bbk.account","description":"Vivo account\nVivo privacy policy is really bad: https://privacy.vivo.com/privacy\nNote: Removing this will obviously break fuctions that require Vivo account authentication: accessibility, data backup etc.","removal":"caution","type":"oem"},{"id":"com.bbk.appstore","description":"Vivo app store.\nNote: apps from this store can still be upgraded with the built-in check upgrade feature even with this package removed","removal":"replace","type":"oem"},{"id":"com.bbk.calendar","description":"Default calendar app.\n50 permissions for a calendar app. What could go wrong?\n\nPithus analysis: https://beta.pithus.org/report/db107cb828a1ec9b7cbcd9fd86542da877fdf4cf947c18c8a48a2b09e568ad10","removal":"replace","type":"oem"},{"id":"com.bbk.cloud","description":"vivoCloud\nVivo cloud services.","removal":"delete","type":"oem"},{"id":"com.bbk.facewake","description":"FaceWake\nIt's used for face unlock.","removal":"caution","type":"oem"},{"id":"com.bbk.iqoo.logsystem","description":"User experience service\nTelemetry app.\nNote:Disabling this will break trial version system upgrade feature.","removal":"delete","type":"oem"},{"id":"com.bbk.launcher2","description":"Vivo launcher.","removal":"caution","type":"oem"},{"id":"com.bbk.photoframewidget","description":"Photo frame\nPhoto widget.","removal":"delete","type":"oem"},{"id":"com.bbk.scene.databaseprovider","description":"May be needed for the launcher, not sure.","removal":"caution","type":"oem"},{"id":"com.bbk.scene.launcher.theme","description":"SceneThemeLauncher\nMay be needed for the launcher, not sure.","removal":"caution","type":"oem"},{"id":"com.bbk.theme","description":"Vivo theme (https://play.google.com/store/apps/details?id=com.bbk.theme)\nLets you add new themes, fonts and wallpapers.\nIt has annoying notifications that cannot be disabled by going to the app settings. This app use 50 permissions and can install packages (REQUEST_INSTALL_PACKAGES)\nNote: Removing this app will prevent you to change themes.\n\nPithus analysis: https://beta.pithus.org/report/0f15055131637d3dbc55d3a49b8e79b4f76ca09871abf9eb43b5f88afde11800","removal":"delete","type":"oem"},{"id":"com.bbk.theme.resources","description":"WallpaperRes\nVivo wallpapers. A lot of PNG wallpapers. Safe to remove if you don't need theme resources.","removal":"caution","type":"oem"},{"id":"com.bbk.updater","description":"System update\nProvides system updates.","removal":"caution","type":"oem"},{"id":"com.bjbyhd.screenreader_huawei","description":"An accessibility feature for visually impaired people\n","removal":"delete","type":"oem"},{"id":"com.bluetooth.aptxmode","description":"Hidden aptX ALS Audio Bluetooth sample improvement from Qualcomm. Useless 96kHz sample.","removal":"delete","type":"oem"},{"id":"com.bsp.catchlog","description":"bsp = Board support package\nUsed to catch log files obviously.\n","removal":"delete","type":"oem"},{"id":"com.casper.turkiye","description":"Support app for Casper, see https://play.google.com/store/apps/details?id=com.casper.turkiye&hl=en","removal":"delete","type":"oem"},{"id":"com.cdfinger.factorytest","description":"Fingerprint Test App.","removal":"delete","type":"oem"},{"id":"com.chaozhuo.filemanager","label":"File Manager","description":"Adware file manager aka 'CZ File Manager' that mostly came preinstalled on Lenovo Tablets.\nFile management is a basic necessity, make sure you install a third-party file manager for this purpose.\n","web":["https://forums.lenovo.com/t5/Security-Malware/Tab4-Preinstalled-file-manager-has-Adware-trying-to-get-your/td-p/4355222","https://beta.pithus.org/report/02b0b4c60941960d3ac82177df0e5e93f61ee0f5c181ba50668d4ab0dae8d508"],"removal":"replace","warning":"You must allow 'location & phone calls' permission to run.","suggestions":"file_managers","type":"oem"},{"id":"com.chsc.semitouchtester","description":"Semi touch test, debug.","removal":"delete","type":"oem"},{"id":"com.cleanmaster.sdk","description":"(discontinued) old clean master app that cleans phone.","removal":"delete","type":"oem"},{"id":"com.cnn.mobile.android.phone.edgepanel","description":"CNN Edge panel. Twitter trends, and news from CNN.\n","removal":"delete","type":"oem"},{"id":"com.codeaurora.fmradio","label":"FM Radio","description":"Default FM app for lenovo devices.","removal":"replace","suggestions":"radios","type":"oem"},{"id":"com.color.uiengine","description":"Needed for themes","removal":"caution","type":"oem"},{"id":"com.coloros.accessibilityassistant","description":"Another thing for smart assistant","removal":"delete","type":"oem"},{"id":"com.coloros.activation","label":"E-warranty card","description":"Lets you check if your registered phone is still under warranty (will send your IMEI to 'esa-reg-eup.myoppo.com'). Has a lot of permissions and run at boot.","web":["https://beta.pithus.org/report/2a1dc5caedd2347fa009563e9b4d1c11b1cb42726f9046151934c456fdd77d88"],"removal":"caution","type":"oem"},{"id":"com.coloros.activation.overlay.common","description":"E-warranty card\nLets you check if your registered phone is still under warranty. Has a lot of permissions and runs at boot. Pithus analysis: https://beta.pithus.org/report/2a1dc5caedd2347fa009563e9b4d1c11b1cb42726f9046151934c456fdd77d88","removal":"replace","type":"oem"},{"id":"com.coloros.alarmclock","description":"Stock Clock app.","removal":"replace","type":"oem"},{"id":"com.coloros.appmanager","label":"Permanent process management","description":"Used to uninstall applications, but uninstalling apps works nonetheless, even after uninstalling this package.","removal":"caution","warning":"GameSpace's game assistant does not work after uninstalling this package.","type":"oem"},{"id":"com.coloros.apprecover","description":"Used for app recovery?","removal":"delete","type":"oem"},{"id":"com.coloros.assistantscreen","label":"Shelf","description":"Previously Breeno, developed by HeyTap. ColorOS 'At a Glance'.\nShelf provides a variety of widgets that let you access important information or services from apps with ease. Swipe down on the Home screen to enter Shelf.","web":["https://play.google.com/store/apps/details?id=com.coloros.assistantscreen"],"removal":"delete","type":"oem"},{"id":"com.coloros.athena","description":"Memory management, maybe useful, maybe not","removal":"caution","type":"oem"},{"id":"com.coloros.athena.config_plugin","description":"Needed to configure com.coloros.athena. Memory management, maybe useful, maybe not.","removal":"caution","type":"oem"},{"id":"com.coloros.avastofferwall","description":"Avast Offerwall\nAvast anti-virus things.","removal":"delete","type":"oem"},{"id":"com.coloros.backuprestore","label":"OPPO Clone Phone","description":"Oppo backup/restore tool.","web":["https://play.google.com/store/apps/details?id=com.coloros.backuprestore"],"removal":"replace","suggestions":"backup_apps","type":"oem"},{"id":"com.coloros.backuprestore.remoteservice","description":"It has something to scan always Wi-Fi but it's probably unused.","removal":"delete","type":"oem"},{"id":"com.coloros.blacklistapp","description":"Blacklist Contacts\nBlack list contacts, blocking calling.","removal":"caution","type":"oem"},{"id":"com.coloros.bootreg","description":"Setup Wizard\nCause bootloop after uninstall app.","removal":"unsafe","type":"oem"},{"id":"com.coloros.calculator","description":"Calculator app\nHas internet access because it can convert exchange rate, but I don't know whether it's used for other things as well or not.","removal":"delete","type":"oem"},{"id":"com.coloros.childrenspace","label":"Kid Space","description":"Limit time spending on apps for kids.","removal":"delete","type":"oem"},{"id":"com.coloros.cloud","label":"OPPO Cloud","description":"Oppo cloud storage?","removal":"replace","suggestions":"cloud_services","type":"oem"},{"id":"com.coloros.codebook","description":"Password manager\nApp where you can keep passwords.","removal":"delete","type":"oem"},{"id":"com.coloros.colordirectservice","description":"Screen Recognition (Breeno Touch).\nThis app is used to recognize screen contents and provide quick access to relevant services (like Text Extraction, AI Summarizer). com.coloros.ocrscanner, com.coloros.ocrservice, com.oplus.ocs, com.oplus.aiunit, com.coloros.ocs.opencapabilityservice are also needed for this to work.","dependencies":["com.coloros.ocrscanner","com.coloros.ocrservice","com.oplus.ocs","com.oplus.aiunit","com.coloros.ocs.opencapabilityservice"],"removal":"delete","type":"oem"},{"id":"com.coloros.colorfilestand","description":"It has only Request permission activity. Probably useless framework also it has something to do with cloud.","removal":"delete","type":"oem"},{"id":"com.coloros.compass2","label":"Compass","description":"ColorOS default compass app\nKeep in mind that by using this app you give your location to the weather Oppo servers.","web":["https://beta.pithus.org/report/9a965f5587fa6ee21c526612f3d72c50ef3cc53679b741260298387c44f5a3dc"],"removal":"replace","type":"oem"},{"id":"com.coloros.digitalwellbeing","description":"App usage time that can be found in settings.","removal":"replace","type":"oem"},{"id":"com.coloros.directui","label":"Breeno Touch","description":"Smart things it can be found in settings.","removal":"delete","type":"oem"},{"id":"com.coloros.dirrecord","description":"Security features, anti-trojan?","removal":"delete","type":"oem"},{"id":"com.coloros.encryption","description":"Private Safe\nRemoving breaks the stock launcher? Private Safe (located in Privacy Settings)","removal":"caution","type":"oem"},{"id":"com.coloros.exserviceui","description":"Edge touch\nGesture-related things. Edge touch","removal":"delete","type":"oem"},{"id":"com.coloros.exsystemservice","label":"OplusExSystemService","description":"Responsible for gesture-based features, such as double tap to wake up and possibly other motion or gesture recognition services, essential for some display and wake/sleep functionalitiesResponsible for gesture-based features, such as double tap to wake up and possibly other motion or gesture recognition services, essential for some display and wake/sleep functionalities. The service is designed to auto-start and run persistently at boot, ensuring gesture features always work as expected. It also includes an accessibility activity, potentially to support features for users with disabilities or for system interaction enhancements","removal":"caution","warning":"Disabling or uninstalling this package may break core functions such as double-tap-to-wake or other gesture-based features.","type":"oem"},{"id":"com.coloros.eyeprotect","description":"It's the only eye protection option","removal":"delete","type":"oem"},{"id":"com.coloros.feedback","description":"Feedback service\nFeedback for OPPO","removal":"delete","type":"oem"},{"id":"com.coloros.filemanager","label":"My Files","description":"OPPO's official file management app","removal":"replace","suggestions":"file_managers","type":"oem"},{"id":"com.coloros.findmyphone","label":"Find My","description":"Previously Find My Phone Service. Oppo's find my phone service. Logout from OPPO account before removing.","removal":"replace","suggestions":"locators","type":"oem"},{"id":"com.coloros.findphone.client2","description":"Find phone\nFind phone, logout from oppo account before","removal":"caution","type":"oem"},{"id":"com.coloros.floatassistant","description":"GA ball, Assistive ball feature.","removal":"delete","type":"oem"},{"id":"com.coloros.focusmode","description":"Focus mode\nit's option that can be found in settings.","removal":"delete","type":"oem"},{"id":"com.coloros.gallery3d","description":"Gallery\nStock gallery app.","removal":"replace","type":"oem"},{"id":"com.coloros.gamespace","label":"Game Space","description":"Previously APP Enhancement Services. Hub for your Games + some performance optimizations","web":["https://community.coloros.com/thread-9962-1-1.html"],"removal":"delete","type":"oem"},{"id":"com.coloros.gamespaceui","label":"Game Space","description":"Gaming utility aiming at 'optimizing your gaming experience'. Has a lot of permissions. For instance, it has internet access, will scans all the apps you have on your phones (to find games), can performs Bluetooth scan and has access to the metadata of your media files (e.g the place where you took a picture).","removal":"delete","type":"oem"},{"id":"com.coloros.gesture","description":"Gestures & Motions\nSomatic gestures (double tap to light up the screen)","removal":"delete","type":"oem"},{"id":"com.coloros.healthcheck","label":"Diagnostics","description":"Previously Quick Check. Health check? Probably hidden.","removal":"delete","type":"oem"},{"id":"com.coloros.healthservice","description":"Health service\nAnother thing related to health check","removal":"delete","type":"oem"},{"id":"com.coloros.karaoke","description":"Karaoke\nKaraoke (Do not delete if you need Karaoke)","removal":"delete","type":"oem"},{"id":"com.coloros.launcher.layout","description":"Useless code that means nothing for the launcher","removal":"delete","type":"oem"},{"id":"com.coloros.lockassistant","description":"You can't remove this app using adb.","removal":"unsafe","type":"oem"},{"id":"com.coloros.logkit","description":"LogKit\nLogs, Dial *#800# for running it","removal":"delete","type":"oem"},{"id":"com.coloros.logkit.plugin.upload","description":"Useless plugin for logs","removal":"delete","type":"oem"},{"id":"com.coloros.mapcom.frame","description":"Useless frameworks","removal":"delete","type":"oem"},{"id":"com.coloros.mcs","label":"System Messages","description":"More info needed.","removal":"delete","type":"oem"},{"id":"com.coloros.musiclink","description":"Music Party\nThis app allows you to play music through different phones, synchronised.\nhttps://www.youtube.com/watch?v=3Ak222Z79RY","removal":"replace","type":"oem"},{"id":"com.coloros.notificationmanager","description":"Needed for notification manager and scheduled do not disturb.","removal":"caution","type":"oem"},{"id":"com.coloros.ocrscanner","label":"Breeno Scan","description":"ColorOS Optical character recognition scanner","removal":"replace","type":"oem"},{"id":"com.coloros.ocrservice","description":"Extract text from images.","removal":"delete","type":"oem"},{"id":"com.coloros.ocs.opencapabilityservice","description":"This service acts as a core system capability provider on Oppo, Realme, and related devices, enabling or mediating various inter-app or system-level features.\nIt's also used for live alerts by front facing camera/status bar for iPhone-like style music widget (for Spotify), battery charge indicator, flashlight indicator, and more.","removal":"caution","warning":"If removed, live alerts will stop working.","type":"oem"},{"id":"com.coloros.onekeylockscreen","label":"Screen Lock","description":"Lock your phone if you click on the app icon. Completely useless unless your physical power button is damaged.\nThis app still has the permission to list all the apps installed on the phone.","web":["https://beta.pithus.org/report/ece4088357c0a47dffd96bdc46a7b535d448c1a3619d995f7032df3be6cb0a38"],"removal":"delete","type":"oem"},{"id":"com.coloros.operationManual","description":"Help & feedback\nHelp (in Settings - Other Settings)","removal":"delete","type":"oem"},{"id":"com.coloros.operationtips","description":"Tips","removal":"delete","type":"oem"},{"id":"com.coloros.oppoguardelf","description":"Needed for power-saving and security","removal":"caution","type":"oem"},{"id":"com.coloros.oppoguardelf.restrict_plugin","description":"Needed components for com.coloros.oppoguardelf. Needed for power-saving and security. Also, the plugin is bloated with logs and statistics.","removal":"caution","type":"oem"},{"id":"com.coloros.oppoguardelf.secretplugin","description":"Needed components for com.coloros.oppoguardelf. Needed for power-saving and security. Also, the plugin is bloated with logs and statistics.","removal":"caution","type":"oem"},{"id":"com.coloros.oppomultiapp","label":"App Cloner","description":"Needed for app clone feature.","removal":"delete","type":"oem"},{"id":"com.coloros.oshare","label":"OnePlus Share","description":"Previously OPPO Share. File sharing app to transfer data from/to Oppo devices only. Seems to use weak crypto (AES ECB mode) and has weird permissions (such as `READ_CONTACTS`).","web":["https://beta.pithus.org/report/170f4a14be24a2e2135cd956a038aae9e2f78c845f3161b84c5545dbec03fad9"],"removal":"caution","warning":"Removing this app will break the functionality to share photos directly from ColorOS Photos app and break the 'share with' prompt after taking a screenshot.","type":"oem"},{"id":"com.coloros.phonemanager","label":"Phone Manager","description":"Provides so called 'optimization tools' and various security scanning services.\nThese virus scanning services may have privacy implications.","web":["https://beta.pithus.org/report/6b7d9e117ffb600b852f3785ede4f3773385fc291376e94a061bf7ed787dec48"],"removal":"delete","type":"oem"},{"id":"com.coloros.phonenoareainquire","label":"Number Origin","description":"More info needed.","removal":"delete","type":"oem"},{"id":"com.coloros.pictorial","description":"LockscreenMagazine\nRemoval will result in no longer being able to access Lockscreen settings.\n","removal":"unsafe","type":"oem"},{"id":"com.coloros.prome.service","description":"Useless frameworks. About feedback and smart touch.","removal":"delete","type":"oem"},{"id":"com.coloros.prome.smsservice","description":"Useless SMS frameworks.","removal":"delete","type":"oem"},{"id":"com.coloros.recents","description":"Recent apps. Provides navigation to alternate between multiple apps. Removal will result in no \"Recent App\" list and re-enablement will cause a soft reload.","removal":"caution","type":"oem"},{"id":"com.coloros.regservice","description":"Mobile DM authentication related (recommended to be uninstalled)","removal":"delete","type":"oem"},{"id":"com.coloros.remoteguardservice","description":"Remote Guard Service\nHave only useless statistics and depends on: com.coloros.mcs, com.heytap.mcs ONLY FOR CHINA.","removal":"delete","type":"oem"},{"id":"com.coloros.safecenter","description":"Security Center\nIt breaks 'display over other apps' permission!","removal":"unsafe","type":"oem"},{"id":"com.coloros.safecenter.config_plugin","description":"This plugin doesn't have any important things. It's safe to remove, useless logs code.","removal":"delete","type":"oem"},{"id":"com.coloros.safesdkproxy","description":"It has cleaner and security things.","removal":"delete","type":"oem"},{"id":"com.coloros.sau","description":"System Upgrade Services\nNeeded to update ColorOS.","removal":"caution","type":"oem"},{"id":"com.coloros.sauhelper","description":"SAUHelper\nIt has only statistics and logs.","removal":"delete","type":"oem"},{"id":"com.coloros.scenemode","description":"Scene mode and simple mode","removal":"delete","type":"oem"},{"id":"com.coloros.sceneservice","description":"Data Services Platform\nData Services Platform (the entire Breeno option in the settings menu disappears after deletion)","removal":"delete","type":"oem"},{"id":"com.coloros.screenrecorder","description":"Screen recorder\nStock screen recording app.","removal":"replace","type":"oem"},{"id":"com.coloros.screenshot","description":"Screenshot\nNeeded for screenshots. Very useful app.","removal":"caution","type":"oem"},{"id":"com.coloros.securepay","label":"Payment Protection","description":"Payment system from Oppo allowing you to pay with your phone.","web":["https://beta.pithus.org/report/65246664d3795a5ac1b402d28456903e1b3bd76176de8298b3ea96c6c592ae9a"],"removal":"delete","type":"oem"},{"id":"com.coloros.securityguard","description":"Security Events (located in Phone Manager - Security Tools, it is recommended to uninstall it).","removal":"delete","type":"oem"},{"id":"com.coloros.securitykeyboard","description":"Secure keyboard\nNot needed if your keyboard is already good.","removal":"delete","type":"oem"},{"id":"com.coloros.securitypermission","description":"Handles app permission management. DO NOT REMOVE THIS\n","removal":"unsafe","type":"oem"},{"id":"com.coloros.sharescreen","description":"Shared screen assistance\nShared screen assistance (nobody need it)","removal":"delete","type":"oem"},{"id":"com.coloros.simsettings","description":"Data usage\nData usage and OTA updates","removal":"caution","type":"oem"},{"id":"com.coloros.smartdrive","label":"Driving mode","description":"Previously Breeno Driving, and Smart Driving. It's for smart things.","removal":"delete","type":"oem"},{"id":"com.coloros.smartlock","description":"\"Phone stays unlocked when using a wearable device\" feature support component.","removal":"delete","type":"oem"},{"id":"com.coloros.smartsidebar","description":"Smart Sidebar\nEdge panel settings","removal":"delete","type":"oem"},{"id":"com.coloros.soundrecorder","label":"Recorder","description":"ColorOS default Sound Recorder","removal":"replace","suggestions":"audio_recorders","type":"oem"},{"id":"com.coloros.speechassist","description":"ColorOS default Speech Assistant","removal":"replace","suggestions":"tts","type":"oem"},{"id":"com.coloros.systemclone","label":"System Cloner","description":"Creates multiple users on device","removal":"replace","suggestions":"sandboxing_apps","type":"oem"},{"id":"com.coloros.trafficlimit","description":"Useless frameworks","removal":"delete","type":"oem"},{"id":"com.coloros.translate.engine","description":"Used for translation?","removal":"delete","type":"oem"},{"id":"com.coloros.uxdesign","description":"Needed for themes","removal":"caution","type":"oem"},{"id":"com.coloros.video","label":"Video","description":"Default Oppo video player with too much permissions (21) for a video player!","web":["https://beta.pithus.org/report/4ceb96c23ad0e26ee8eceab293d251f8b1bddaf4a901741ee467e0bb867db6e9"],"removal":"replace","warning":"Using inbuilt screen recorder you won't be able to open the recorded video from the notification view.","suggestions":"video_players","type":"oem"},{"id":"com.coloros.wallet","label":"com.coloros.wallet","description":"Oppo default Wallet app.","removal":"delete","type":"oem"},{"id":"com.coloros.wallpapers","description":"Wallpapers\nNeeded for wallpapers","removal":"caution","type":"oem"},{"id":"com.coloros.weather.service","description":"Name indicates it's for the weather, but removing this causes the screen to flash and the phone will eventually become unresponsive.\nhttps://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/issues/585","removal":"unsafe","type":"oem"},{"id":"com.coloros.weather2","label":"Weather","description":"ColorOS weather app.You should try, several users removed this app without any trouble on Oppo/Realme device with Android 11+.","web":["https://play.google.com/store/apps/details?id=com.coloros.weather2","https://github.com/0x192/universal-android-debloater/issues/211"],"removal":"unsafe","warning":"Removal seems to trigger a bootloop on some phones.","suggestions":"weather_apps","type":"oem"},{"id":"com.coloros.widget.smallweather","label":"Clock","description":"More info needed.","removal":"replace","suggestions":"weather_apps","type":"oem"},{"id":"com.coloros.wifibackuprestore","label":"WifiBackupRestore","description":"Backs up Wi-Fi access points and credentials to the cloud","removal":"caution","warning":"Removal would cause the Backup and Restore to unable to backup locally-stored Wi-Fi access points and credentials.","type":"oem"},{"id":"com.coloros.wifisecuredetect","description":"Useless for Wi-Fi. Has captcha and mobile number verification.","removal":"delete","type":"oem"},{"id":"com.coloros.wirelesssettings","description":"Wireless settings\nProbably needed.","removal":"caution","type":"oem"},{"id":"com.coremobility.app.vnotes","label":"Sprint Voicemail","description":"More info needed.","removal":"replace","type":"oem"},{"id":"com.daemon.shelper","description":"Shelper\nTracking, monitoring, logs","removal":"delete","type":"oem"},{"id":"com.data.overlay.base.s600ww","label":"com.data.overlay.base.s600ww","description":"Some kind of theme overlay for Nokia devices?\nSome users claim to not see any differences when removed.","removal":"caution","type":"oem"},{"id":"com.detection.ts.noise_detection_apk","description":"It's used by bsptest (com.vivo.bsptest) for noise detection test.","removal":"delete","type":"oem"},{"id":"com.detection.ts.touch_detection","description":"It's used by bsptest (com.vivo.bsptest) for touch detection test.","removal":"delete","type":"oem"},{"id":"com.diotek.diodict4.EDictionary","description":"Dictionary app, only for japanese and korea","removal":"delete","type":"oem"},{"id":"com.dirac.acs","description":"Dirac Control Service\nFor audio control, not sure if it's useful.","removal":"caution","type":"oem"},{"id":"com.dragon.read","description":"Chinese Unknown partner app from Miui China.","removal":"delete","type":"oem"},{"id":"com.droidlogic","description":"DroidBtPair\nLooks like an important app for the TV to work properly.","removal":"unsafe","type":"oem"},{"id":"com.dropbox.android","description":"dropbox app\nhttps://play.google.com/store/apps/details?id=com.dropbox.android","removal":"delete","type":"oem"},{"id":"com.dropboxchmod","description":"I found logs in code. This app means nothing.","removal":"delete","type":"oem"},{"id":"com.dti.lenovo.tablet","label":"Mobile Services","description":"Silently installs unwanted packages without user permission.\n","web":["https://www.andrewnile.co.uk/blog/lenovo-mobile-services/","https://beta.pithus.org/report/4ebcc8b5a13851054d85153b0d4086e4b2b5adaee66eaea5843078c6134e9dd7"],"removal":"delete","type":"oem"},{"id":"com.dts.dtsxultra","description":"Audio Effect for car, headphones. You don't need that.","removal":"delete","type":"oem"},{"id":"com.duokan.phone.remotecontroller","label":"Mi Remote","description":"Control your electric appliances with your phone using Mi Remote.","web":["https://play.google.com/store/apps/details?id=com.duokan.phone.remotecontroller"],"removal":"delete","type":"oem"},{"id":"com.duokan.phone.remotecontroller.peel.plugin","label":"Peel Mi Remote","description":"Peel Mi Remote is a TV guide extension for Xiaomi Mi Remote by \"Peel Smart Remote\".","web":["https://play.google.com/store/apps/details?id=com.duokan.phone.remotecontroller.peel.plugin"],"removal":"delete","type":"oem"},{"id":"com.duokan.reader","description":"MIUIDuokanReader\nChinese app that has too much tracking and ads.\nMay be uninstalled without ADB.","removal":"delete","type":"oem"},{"id":"com.eg.android.AlipayGphone","description":"Chinese Alipay app.","removal":"delete","type":"oem"},{"id":"com.elephanttek.faceunlock","label":"com.elephanttek.faceunlock","description":"Standard FaceUnlock functionality?\nUnlock your device by simply looking at the display.\nFace unlock is bad for security and privacy.","web":["https://www.ubergizmo.com/2017/03/galaxy-s8-facial-unlock-photograph/","https://www.kaspersky.com/blog/face-unlock-insecurity/21618/","https://www.freecodecamp.org/news/why-you-should-never-unlock-your-phone-with-your-face-79c07772a28/"],"removal":"caution","type":"oem"},{"id":"com.enhance.gameservice","label":"Samsung Game Optimizing Service","description":"Previously GameMode.\nLegacy game Optimizing Service (is replaced by com.samsung.android.game.gos)\nIs supposed to \"improve\" game performance.","removal":"delete","type":"oem"},{"id":"com.epicgames.portal","description":"Epic Games for Android","removal":"delete","type":"oem"},{"id":"com.evenwell.AprUploadService","description":"Apr Upload Service ???? [MORE INFO NEEDED]","removal":"caution","type":"oem"},{"id":"com.evenwell.AprUploadService.data.overlay.base","description":"Theme overlay for Apr Upload Service?","removal":"caution","type":"oem"},{"id":"com.evenwell.AprUploadService.data.overlay.base.s600id","description":"Theme overlay for Apr Upload Service?","removal":"caution","type":"oem"},{"id":"com.evenwell.AprUploadService.data.overlay.base.s600ww","description":"Theme overlay for Apr Upload Service?","removal":"caution","type":"oem"},{"id":"com.evenwell.CPClient","description":"CP = Client Provisioning.\nSurely used to push new carrier internet/MMS settings automatically\nMaybe it's useful if carriers change their APN... but you still can change it manually, it's not difficult.\n","removal":"replace","type":"oem"},{"id":"com.evenwell.CPClient.overlay.base","description":"Theme overlay for CPClient?","removal":"caution","type":"oem"},{"id":"com.evenwell.CPClient.overlay.base.s600id","description":"Theme overlay for CPClient?","removal":"caution","type":"oem"},{"id":"com.evenwell.CPClient.overlay.base.s600ww","description":"Theme overlay for CPClient?","removal":"caution","type":"oem"},{"id":"com.evenwell.DbgCfgTool","description":"Debug Config Tool?","removal":"delete","type":"oem"},{"id":"com.evenwell.DbgCfgTool.overlay.base","description":"Theme overlay for Debug Config Tool?","removal":"caution","type":"oem"},{"id":"com.evenwell.DbgCfgTool.overlay.base.s600id","description":"Theme overlay for Debug Config Tool?","removal":"caution","type":"oem"},{"id":"com.evenwell.DbgCfgTool.overlay.base.s600ww","description":"Theme overlay for Debug Config Tool?","removal":"caution","type":"oem"},{"id":"com.evenwell.DeviceMonitorControl","description":"Some form of device monitoring?","removal":"replace","type":"oem"},{"id":"com.evenwell.DeviceMonitorControl.data.overlay.base","description":"Theme overlay for Device Monitor Control?","removal":"caution","type":"oem"},{"id":"com.evenwell.DeviceMonitorControl.data.overlay.base.s600id","description":"Theme overlay for Device Monitor Control?","removal":"caution","type":"oem"},{"id":"com.evenwell.DeviceMonitorControl.data.overlay.base.s600ww","description":"Theme overlay for Device Monitor Control?","removal":"caution","type":"oem"},{"id":"com.evenwell.OTAUpdate.overlay.base.s600ww","description":"Theme overlay for OTA Update UI?","removal":"caution","type":"oem"},{"id":"com.evenwell.PowerMonitor","description":"Drains more battery than it saves.","removal":"delete","type":"oem"},{"id":"com.evenwell.PowerMonitor.overlay.base","description":"Theme overlay for Power Monitor?","removal":"caution","type":"oem"},{"id":"com.evenwell.PowerMonitor.overlay.base.s600id","description":"Theme overlay for Power Monitor?","removal":"caution","type":"oem"},{"id":"com.evenwell.PowerMonitor.overlay.base.s600ww","description":"Theme overlay for Power Monitor?","removal":"caution","type":"oem"},{"id":"com.evenwell.SettingsUtils","description":"Settings utils\n(crappy) Audio rendering. \nSee https://gitlab.com/W1nst0n/universal-android-debloater/-/issues/9#note_369056538\n","removal":"delete","type":"oem"},{"id":"com.evenwell.SettingsUtils.overlay.base.s600ww","description":"Theme overlay for SettingsUtils?","removal":"caution","type":"oem"},{"id":"com.evenwell.SetupWizard","description":"The first-boot device setup wizard for new/factory reset devices.","removal":"delete","type":"oem"},{"id":"com.evenwell.SetupWizard.overlay.base","description":"Theme overlay for Setup Wizard?","removal":"caution","type":"oem"},{"id":"com.evenwell.SetupWizard.overlay.base.s600ww","description":"Theme overlay for Setup Wizard?","removal":"caution","type":"oem"},{"id":"com.evenwell.SetupWizard.overlay.d.base.s600ww","description":"Theme overlay for Setup Wizard?","removal":"caution","type":"oem"},{"id":"com.evenwell.UsageStatsLogReceiver","description":"Logging stuff","removal":"replace","type":"oem"},{"id":"com.evenwell.UsageStatsLogReceiver.data.overlay.back.s600id","description":"Theme overlay for Usage Stats Log?","removal":"caution","type":"oem"},{"id":"com.evenwell.UsageStatsLogReceiver.data.overlay.base.s600ww","description":"Theme overlay for Usage Stats Log?","removal":"caution","type":"oem"},{"id":"com.evenwell.apnwidget.overlay.base.s600ww","description":"Some overlay for an APN widget. Overlays are usually themes.\nAPN means Access Point Name and must be configured with carrier values in order for your device to acess the carrier's network.","removal":"caution","type":"oem"},{"id":"com.evenwell.autoregistration","label":"AautoRegistration","description":"Spyware app which sends warranty details to China.","web":["https://milankragujevic.com/the-trade-of-privacy-for-convenience","https://archive.is/https://nitter.privacydev.net/drwetter/status/1108801189662130176"],"removal":"delete","type":"oem"},{"id":"com.evenwell.autoregistration.overlay.base","description":"Theme overlay for a Spyware app?","removal":"caution","type":"oem"},{"id":"com.evenwell.autoregistration.overlay.base.s600id","description":"Theme overlay for a Spyware app?","removal":"caution","type":"oem"},{"id":"com.evenwell.autoregistration.overlay.base.s600ww","description":"Theme overlay for a Spyware app?","removal":"caution","type":"oem"},{"id":"com.evenwell.autoregistration.overlay.d.base.s600id","description":"Theme overlay for a Spyware app?","removal":"caution","type":"oem"},{"id":"com.evenwell.autoregistration.overlay.d.base.s600ww","description":"Theme overlay for a Spyware app?","removal":"caution","type":"oem"},{"id":"com.evenwell.batteryprotect","description":"Battery protect is advertised to improve battery performance, but in practice it drains your battery and kills apps aggressively.\nhttps://dontkillmyapp.com/nokia\nNokia decided to stop using this app-killer in the future:\nhttps://www.androidpolice.com/2019/08/27/nokia-hmd-phones-disable-evenwell-background-process-app-killer/","removal":"delete","type":"oem"},{"id":"com.evenwell.batteryprotect.overlay.base","description":"Theme overlay for Battery Protect?","removal":"caution","type":"oem"},{"id":"com.evenwell.batteryprotect.overlay.base.s600id","description":"Theme overlay for Battery Protect?","removal":"caution","type":"oem"},{"id":"com.evenwell.batteryprotect.overlay.base.s600ww","description":"Theme overlay for Battery Protect?","removal":"caution","type":"oem"},{"id":"com.evenwell.batteryprotect.overlay.d.base.s600e0","description":"Theme overlay for Battery Protect?","removal":"caution","type":"oem"},{"id":"com.evenwell.bboxsbox","description":"??? [MORE INFO NEEDED]","removal":"delete","type":"oem"},{"id":"com.evenwell.bboxsbox.app","description":"????\n","removal":"delete","type":"oem"},{"id":"com.evenwell.bokeheditor","description":"Probably related to adding fake bokeh (a focus blur effect) to photos.","removal":"replace","type":"oem"},{"id":"com.evenwell.bokeheditor.overlay.base.s600ww","description":"Theme overlay for Bokeh Editor?","removal":"caution","type":"oem"},{"id":"com.evenwell.camera2","description":"Nokia camera by evenwell (https://play.google.com/store/apps/details?id=com.evenwell.camera2)\n","removal":"replace","type":"oem"},{"id":"com.evenwell.custmanager","description":"Customer manager\nGiven its name I'd say it is useless but I don't have more info.\n","removal":"replace","type":"oem"},{"id":"com.evenwell.custmanager.data.overlay.base","description":"Theme overlay for Customer Manager?","removal":"caution","type":"oem"},{"id":"com.evenwell.custmanager.data.overlay.base.s600id","description":"Theme overlay for Customer Manager?","removal":"caution","type":"oem"},{"id":"com.evenwell.custmanager.data.overlay.base.s600ww","description":"Theme overlay for Customer Manager?","removal":"caution","type":"oem"},{"id":"com.evenwell.customerfeedback.overlay.base.s600ww","description":"Theme overlay for Customer Feedback?","removal":"caution","type":"oem"},{"id":"com.evenwell.dataagent","description":"Data agent\nUsed for backup/restore? [MORE INFO NEEDED]","removal":"replace","type":"oem"},{"id":"com.evenwell.dataagent.overlay.base","description":"Theme overlay for Data Agent?","removal":"caution","type":"oem"},{"id":"com.evenwell.dataagent.overlay.base.s600id","description":"Theme overlay for Data Agent?","removal":"caution","type":"oem"},{"id":"com.evenwell.dataagent.overlay.base.s600ww","description":"Theme overlay for Data Agent?","removal":"caution","type":"oem"},{"id":"com.evenwell.defaultappconfigure.overlay.base.s600ww","description":"A theme overlay for selecting default apps or something?","removal":"caution","type":"oem"},{"id":"com.evenwell.email.data.overlay.base.s600ww","description":"Theme overlay for email app?","removal":"caution","type":"oem"},{"id":"com.evenwell.factorywizard","description":"Likely part of the first-boot device setup (new/factory reset device).","removal":"delete","type":"oem"},{"id":"com.evenwell.factorywizard.overlay.base","description":"Theme overlay for setup wizard?","removal":"caution","type":"oem"},{"id":"com.evenwell.factorywizard.overlay.base.s600ww","description":"Theme overlay for setup wizard?","removal":"caution","type":"oem"},{"id":"com.evenwell.fmradio.overlay.base.s600ww","description":"Theme overlay for Nokia radio app?","removal":"caution","type":"oem"},{"id":"com.evenwell.foxlauncher.partner","description":"Partner Launcher Customization\nRelated to the Nokia launcher\n","removal":"delete","type":"oem"},{"id":"com.evenwell.fqc","description":"FQC is a secret test menu. It lets you test the hardware (touch screen, speakers, SD card, SIM card, camera...)\nYou need to type *#*#372733#*#* in the Nokia dialer\n","removal":"delete","type":"oem"},{"id":"com.evenwell.hdrservice","description":"HDR Service (https://play.google.com/store/apps/details?id=com.evenwell.hdrservice)\nEnhances contrast and sharpness for normal photos, games and videos dynamically.\n","removal":"replace","type":"oem"},{"id":"com.evenwell.legalterm","description":"Provides terms and conditions (legal notice)","removal":"delete","type":"oem"},{"id":"com.evenwell.legalterm.overlay.base.s600ww","description":"Theme overlay for some terms and conditions?","removal":"caution","type":"oem"},{"id":"com.evenwell.managedprovisioning","description":"Nokia implementation of com.android.managedprovisioning? If so it manages Android user accounts, allowing you to add extra accounts. The typical use-case is setting up a corporate profile that is controlled by the employer on an employee's personal device, to keep personal and work data separate.","removal":"caution","type":"oem"},{"id":"com.evenwell.managedprovisioning.overlay.base","description":"Theme overlay for Managed Provisioning?","removal":"caution","type":"oem"},{"id":"com.evenwell.managedprovisioning.overlay.base.s600id","description":"Theme overlay for Managed Provisioning?","removal":"caution","type":"oem"},{"id":"com.evenwell.managedprovisioning.overlay.base.s600ww","description":"Theme overlay for Managed Provisioning?","removal":"caution","type":"oem"},{"id":"com.evenwell.mappartner","description":"????\n","removal":"delete","type":"oem"},{"id":"com.evenwell.nps","description":"Net Promoter Score\nPreinstalled survey.","removal":"delete","type":"oem"},{"id":"com.evenwell.nps.overlay.base","description":"Theme overlay for Net Promoter Score?","removal":"caution","type":"oem"},{"id":"com.evenwell.nps.overlay.base.s600id","description":"Theme overlay for Net Promoter Score?","removal":"caution","type":"oem"},{"id":"com.evenwell.nps.overlay.base.s600ww","description":"Theme overlay for Net Promoter Score?","removal":"caution","type":"oem"},{"id":"com.evenwell.pandorasbox","description":"WTF is this? [MORE INFO NEEDED]","removal":"delete","type":"oem"},{"id":"com.evenwell.pandorasbox.app","description":"WTF is this?\n","removal":"delete","type":"oem"},{"id":"com.evenwell.partnerbrowsercustomizations","description":"Adds something (Nokia-)partner-related to your browser? Probably adds bookmarks.","removal":"delete","type":"oem"},{"id":"com.evenwell.partnerbrowsercustomizations.overlay.base","description":"Theme overlay for some browser customization?","removal":"caution","type":"oem"},{"id":"com.evenwell.partnerbrowsercustomizations.overlay.base.s600id","description":"Theme overlay for some browser customization?","removal":"caution","type":"oem"},{"id":"com.evenwell.partnerbrowsercustomizations.overlay.base.s600ww","description":"Theme overlay for some browser customization?","removal":"caution","type":"oem"},{"id":"com.evenwell.permissiondetection","description":"???? [MORE INFO NEEDED]","removal":"caution","type":"oem"},{"id":"com.evenwell.permissiondetection.overlay.base.s600ww","description":"A theme overlay for some \"permissiondetection\" package?","removal":"caution","type":"oem"},{"id":"com.evenwell.phone.overlay.base","description":"Some overlay for the dialer app? Overlays are usually themes.","removal":"caution","type":"oem"},{"id":"com.evenwell.phone.overlay.base.s600ww","description":"Theme overlay for the dialer app?","removal":"caution","type":"oem"},{"id":"com.evenwell.powersaving.g3.overlay.d.base.s600e0","description":"Theme overlay for Power Saving?","removal":"caution","type":"oem"},{"id":"com.evenwell.providers.downloads.overlay.base.s600ww","description":"Theme overlay for the downloads app?","removal":"caution","type":"oem"},{"id":"com.evenwell.providers.downloads.ui.overlay.base.s600ww","description":"Theme overlay for the downloads app?","removal":"caution","type":"oem"},{"id":"com.evenwell.providers.partnerbookmarks.overlay.base.s600ww","description":"Theme overlay for Partner Bookmarks?","removal":"caution","type":"oem"},{"id":"com.evenwell.providers.weather","description":"Provider for the Nokia weather app.\nContent providers encapsulate data, providing centralized management of data shared between apps.\nhttps://developer.android.com/guide/topics/providers/content-providers.html","removal":"delete","type":"oem"},{"id":"com.evenwell.providers.weather.overlay.base.s600ww","description":"Theme overlay for weather provider?","removal":"caution","type":"oem"},{"id":"com.evenwell.pushagent","description":"Related to push notifications for Nokia apps?","removal":"delete","type":"oem"},{"id":"com.evenwell.pushagent.overlay.base","description":"Theme overlay for pushagent?","removal":"caution","type":"oem"},{"id":"com.evenwell.pushagent.overlay.base.s600id","description":"Theme overlay for pushagent?","removal":"caution","type":"oem"},{"id":"com.evenwell.pushagent.overlay.base.s600ww","description":"Theme overlay for pushagent?","removal":"caution","type":"oem"},{"id":"com.evenwell.retaildemoapp","description":"Nokia retail demonstration mode\nhttps://en.wikipedia.org/wiki/Demo_mode","removal":"delete","type":"oem"},{"id":"com.evenwell.retaildemoapp.overlay.base","description":"Theme overlay for Nokia retail demonstration mode?\nhttps://en.wikipedia.org/wiki/Demo_mode","removal":"caution","type":"oem"},{"id":"com.evenwell.retaildemoapp.overlay.base.s600id","description":"Theme overlay for Nokia retail demonstration mode?\nhttps://en.wikipedia.org/wiki/Demo_mode","removal":"caution","type":"oem"},{"id":"com.evenwell.retaildemoapp.overlay.base.s600ww","description":"Theme overlay for Nokia retail demonstration mode?\nhttps://en.wikipedia.org/wiki/Demo_mode","removal":"caution","type":"oem"},{"id":"com.evenwell.screenlock.overlay.base.s600ww","description":"Theme overlay for the lock-screen?","removal":"caution","type":"oem"},{"id":"com.evenwell.settings.data.overlay.base","description":"Overlay related to settings. Overlays are usually themes.","removal":"caution","type":"oem"},{"id":"com.evenwell.settings.data.overlay.base.s600ww","description":"Theme overlay for settings?","removal":"caution","type":"oem"},{"id":"com.evenwell.setupwizard.btl.s600ww.overlay","description":"Theme overlay for Setup Wizard?","removal":"caution","type":"oem"},{"id":"com.evenwell.stbmonitor","description":"Apparently used to stabilize phone usage.\nSeems to drain battery.","removal":"delete","type":"oem"},{"id":"com.evenwell.stbmonitor.data.overlay.base","description":"Theme overlay for STB Monitor?","removal":"caution","type":"oem"},{"id":"com.evenwell.stbmonitor.data.overlay.base.s600id","description":"Theme overlay for STB Monitor?","removal":"caution","type":"oem"},{"id":"com.evenwell.stbmonitor.data.overlay.base.s600ww","description":"Theme overlay for STB Monitor?","removal":"caution","type":"oem"},{"id":"com.evenwell.telecom.data.overlay.base","description":"Overlay related to Telecom data? Overlays are usually themes.","removal":"caution","type":"oem"},{"id":"com.evenwell.telecom.data.overlay.base.s600id","description":"Theme overlay for something telecom-related?","removal":"caution","type":"oem"},{"id":"com.evenwell.telecom.data.overlay.base.s600ww","description":"Theme overlay for something telecom-related?","removal":"caution","type":"oem"},{"id":"com.evenwell.weather.overlay.base.s600ww","description":"Theme overlay for the Nokia weather app?","removal":"caution","type":"oem"},{"id":"com.evenwell.weatherservice","description":"Service for the weather app","removal":"delete","type":"oem"},{"id":"com.evenwell.weatherservice.overlay.base.s600ww","description":"Theme overlay for weather service?","removal":"caution","type":"oem"},{"id":"com.evenwell.whitebalance","description":"","removal":"caution","type":"oem"},{"id":"com.evenwell.whitebalance.overlay.base","description":"","removal":"caution","type":"oem"},{"id":"com.example.alpha.chipsemitptest","description":"ChipsemiTpTest\nRawdata Value Test.","removal":"delete","type":"oem"},{"id":"com.example.calibrationmaster","description":"Chinese hidden camera calibration.","removal":"delete","type":"oem"},{"id":"com.example.rftuner","description":"Secret Code: 439. FTM/Custom Test, SAR test mode, OTA Test, ANT Switch/Select.","removal":"delete","type":"oem"},{"id":"com.example.wifirftest","description":"Wifi Radio Frequency test\nProbably used in factory. No hidden test menu to use it.\n","removal":"delete","type":"oem"},{"id":"com.facemoji.lite.transsion","description":"Emoji Keyboard\nHave ads and it's not good for privacy.\nWARNING: On Infinix phones, this package is a hard dependency to show keyboard after initial reboot/startup. Without it, even if you have another keyboard installed, no keyboard will show.\nDon't remove if you're using an Infinix phone and need to enter password after boot.","removal":"replace","type":"oem"},{"id":"com.facemoji.lite.xiaomi","description":"Xiaomi keyboard\nHave ads and analytics.\nBetter alternative: https://f-droid.org/en/packages/dev.patrickgold.florisboard/","removal":"delete","type":"oem"},{"id":"com.facemoji.lite.xiaomi.gp","description":"Facemoji Keyboard Lite for Xiaomi - Emoji & Theme (https://play.google.com/store/apps/details?id=com.facemoji.lite.xiaomi.gp)\nEmoji keyboard\n","removal":"delete","type":"oem"},{"id":"com.factory.mmigroup","description":"Hidden super-menu accessible by dialing *#*#64633#*#*\nThis menu lists all the others hidden test/debug apps.\n","removal":"delete","type":"oem"},{"id":"com.fairphone.activator","description":"Fairphone activation service\nhttps://forum.fairphone.com/t/telemetry-spyware-list-of-privacy-threats-on-fp3-android-9/55179/74","removal":"delete","type":"oem"},{"id":"com.fairphone.myfairphone","description":"My Fairphone app\nhttps://www.fairphone.com/en/2021/12/20/my-fairphone-app/","removal":"replace","type":"oem"},{"id":"com.fido.fido2client","description":"FIDO UAF1.0 ASM\nRelated to app fingerprint unlocking and payments. Safe to remove if you don't use passwordless authentication to access online services.\n'com.fido.asm' is the same app.","removal":"replace","type":"oem"},{"id":"com.fido.xiaomi.uafclient","label":"FIDO UAF1.0 Client","description":"Fido is a set of open technical specifications for mechanisms of authenticating users to online services that do not depend on passwords.\nThe UAF protocol is designed to enable online services to offer passwordless and multi-factor security by allowing users to register their device to the online service and using a local authentication mechanism such as iris or fingerprint recognition.\nSafe to remove if you don't use password-less authentication to access online services.","web":["https://fidoalliance.org/specs/u2f-specs-1.0-bt-nfc-id-amendment/fido-glossary.html","https://fidoalliance.org/specs/fido-v2.0-rd-20170927/fido-overview-v2.0-rd-20170927.html","https://developers.google.com/identity/fido/android/native-apps"],"removal":"caution","type":"oem"},{"id":"com.fih.StatsdLogger","description":"Foxconn stats logger\n","removal":"delete","type":"oem"},{"id":"com.fih.infodisplay","description":"Foxconn info display\n????\n","removal":"delete","type":"oem"},{"id":"com.fingerprints.extension.service","description":"FingerprintExtensionService\nNeeded for fingerprint sensor test.","removal":"delete","type":"oem"},{"id":"com.fingerprints.fingerprintsensortest","description":"Sensor Test Tool\nProvides hidden fingerprint test menu. Type *#806# in OnePlus dialer to open.","removal":"delete","type":"oem"},{"id":"com.fingerprints.fpctest","description":"Fingerprint test","removal":"delete","type":"oem"},{"id":"com.fingerprints.imagecollection","description":"it's hidden image collection of fingerprint and sensor test.","removal":"delete","type":"oem"},{"id":"com.fingerprints.optical","description":"Optical Test Tool\nTesting things.","removal":"delete","type":"oem"},{"id":"com.fingerprints.sensortesttool","description":"Sensor Test Tool\nHidden test app used to test working of the fingerprint sensors.\n","removal":"delete","type":"oem"},{"id":"com.fingerprints.serviceext","label":"com.fingerprints.serviceext","description":"Fingerprint test, fingerprint authentication.","removal":"delete","type":"oem"},{"id":"com.finshell.wallet","description":"Finshell wallet\nWallet app by Finshell.","removal":"delete","type":"oem"},{"id":"com.flyme.aod","description":"AlwaysOnDisplay","removal":"delete","type":"oem"},{"id":"com.flyme.netadmin","description":"Speedtest and security things.","removal":"delete","type":"oem"},{"id":"com.flyme.systemuiex","description":"System UI Ext\nLooks like an important app, has webview activity only.","removal":"caution","type":"oem"},{"id":"com.flyme.systemuitools","description":"System UI Tools\nLooks like an important app. It has gameassist, drivemode, windowmode things.","removal":"caution","type":"oem"},{"id":"com.flyme.telecom.usagedata.service","description":"Phone services\nIt's just Usage data and it will be sent to cloud probably.","removal":"caution","type":"oem"},{"id":"com.focaltech.fingerprint","description":"Another fingerprint sensor test tool but Chinese.","removal":"delete","type":"oem"},{"id":"com.foxconn.ifaa","description":"IFAA = (China’s) Internet Finance Authentication Alliance\nProvides biometric authentication for Alipay. Probably safe to disable if you don't use it.","removal":"delete","type":"oem"},{"id":"com.fp.camera","description":"Fairphone Camera app","removal":"replace","type":"oem"},{"id":"com.fpsensor.fpSensorExtensionSvc2","description":"Fingerprint sensor test tool\nHidden testing Fingerprint not available for normal users.","removal":"delete","type":"oem"},{"id":"com.freemeimp.factory","description":"aging test\nHidden aging test.","removal":"delete","type":"oem"},{"id":"com.funbase.xradio","description":"WOW FM\nApp for FMRadio","removal":"replace","type":"oem"},{"id":"com.funtouch.uiengine","description":"FuntouchUIEngine\nNeeded for themes probably","removal":"caution","type":"oem"},{"id":"com.futuredial.asusdatatransfer","description":"ASUS Phone Clone\nit's app to move your data to new or old phone.","removal":"delete","type":"oem"},{"id":"com.futuredial.asuslocalbackup","description":"ASUS Device backup\nContains Google Analytics and numerous permissions.","removal":"delete","type":"oem"},{"id":"com.gallery20","label":"AI Gallery","description":"Stock gallery app with picture editing (filters, crop, add text, watermark, frame, blur). Sends analytics to api.meishesdk.com","web":["https://play.google.com/store/apps/details?id=com.gallery20","https://beta.pithus.org/report/d9cf633450ed90d2c89c941c5c202845b2789ceffe6d6337ecf772d223d157de"],"removal":"replace","suggestions":"gallery","type":"oem"},{"id":"com.gameloft.android.ANMP.GloftA9HM","description":"Asphalt 9: Legends","removal":"delete","type":"oem"},{"id":"com.goodix","description":"Hidden tests sensors, fingerprint.","removal":"delete","type":"oem"},{"id":"com.goodix.deltadiff","description":"it's app for testing things.","removal":"delete","type":"oem"},{"id":"com.goodix.fingerprint","description":"GFManager\nFingerprint test? This app don't have any code.","removal":"delete","type":"oem"},{"id":"com.goodix.fingerprint.producttest","description":"Fingerprint test\nHidden Fingerprint testing not available for normal users.","removal":"delete","type":"oem"},{"id":"com.goodix.fingerprint.sampling","description":"Fingerprint test\nHidden Fingerprint testing not available for normal users.","removal":"delete","type":"oem"},{"id":"com.goodix.fingerprint.setting","description":"In-Display Fingerprint test\nHidden testing Fingerprint not available for normal users.","removal":"delete","type":"oem"},{"id":"com.goodix.gftest","description":"Fingerprint test\nHidden app that tests your fingerprint. Not available for users.","removal":"delete","type":"oem"},{"id":"com.goodix.rawdata","description":"DrawLineTest, Raw Data Test.","removal":"delete","type":"oem"},{"id":"com.google.RilConfigService","description":"May break calls after remove.","removal":"caution","type":"oem"},{"id":"com.google.SSRestartDetector","description":"SubSystem Restart Service, Collects ram dump, crash count.\nStill unknown.","removal":"replace","type":"oem"},{"id":"com.google.ambient.streaming","description":"Access and use your Android phone's apps from your Chromebook","removal":"delete","type":"oem"},{"id":"com.google.android.accessibility.switchaccess","description":"Switch Access\nhttps://play.google.com/store/apps/details?id=com.google.android.accessibility.switchaccess&hl=en_US","removal":"delete","type":"oem"},{"id":"com.google.android.aicore","description":"AI Core\nLooks like AI things but code means nothing about AI or Performance.\nhttps://developer.android.com/ml/aicore Android apps can access this package for Google Gemini.","removal":"replace","type":"oem"},{"id":"com.google.android.apps.accessibility.voiceaccess","description":"Voice Access\nHelps anyone who has difficulty manipulating a touch screen (e.g. due to paralysis, tremor, or temporary injury) use their Android device by voice.\nhttps://play.google.com/store/apps/details?id=com.google.android.apps.accessibility.voiceaccess","removal":"delete","type":"oem"},{"id":"com.google.android.apps.betterbug","description":"Android Beta Feedback\nAvailable on Beta Android Pixel Phones.","removal":"delete","type":"oem"},{"id":"com.google.android.apps.camera.services","description":"Pixel Camera Services\nExperimental stuff, Camera Calibration.","removal":"replace","type":"oem"},{"id":"com.google.android.apps.carrier.carrierwifi","label":"Wi-Fi Provisioner","description":"On the code I found only:\nOpenRoaming is a network of free and secure Wi-Fi hotspots. Require google account and google play services.\nIt can be also for wifi calling.","removal":"caution","type":"oem"},{"id":"com.google.android.apps.carrier.log","description":"Carrier App Logging\nRequire google play services\nLogs everything.","removal":"delete","type":"oem"},{"id":"com.google.android.apps.cbrsnetworkmonitor","description":"it's used for spying on you. (citizen broadband network monitor)\nRequire google play services and have location permission.","removal":"delete","type":"oem"},{"id":"com.google.android.apps.customization.pixel","description":"Basic 4 colors","removal":"caution","type":"oem"},{"id":"com.google.android.apps.diagnostictool","description":"DiagnosticTool\nHidden testing components.","removal":"delete","type":"oem"},{"id":"com.google.android.apps.dreamliner","description":"Pixel Stand, Google Pixel AI wallpaper.","removal":"replace","type":"oem"},{"id":"com.google.android.apps.healthdata","description":"Useless frameworks and it's not needed for Health Connect.","removal":"delete","type":"oem"},{"id":"com.google.android.apps.helprtc","description":"Google Support Services\nAllows you to share your Android device screen with a Google customer support agent for a personalized support experience.\nhttps://play.google.com/store/apps/details?id=com.google.android.apps.helprtc","removal":"delete","type":"oem"},{"id":"com.google.android.apps.internal.betterbug","description":"betterbug\nbetterbug? All of these code means nothing.\nalso have a lot of permissions and nexuslogger permission.","removal":"delete","type":"oem"},{"id":"com.google.android.apps.mediashell","description":"Chromecast built-in\nNeeded to support broadcast?\nhttps://play.google.com/store/apps/details?id=com.google.android.apps.mediashell&hl=en&gl=US","removal":"caution","type":"oem"},{"id":"com.google.android.apps.pixel.support","description":"Pixel Troubleshooting\nThere's Battery diagnostics also google, tiktok? account load.","removal":"delete","type":"oem"},{"id":"com.google.android.apps.pixelmigrate","description":"Data Transfer Tool\nIt's probably restore apps on setup wizard first-boot setup.","removal":"delete","type":"oem"},{"id":"com.google.android.apps.retaildemo.preload","description":"Retail Demo Services\nShould not be used for normal users.","removal":"delete","type":"oem"},{"id":"com.google.android.apps.tips","description":"Pixel Tips\na lot bloated.","removal":"delete","type":"oem"},{"id":"com.google.android.apps.tv.dreamx","description":"Ambient Mode\nRunning wallpapers from this app like a slideshow.\nhttps://play.google.com/store/apps/details?id=com.google.android.apps.tv.dreamx&hl=en&gl=US","removal":"delete","type":"oem"},{"id":"com.google.android.apps.tv.launcherx","description":"Google TV\nLauncher to TV, there's no replace probably.\nhttps://play.google.com/store/apps/details?id=com.google.android.apps.tv.launcherx&hl=en&gl=US","removal":"caution","type":"oem"},{"id":"com.google.android.apps.tv.netoscope","description":"Connectivity Diagnostics\nDiagnoses not only connectivity but also time, not needed.","removal":"delete","type":"oem"},{"id":"com.google.android.apps.wearable.retailattractloop","description":"Demo mode - you see it in the stores (the video playing while idle).","removal":"delete","type":"oem"},{"id":"com.google.android.apps.wearable.settings","description":"WearOS settings","removal":"caution","type":"oem"},{"id":"com.google.android.apps.wearables.maestro.companion","description":"Google Pixel Buds\nSet up and manage your Pixel Buds right from your Android 6.0+ device with the Google Pixel Buds app.\nhttps://play.google.com/store/apps/details?id=com.google.android.apps.wearables.maestro.companion","removal":"delete","type":"oem"},{"id":"com.google.android.apps.work.clouddpc","description":"Device Policy\nwork apps handling","removal":"delete","type":"oem"},{"id":"com.google.android.apps.youtube.music.setupwizard","description":"YT Music Setup Wizard\nYT Music Setup Wizard?! Everything is possible.\nI found only that they want to give you a free trial or premium.","removal":"delete","type":"oem"},{"id":"com.google.android.backdrop","description":"Needed for screensaver.\nhttps://play.google.com/store/apps/details?id=com.google.android.backdrop&hl=en&gl=US","removal":"delete","type":"oem"},{"id":"com.google.android.carrier","description":"Carrier Settings\nGoogle Api Activity, requires GMS, totally random api's.\nRequired for 4G, but does not cause bootloop after removal.","removal":"unsafe","type":"oem"},{"id":"com.google.android.carrierlocation","description":"Sharing location to carrier.","removal":"caution","type":"oem"},{"id":"com.google.android.carriersetup","description":"it's only needed on first-boot setup","removal":"delete","type":"oem"},{"id":"com.google.android.cellbroadcastreceiver.overlay","description":"CellBroadcastReceiver overlay.\nOverlays work by mapping resources defined in the overlay package to resources defined in the target package. When an app attempts to resolve the value of a resource in the target package, the value of the overlay resource the target resource is mapped to is returned instead.\nThe CellBroadcastReceiver app is a default system app that handles emergency and nonemergency alerts (such as amber and presidential alerts) and presents the information to end users based on carrier and regional regulations.\nhttps://source.android.com/docs/core/runtime/rros\nhttps://source.android.com/docs/core/ota/modular-system/cellbroadcast","removal":"caution","type":"oem"},{"id":"com.google.android.cellbroadcastreceiver.overlay.miui","description":"Disable opt out dialog to cellbroadcastreceiver? I never seen that. Unused.","removal":"delete","type":"oem"},{"id":"com.google.android.cellbroadcastreceiver.rro_common","description":"Useless overlay code for cellbroadcastreceiver","removal":"delete","type":"oem"},{"id":"com.google.android.cellbroadcastservice.overlay","description":"CellBroadcastService overlay.\nOverlays work by mapping resources defined in the overlay package to resources defined in the target package. When an app attempts to resolve the value of a resource in the target package, the value of the overlay resource the target resource is mapped to is returned instead.\nThe CellBroadcastService service supports CellBroadcast SMS decoding, geofencing for wireless emergency alert (WEA) 3.0, message duplication checks, and broadcasting messages to apps. It's a one-to-many geotargeted and geofenced messaging service designed to deliver messages to multiple mobile phone users, in a defined area, at the same time. The service is defined by the [ETSI](https://www.etsi.org/about) GSM committee, [3GPP](https://www.3gpp.org/about-3gpp), and is a part of the telecommunication standards.\nhttps://source.android.com/docs/core/runtime/rros\nhttps://source.android.com/docs/core/ota/modular-system/cellbroadcast","removal":"caution","type":"oem"},{"id":"com.google.android.cellbroadcastservice.overlay.miui","description":"cross sim duplicate detection disable? I never seen this. Unused.","removal":"delete","type":"oem"},{"id":"com.google.android.cellbroadcastservice.rro_common","description":"Useless overlay code for cellbroadcastservice","removal":"delete","type":"oem"},{"id":"com.google.android.chromecast.chromecastservice","description":"Needed for broadcast support?","removal":"caution","type":"oem"},{"id":"com.google.android.chromecast.setupcustomization","description":"Needed for Chromecast setup.","removal":"replace","type":"oem"},{"id":"com.google.android.clockwork.oemsetup","description":"Installs carrier apps after the first time setup. Haven't noticed any consequences after uninstalling. I also saw some similar bloatware packages on the net, ending with clockwork.gestures.tutorial - first time use tutorial or clockwork.flashlight, clockwork.nfc, clockwork.brightness","removal":"delete","type":"oem"},{"id":"com.google.android.connectivity.resources.overlay","description":"Useless default configs (?)","removal":"caution","type":"oem"},{"id":"com.google.android.connectivity.resources.overlay.oplus","description":"Needed for network, may cause bootloop.","removal":"unsafe","type":"oem"},{"id":"com.google.android.connectivitythermalpowermanager","description":"powersaving?","removal":"caution","type":"oem"},{"id":"com.google.android.dialer.rro_common","description":"Useless overlay code for Google Dialer","removal":"delete","type":"oem"},{"id":"com.google.android.documentsui.icon_overlay","description":"Another useless icon overlay.","removal":"delete","type":"oem"},{"id":"com.google.android.documentsui.theme.pixel","description":"Useless code to documentsui.","removal":"delete","type":"oem"},{"id":"com.google.android.dreamlinerupdater","description":"Dock Updater\nUpdates to Dreamliner(Google Pixel AI wallpaper).","removal":"replace","type":"oem"},{"id":"com.google.android.euicc","label":"SIM Manager","description":"eUICC (embedded UICC) refers to the architectural standards for eSIM, a device used to securely store one or more SIM card profiles, which are the unique identifiers and cryptographic keys used by cellular network service providers to uniquely identify and securely connect to mobile network devices.","removal":"unsafe","type":"oem"},{"id":"com.google.android.euiccoverlay","description":"Partner customization on first boot Setup, overlay.","removal":"delete","type":"oem"},{"id":"com.google.android.factoryota","description":"Factory OTA Mode\nit's for testing OTA.","removal":"delete","type":"oem"},{"id":"com.google.android.flipendo","description":"Extreme Battery Saver\nIt can be found in settings, lets you to choose which app are important to you to use when this feature is enabled.","removal":"caution","type":"oem"},{"id":"com.google.android.flipendo.auto_generated_rro_product__","description":"Data connection 5G? it's for extreme battery saver.","removal":"unsafe","type":"oem"},{"id":"com.google.android.flipendo.auto_generated_rro_vendor__","description":"Data connection 5G? it's for extreme battery saver.","removal":"unsafe","type":"oem"},{"id":"com.google.android.gmsintegration","description":"it's Google Sample Home Screen. Useless.","removal":"delete","type":"oem"},{"id":"com.google.android.grilservice","description":"Has too much random logging, metrics stuff.","removal":"delete","type":"oem"},{"id":"com.google.android.hardwareinfo","description":"Has a lot statistics and takes info about device.\nRequires Google Play Services, also assumes call notifications?","removal":"replace","type":"oem"},{"id":"com.google.android.katniss","description":"Google app, no replacement?\nhttps://play.google.com/store/apps/details?id=com.google.android.katniss&hl=en&gl=US","removal":"replace","type":"oem"},{"id":"com.google.android.networkstack.tethering.overlay2021","description":"Useless hotspot configs (?)","removal":"caution","type":"oem"},{"id":"com.google.android.odad","description":"Google Play Protect Service\nGoogle Play Protect Service?\nit have only third party notices activity\nand useless frameworks","removal":"delete","type":"oem"},{"id":"com.google.android.overlay.flipendo","description":"Unused colors to 5G","removal":"caution","type":"oem"},{"id":"com.google.android.overlay.gmsconfig.healthconnect","description":"Useless overlay gmsconfig to Health Connect","removal":"delete","type":"oem"},{"id":"com.google.android.overlay.gmsconfig.nosafetycenter","description":"Has no safety center in the code? Weird. Not needed.","removal":"delete","type":"oem"},{"id":"com.google.android.overlay.gmsconfig.tetheringentitlement","description":"Useless overlay gmsconfig to tetheringentitlement. No effects after remove","removal":"delete","type":"oem"},{"id":"com.google.android.overlay.gmsconfig.tier1","description":"Has random set to default google apps and vivo gallery.","removal":"replace","type":"oem"},{"id":"com.google.android.overlay.gmsgsaconfig","description":"Not needed overlay, only has code to set `googlequicksearchbox`(Google App) as default assistant","removal":"delete","type":"oem"},{"id":"com.google.android.overlay.googleconfig","description":"Once configs all default things to google. Bricks google login functionality.","removal":"unsafe","type":"oem"},{"id":"com.google.android.overlay.googlewebview","description":"Once configs webview default to google. Bricks google login functionality.","removal":"unsafe","type":"oem"},{"id":"com.google.android.overlay.healthconnect","description":"Overlay to 'com.google.android.apps.healthdata'.","removal":"delete","type":"oem"},{"id":"com.google.android.overlay.modules.modulemetadata.forframework","label":"com.google.android.overlay.modules.modulemetadata.forframework","description":"If you remove this package, your phone has an extremely large chance of bootlooping. Just don't mess around with this package. Tested on a Samsung A34 5G.","removal":"unsafe","type":"oem"},{"id":"com.google.android.overlay.modules.packageinstaller","description":"Package installer ui changes to the classic after uninstallation.","removal":"replace","type":"oem"},{"id":"com.google.android.overlay.pixelconfig2018","description":"Useless default configs? You can lose some basic functionality so it's not worth touching it.","removal":"unsafe","type":"oem"},{"id":"com.google.android.overlay.pixelconfig2019","description":"Useless default configs? You can lose some basic functionality so it's not worth touching it.","removal":"unsafe","type":"oem"},{"id":"com.google.android.overlay.pixelconfig2019midyear","description":"Useless default configs? You can lose some basic functionality so it's not worth touching it.","removal":"unsafe","type":"oem"},{"id":"com.google.android.overlay.pixelconfigcommon","description":"Useless default configs? You can lose some basic functionality so it's not worth touching it.","removal":"unsafe","type":"oem"},{"id":"com.google.android.overlay.settingsProvider","description":"Have some stuff about google gms: backup.BackupTransportService.\nUseless backup things, without it backup and cloud work.\nMaybe it's used for backup settings, but probably not, and if it is, I don't think you need it.","removal":"delete","type":"oem"},{"id":"com.google.android.overlay.udfpsoverlay","description":"Configs to fingerprints FRP auto generated.","removal":"delete","type":"oem"},{"id":"com.google.android.permissioncontroller.overlay.nothing","description":"Better keep this for permissioncontroller. Better don't risk","removal":"unsafe","type":"oem"},{"id":"com.google.android.permissioncontroller.overlay.oplus","description":"Permission controller overlay.\nOverlays work by mapping resources defined in the overlay package to resources defined in the target package. When an app attempts to resolve the value of a resource in the target package, the value of the overlay resource the target resource is mapped to is returned instead.\nhttps://source.android.com/docs/core/runtime/rros","removal":"caution","type":"oem"},{"id":"com.google.android.pixel.setupwizard.auto_generated_rro_product__","description":"Useless first-boot setup configs","removal":"delete","type":"oem"},{"id":"com.google.android.pixel.setupwizard.overlay","description":"Useless first-boot setup configs","removal":"delete","type":"oem"},{"id":"com.google.android.pixel.setupwizard.overlay2019","description":"Useless first-boot setup configs","removal":"delete","type":"oem"},{"id":"com.google.android.pixel.setupwizard.overlay2021","description":"Useless first-boot setup configs","removal":"delete","type":"oem"},{"id":"com.google.android.pixelnfc","description":"Application that checks the SKU of a Pixel device to Region Lock the Felica chip (Japanese NFC technology used in E-money cards) of the phone to prevent non-Japanese people from using it.\nRemoving it will make the Osaifu-Keitai crash upon start-up but will not cause any bootloop.\nPatching it (ROOT), however allows people with a Felica Chip to add IC cards to their phone and use it to pay while in Japan.","required_by":["com.felicanetworks.mfm.main"],"removal":"replace","type":"oem"},{"id":"com.google.android.repairmode","description":"Repair mode to Pixel, not sure how it works.","removal":"replace","type":"oem"},{"id":"com.google.android.settings.future.biometrics.faceenroll","description":"Needed to face unlock.","removal":"replace","type":"oem"},{"id":"com.google.android.sss.authbridge","description":"Second Screen Setup Auth Bridge\nThere's google api's used on first boot setup.","removal":"delete","type":"oem"},{"id":"com.google.android.storagemanager.auto_generated_rro_product__","description":"Configs to settings auto generated but better keep.","removal":"unsafe","type":"oem"},{"id":"com.google.android.storagemanager.auto_generated_rro_vendor__","description":"Configs to settings auto generated but better keep.","removal":"unsafe","type":"oem"},{"id":"com.google.android.systemui.gxoverlay","description":"Configs to systemui auto generated but better keep.","removal":"unsafe","type":"oem"},{"id":"com.google.android.tungsten.setupwraith","label":"TV Setup","description":"First boot setup TV.","web":["https://play.google.com/store/apps/details?id=com.google.android.tungsten.setupwraith"],"removal":"caution","warning":"Removing it crashes sound related system settings and breaks volume control on Sony Bravia TV.","type":"oem"},{"id":"com.google.android.tv","description":"Live Channels\nTV tuner and watching stuff, sadly has google analytics.","removal":"caution","type":"oem"},{"id":"com.google.android.tv.axel","description":"Android TV Infrared Service\nNeeded for volume setup.","removal":"caution","type":"oem"},{"id":"com.google.android.tv.bugreportsender","description":"BugReportSender\nBug reports app.","removal":"delete","type":"oem"},{"id":"com.google.android.tv.dfuservice","description":"Remote Control Update Service.","removal":"caution","type":"oem"},{"id":"com.google.android.tv.frameworkpackagestubs","description":"Activity Stub\nWeird app that probably needed for SQLite, webview browser.","removal":"caution","type":"oem"},{"id":"com.google.android.tv.remote.service","description":"Android TV Remote Service, pairing requests from your phone.\nhttps://play.google.com/store/apps/details?id=com.google.android.tv.remote.service&hl=en&gl=US","removal":"delete","type":"oem"},{"id":"com.google.android.tv.remotecontrol.logging","description":"Needed for logging?","removal":"caution","type":"oem"},{"id":"com.google.android.tvlauncher","description":"Android TV Home\nhttps://play.google.com/store/apps/details?id=com.google.android.tvlauncher&hl=en&gl=US","removal":"caution","type":"oem"},{"id":"com.google.android.tvrecommendations","description":"Android TV Core Services\nSometimes has only Notification to Android11UpgradeReceiver,\nand on some TV's has a lot frameworks, it's weird.\nhttps://play.google.com/store/apps/details?id=com.google.android.tvrecommendations&hl=en&gl=US","removal":"caution","type":"oem"},{"id":"com.google.android.uvexposurereporter","description":"UvExposureReporter, logs, VendorAtom.\nUnknown.","removal":"replace","type":"oem"},{"id":"com.google.android.wearable.ambient","description":"It's like doze on Android phones. Not recommended to disable, as this package reduces battery drain when idle.","removal":"caution","type":"oem"},{"id":"com.google.android.wearable.assistant","description":"Google Assistant for Android wearables (https://play.google.com/store/apps/details?id=com.google.android.wearable.assistant)\n\nHas obviously all the dangerous permissions: https://beta.pithus.org/report/efccf27aa68d9c263e4288d38af76f855b5fd4156034ebdaabeb185d8c4f1411","removal":"replace","type":"oem"},{"id":"com.google.android.wearable.batteryservices","description":"It's used to manage battery-related things on Android smartwatches, like monitoring the battery level, managing power consumption (auto battery saving I think), and handling battery-related events (pop-up when battery at 15%, etc.). It is typically used by developers to create battery-aware applications for wearable devices.","removal":"caution","type":"oem"},{"id":"com.google.android.wearable.healthservices","description":"Health Services by Google\n (https://play.google.com/store/apps/details?id=com.google.android.wearable.healthservices)\n\nDisabling this on a Watch5 broke heart rate measuring and some workouts.","removal":"caution","type":"oem"},{"id":"com.google.android.wfcactivation","description":"Carrier Setup\nNeeded for IMS, WiFi calling and have emergency things.","removal":"replace","type":"oem"},{"id":"com.google.android.wifi.resources.pixel","description":"WiFi configs","removal":"unsafe","type":"oem"},{"id":"com.google.android.youtube.tv","description":"YouTube app\nhttps://play.google.com/store/apps/details?id=com.google.android.youtube.tv&hl=en_US","removal":"delete","type":"oem"},{"id":"com.google.android.youtube.tvmusic","description":"YouTube Music\nhttps://play.google.com/store/apps/details?id=com.google.android.youtube.tvmusic&hl=en&gl=US","removal":"delete","type":"oem"},{"id":"com.google.euiccpixel","description":"NFC, eSE, eSIM firmware updater","removal":"replace","type":"oem"},{"id":"com.google.euiccpixel.overlay.gs101","description":"Overlay to (com.google.euiccpixel) NFC, eSE, eSIM firmware updater","removal":"replace","type":"oem"},{"id":"com.google.pixel.camera.services","label":"com.google.pixel.camera.services","description":"CameraIDRemapper and a lot Debug stuff. A dependency for Private space on Pixel devices.","web":["https://source.android.com/docs/security/features/private-space"],"removal":"caution","type":"oem"},{"id":"com.google.pixel.digitalkey.timesync","description":"CccDkTimeSyncService, IBluetoothCcc.\nUnknown app to car connectivity.","removal":"delete","type":"oem"},{"id":"com.google.pixel.livewallpaper","description":"Pixel live wallpaper\nHave a lot of wallpapers.","removal":"replace","type":"oem"},{"id":"com.hancom.office.viewer","description":"(discontinued) old Hancom Office Viewer","removal":"delete","type":"oem"},{"id":"com.hawk.android.browser","label":"Turbo Browser","description":"Likely a WebView-based web browser. Last updated 2019. Not maintained anymore.","web":["https://www.apkmirror.com/apk/mie-alcatel-support/turbo-browser-private-adblocker-fast-download/"],"removal":"delete","suggestions":"browsers","type":"oem"},{"id":"com.heytap.appplatform","description":"Needed for OTA Updates also causes bootloop on some phones after uninstall.","removal":"unsafe","type":"oem"},{"id":"com.heytap.browser","label":"Internet Browser","description":"Rebranded HeyTap browser for Oppo. Full of ads and spams you in notifications. You should never use this browser.","web":["https://brand.heytap.com/eu/privacy.html"],"removal":"replace","suggestions":"browsers","type":"oem"},{"id":"com.heytap.cast","description":"Screencast\nPhone casting (casting phone screen to other devices).","removal":"caution","type":"oem"},{"id":"com.heytap.cloud","description":"HeyTap Cloud\nBad privacy policy: https://muc.heytap.com/document/heytap/oversea/privacyPolicy/privacyPolicy_en-US.html\nWhy does the app need `REQUEST_INSTALL_PACKAGES` (can install packages)?\nPithus analysis: https://beta.pithus.org/report/dbf265db47f8632453bb83ef51ea1d921413c02a8d24c989345896de83704a75","removal":"delete","dependencies":["com.heytap.mcs"],"type":"oem"},{"id":"com.heytap.colorfulengine","description":"Useless frameworks for theming. Maybe needed for premium","removal":"delete","type":"oem"},{"id":"com.heytap.datamigration","description":"Some tools for stock browser (not Chrome), but it's Chinese and probably unused","removal":"delete","type":"oem"},{"id":"com.heytap.habit.analysis","description":"Most likely used to track your habits from IoT HeyTap devices [TO BE CONFIRMED]","removal":"delete","type":"oem"},{"id":"com.heytap.headset","description":"HeyTap Melody App used to manage Oppo Bluetooth Earphones.","removal":"replace","type":"oem"},{"id":"com.heytap.htms","description":"Mobile Services\nMobile services (removal may not be able to log in to OPPO account).","removal":"caution","type":"oem"},{"id":"com.heytap.linker","description":"DistUI\nNeeded for broadcast","removal":"caution","type":"oem"},{"id":"com.heytap.market","description":"Heytap/Oppo app store.There is no benefit of using this app store and you should not keep a privileged app with as many permissions.\n\nhttps://developers.oppomobile.com/newservice/capability?pagename=app_store\nPithus analysis: https://beta.pithus.org/report/3a2a10af9310411d814fd6dd252adec1ab0c06adf32a675b7534c3edc0e534bf","removal":"delete","dependencies":["com.heytap.mcs"],"type":"oem"},{"id":"com.heytap.mcs","label":"System Messages","description":"System messages with trackers.","web":["https://beta.pithus.org/report/8920395af63782fca8dfce18715a10ca5a2d8236d525208ea347eff8f738731e"],"removal":"delete","type":"oem"},{"id":"com.heytap.music","label":"Music","description":"Default Oppo Music App with insecure WebView Implementation (execution of user controlled code in WebView is an important security hole).\nHas also weird permissions (QUERY_ALL_PACKAGES and BLUETOOTH ?).","web":["https://beta.pithus.org/report/befa0ec0616c553632379f069453b0ca74ee29fd1428b9fce19c1657e6f97d8b"],"removal":"replace","suggestions":"music_apps","type":"oem"},{"id":"com.heytap.mydevices","description":"My devices\nMy devices (showing headphones and such)","removal":"caution","type":"oem"},{"id":"com.heytap.openid","description":"OpenID\nDevice logos and advertisements; uninstallation causes anomalies in cloud drive functionality","removal":"delete","type":"oem"},{"id":"com.heytap.pictorial","label":"Lock Screen Magazine","description":"It provides high-quality wallpapers and allows users to customize their lock screens. Every time you wake your screen, the wallpaper automatically changes.","web":["https://play.google.com/store/apps/details?id=com.heytap.pictorial"],"removal":"delete","type":"oem"},{"id":"com.heytap.quicksearchbox","description":"Global Search\nRealme. This will remove the single swipe from top to bottom search that has lots of Chinese in it.","removal":"delete","type":"oem"},{"id":"com.heytap.speechassist","description":"Breeno Voice\nBreeno assistant.","removal":"delete","type":"oem"},{"id":"com.heytap.synergy","description":"HeySynergy\nAnother thing related to broadcast","removal":"caution","type":"oem"},{"id":"com.heytap.themestore","description":"Theme Store with 73 permissions! (including CAMERA, CALL_PHONE, READ_CONTACTS, REQUEST_INSTALL_PACKAGES...) and 2 trackers.\n\nPithus analysis: https://beta.pithus.org/report/e8c4fc2bae420cf5f094ce914f25accdede5152f9d801db6eb32a4020a7726b2","removal":"delete","type":"oem"},{"id":"com.heytap.usercenter","description":"Login service for various HeyTap related services like HeyTap Cloud etc.\nNeeded if you want to join Early Access Testing for new ColorOS/RealmeUI\n\n[APK NEEDED]","removal":"replace","type":"oem"},{"id":"com.heytap.usercenter.overlay","description":"overlay needed to usercenter app has resources","removal":"replace","type":"oem"},{"id":"com.heytap.yoli","description":"Video player with Chinese tracking.","removal":"delete","type":"oem"},{"id":"com.hicloud.android.clone","description":"Huawei Phone Clone (https://play.google.com/store/apps/details?id=com.hicloud.android.clone)\n171 Permissions (https://reports.exodus-privacy.eu.org/fr/reports/144565/)\nData migration application between Huawei phones.\nKeep in mind that all your data will be synchronised in the Huawei cloud and collected by the company.\nhttps://cloud.huawei.com/privacyStatementTransit\n","removal":"delete","type":"oem"},{"id":"com.hilauncherconfig","description":"Configures app icons and folders on first run HiOS Launcher.","removal":"replace","type":"oem"},{"id":"com.hisi.mapcon","description":"Uses Location permissions for VoWifi Special, rtc wifi signal.","removal":"replace","type":"oem"},{"id":"com.hisi.supl","description":"SUPL20Services\nNot needed for location and dont have code.","removal":"delete","type":"oem"},{"id":"com.hiya.axolotl.tcl","description":"","removal":"caution","type":"oem"},{"id":"com.hiya.star","description":"also called android-ss-service-lib (Samsung-exclusive)\nThird-party that provides caller profile information to help consumers identify incoming calls and block unwanted ones.\nhttps://en.wikipedia.org/wiki/Hiya_(company)\nhttps://hiya.com/\nNOTE : Never trust a company which promotes spam blocking features\nhttps://itmunch.com/robocall-caught-sending-customers-confidential-data-without-consent/\n\nHave a look at their privacy policy. That's... pretty scary : https://hiya.com/fr/hiya-data-policy\nNeeded for Samsung Smart Call (com.samsung.android.smartcallprovider)","removal":"delete","required_by":["com.samsung.android.smartcallprovider"],"type":"oem"},{"id":"com.hmdglobal.camera2","description":"Nokia camera (https://play.google.com/store/apps/details?id=com.hmdglobal.camera2)\n","removal":"replace","type":"oem"},{"id":"com.hmdglobal.datago","description":"Sends diagnostic data to HMD (Company behind Nokia)?","removal":"delete","type":"oem"},{"id":"com.hmdglobal.datago.overlay.base","description":"Theme overlay for a Nokia telemetry package?","removal":"caution","type":"oem"},{"id":"com.hmdglobal.datago.overlay.base.s600ww","description":"Theme overlay for a Nokia telemetry package?","removal":"caution","type":"oem"},{"id":"com.hmdglobal.enterprise.api","description":"I can't find this app on the internet, but I heard it has telemetry.","removal":"delete","type":"oem"},{"id":"com.hmdglobal.support","description":"My Phone (https://play.google.com/store/apps/details?id=com.hmdglobal.support)\nLets you join the Nokia phones community, get app recommendations, explore your phone’s user guide and more.\n","removal":"delete","type":"oem"},{"id":"com.hoffnung","description":"TPMS. Remote Config Test.\n⚠️ WARNING: uninstalling might cause bootloops and screen flicker! Disabling this also may remove the ability to see notifications on your lock screen.\nhttps://xdaforums.com/t/infinix-note-10-uninstall-tpms-com-hoffnung-package-causes-bootloop.4647456","removal":"unsafe","type":"oem"},{"id":"com.htc.masthead","description":"HTC Lockscreen Theme.","removal":"replace","type":"oem"},{"id":"com.htc.weather","description":"HTC Weather","removal":"replace","type":"oem"},{"id":"com.huaqin.btlogger","description":"btlogger\ncit bluetooth logging","removal":"delete","type":"oem"},{"id":"com.huaqin.diaglogger","description":"Secret logging menu only accessible by dialing using a \"secret code\" (*#*#CODE#*#*)\nYou can use any of these code : \"995995\", \"996996\", \"9434\", \"334334\", \"5959\", \"477477\"\nUsed to log Bluetooth traffic and send them to com.miui.bugreport\nWrite logs to \"/sdcard/diag_logs/\" | \"/sdcard/wlan_logs/\" | \"/sdcard/MIUI/debug_log/common/\"\n#\nFYI Huaqin is a Chinese mobile phone research and development company.\n","removal":"delete","type":"oem"},{"id":"com.huaqin.factory","description":"Hidden test app (dial *#*#64663#*#*)\nUsed by technician in factory to test the hardware. Not intented to be run by end-users. \nHas a huge amount of permissions.\nA vulnerability was found in 2019 (CVE-2019-15340) allowing any app co-located on the device to \nprogrammatically disable and enable Wi-Fi, Bluetooth, and GPS silently (and without the corresponding access permission)\nhttps://nvd.nist.gov/vuln/detail/CVE-2019-15340\n","removal":"delete","type":"oem"},{"id":"com.huaqin.sar","description":"SetTransmitPower\nI can't access the apk but I'm pretty sure it is another hidden test app not meant to be used by end-user\nGiven its name it could be used to adjust the transmit power of the cell phone antennas\nSAR = Specific Absorption Rate (https://en.wikipedia.org/wiki/Specific_absorption_rate)\nXDA users removed this without any issues. To be 100% sure it would be good to test the SAR without this package (just in case)\n","removal":"delete","type":"oem"},{"id":"com.huawei.HwMultiScreenShot","description":"Scrolling screenshot feature\n","removal":"delete","type":"oem"},{"id":"com.huawei.KoBackup","description":"As of writing this, Huawei phones cannot be rooted. \nThis Backup application is probably able to backup more than any other 3rd party backup app.\n","removal":"replace","type":"oem"},{"id":"com.huawei.airlink","description":"Air link service, it's probably needed for Wireless Projection.\nNo activities, only more debugging stuff, location things, logs","removal":"replace","type":"oem"},{"id":"com.huawei.android.FMRadio","description":"FM-Radio\nHuawei's stock radio player. Remove if FM isn't relevant for you.","removal":"replace","type":"oem"},{"id":"com.huawei.android.FloatTasks","description":"Floating/Navigation dock (also called NaviDot).\nhttps://consumer.huawei.com/en/support/how-to/detail-troubleshooting/en-us00310067/","removal":"delete","type":"oem"},{"id":"com.huawei.android.airsharing","description":"Wireless Projection\nMiracast, Requires Huawei Mobile Services, also it's so bloated, also found unused frameworks HUAWEI GameCenter","removal":"replace","type":"oem"},{"id":"com.huawei.android.chr","description":"HwChrService\nHuawei Call History Record. \n","removal":"delete","type":"oem"},{"id":"com.huawei.android.dsdscardmanager","label":"Dual SIM settings","description":"It's sim card management in Huawei settings.","removal":"unsafe","warning":"Upon removing, sim card management in settings will no longer work.","type":"oem"},{"id":"com.huawei.android.findmyphone","description":"Find Device is an app that lets you locate your device and protect your data remotely.\nTo provide these features, this app needs to connect to the Internet during use.","dependencies":["com.huawei.hwid"],"removal":"replace","type":"oem"},{"id":"com.huawei.android.hsf","description":"Huawei Services Framework\n3 permissions : DELETE_PACKAGES, INSTALL_PACKAGES, PACKAGE_USAGE_STATS\nSafe to remove according to huawei users\n","removal":"delete","type":"oem"},{"id":"com.huawei.android.hwaps","label":"HwAps","description":"Manages Intelligent Resolution and changing screen resolution in settings.","removal":"unsafe","warning":"Upon removing, Intelligent Resolution will be not available, and screen resolution cannot be changed in settings.","type":"oem"},{"id":"com.huawei.android.hwouc","label":"System Update","description":"OTA updates. Safe to remove if you have a very outdated device or flashed recovery.","removal":"caution","type":"oem"},{"id":"com.huawei.android.hwpay","description":"Huawei Pay\nMobile payment and e-wallet service for Huawei devices that offers the same services as Apple Pay, Samsung Pay etc...\nhttps://consumer.huawei.com/en/mobileservices/huawei-wallet/\n","removal":"delete","type":"oem"},{"id":"com.huawei.android.hwupgradeguide","label":"HwUpgradeGuide","description":"It's guide menu at first start AppGallery.","removal":"delete","type":"oem"},{"id":"com.huawei.android.instantonline","label":"HwInstantOnline","description":"No noticeable consequences.","removal":"delete","type":"oem"},{"id":"com.huawei.android.instantshare","description":"Huawei Share features.\nFile transfer tool between Huawei mobiles, using Bluetooth connection and WiFi Direct technology.\n","removal":"delete","type":"oem"},{"id":"com.huawei.android.internal.app","label":"Android HwResolver","description":"Component of Huawei sharing.","removal":"caution","warning":"This may break the sharing function in some apps causing them to crash.","type":"oem"},{"id":"com.huawei.android.karaoke","description":"Karaoke mode feature.\n","removal":"delete","type":"oem"},{"id":"com.huawei.android.launcher","description":"Huawei launcher app.\nIt's basically the home screen, the way icons apps are organized and displayed.","removal":"replace","warning":"Make sure you've another installed before you disable.\nMaybe you'll need this package for the recent apps feature to work (even if you have another launcher installed)","suggestions":"launchers","type":"oem"},{"id":"com.huawei.android.mirrorshare","description":"MirrorShare feature (Miracast rebranded by Huawei)\nUsed to mirror screen of you smartphone on a TV.\n","removal":"delete","type":"oem"},{"id":"com.huawei.android.overlay.modules.modulemetadata","description":"Module that contains metadata about the list of modules on the device and that's about it. I wouldn't advise you to mess with it as it could break important modules (see W1nst0n/universal-android-debloater#37 on GitLab).\nGood explanation of what Android modules are: https://www.xda-developers.com/android-project-mainline-modules-explanation/","removal":"unsafe","type":"oem"},{"id":"com.huawei.android.projectmenu","label":"ProjectMenu","description":"Hidden settings not available for users. ProjectMenu interface: phone *#*#2846579#*#*","web":["https://www.99mediasector.com/open-project-menu-huawei-device-huawei-code/"],"removal":"delete","type":"oem"},{"id":"com.huawei.android.pushagent","description":"push notification agent\nSeems to only be used for Huawei apps\nThe recompiled java code makes it look like it's once again mainly used for analytics.\n","removal":"delete","type":"oem"},{"id":"com.huawei.android.remotecontroller","description":"Huawei Smart Controller app.\nLets you you add, customize, and set up remote controls, allowing control of your electronic appliances through your phone. \n","removal":"delete","type":"oem"},{"id":"com.huawei.android.rftest","description":"HwRFTest\nMT Test Station.","removal":"delete","type":"oem"},{"id":"com.huawei.android.thememanager","label":"Themes","description":"Lets you download and use Huawei themes.","removal":"caution","warning":"After uninstalling the app, setting wallpapers directly will no longer work, but the wallpaper entry in the Settings app will still let you choose wallpapers. Without this package, you may no longer be able to change a notification sound.","type":"oem"},{"id":"com.huawei.android.tips","description":"HUAWEI Feature Advisor\nPeriodically gives you notifications on how to use certain features on your phone.\n","removal":"delete","type":"oem"},{"id":"com.huawei.android.totemweather","description":"Huawei Weather app (and its widget)","removal":"delete","type":"oem"},{"id":"com.huawei.android.totemweatherapp","description":"Huawei Weather app (and its widget)","removal":"delete","type":"oem"},{"id":"com.huawei.android.totemweatherwidget","description":"Huawei Weather app (and its widget)\n","removal":"delete","type":"oem"},{"id":"com.huawei.android.wfdft","description":"Wi-Fi Direct feature.\nNote: Wifi direct enables devices to establish a direct Wi-Fi connection (without a router) over which the two can send and receive files.","removal":"delete","type":"oem"},{"id":"com.huawei.android.wfdirect","description":"Wi-Fi Direct feature.\nNote: Wifi direct enables devices to establish a direct Wi-Fi connection (without a router) over which the two can send and receive files. \n","removal":"replace","type":"oem"},{"id":"com.huawei.androidx","description":"It have something to MediaPlayer but by name app it looks like important.\nSomeone will need to check if it bootloop or not because this app dont have code.","removal":"caution","type":"oem"},{"id":"com.huawei.aod","description":"Always On Display\nWhen enabled in settings it shows clock and notifications when you raise the phone or touch the screen.\nThis is basically a lower-power lock-screen. It could in theory reduce power draw if you check notifications/clock often as OLED screens draw minimal power showing a mostly black screen(black = pixel off), but in practice the number of times you'll unintentionally trigger it will likely eat up any potential power savings and more. And if your device doesn't have an OLED screen this will draw way more power.\nMost of these power savings could be applied to your standard lock-screen simply by making your background image completely black.\nRedSkull23 says it's unsafe to remove. Does it bootloop?","removal":"delete","type":"oem"},{"id":"com.huawei.appmarket","description":"Huawei app store (AppGallery)\nhttps://www.xda-developers.com/appgallery-huawei-alternative-google-play-store-android/\n","removal":"replace","type":"oem"},{"id":"com.huawei.ar.measure","description":"Ar Measure\nMeasure length, depth, area and volume.","removal":"delete","type":"oem"},{"id":"com.huawei.arengine.service","description":"Augmented reality service.\n","removal":"delete","type":"oem"},{"id":"com.huawei.assetsync","description":"HwAssetSync\nNeeded for HiCloud, Cloud syncing.","removal":"delete","type":"oem"},{"id":"com.huawei.assetsyncservice","description":"It's for 'com.huawei.securityserver' for start this service app.\nNeeded for HiCloud sync security.","removal":"delete","type":"oem"},{"id":"com.huawei.audioaccessorymanager","description":"Audio Accessory Manager\nA lot stuff: Huawei health, Huawei music, earphones.\nProbably only useful when you got Huawei FreeBuds.","removal":"delete","type":"oem"},{"id":"com.huawei.autoinstallapkfrommcc","label":"Information","description":"Auto Install apk from mcc\nAuto Install apk from mcc? it's only information and logs.","removal":"delete","type":"oem"},{"id":"com.huawei.bd","description":"HwUE (Huawei UserExperience)\nWhen a company call a something 'UserExperience' you know you don't need this.\nAnalytics service, run at boot. Collect information about packages/apps usages.\nHas a nice custom permission called com.huawei.permission.BIG_DATA\n","removal":"delete","type":"oem"},{"id":"com.huawei.behaviorauth","description":"It seems to be related to verifying the authenticity of user behavior and enhancing password security. User can disable it so it's safe to remove.","web":["https://www.android-hilfe.de/forum/huawei-p30-p30-pro-p30-lite.3510/was-ist-die-behaviorauth-app-fuer-das-p30.1008750.html"],"removal":"delete","type":"oem"},{"id":"com.huawei.betaclub","description":"BetaClub\nIt's app for testing, collection logs and feedback.","removal":"delete","type":"oem"},{"id":"com.huawei.bluetooth","description":"Lets you import your contacts via Bluetooth\nBluetooth will still work if you remove this package.\n","removal":"delete","type":"oem"},{"id":"com.huawei.browser","description":"Huawei Browser app. Don't expect privacy using it\n","removal":"replace","suggestions":"browsers","type":"oem"},{"id":"com.huawei.browserhomepage","description":"Huawei Browser component.\n","removal":"delete","type":"oem"},{"id":"com.huawei.ca","description":"CA\nUses permissions: GET_TASKS, LOCATION_HARDWARE, etc.\nThis app looks like a backdoor or logs.","removal":"delete","type":"oem"},{"id":"com.huawei.calculator","description":"Calculator app","removal":"replace","type":"oem"},{"id":"com.huawei.calendar","description":"Huawei Calendar app.\n","removal":"replace","suggestions":"calendars","type":"oem"},{"id":"com.huawei.camera","label":"HUAWEI Camera","description":"Huawei's stock Camera app","removal":"replace","suggestions":"cameras","type":"oem"},{"id":"com.huawei.camerakit.impl","description":"HwCameraKit\nIn the code it was found that it's for testing camera functions.","removal":"delete","type":"oem"},{"id":"com.huawei.cloud","description":"Chinese only.\nHuawei Cloud Computer provides you with basic services of online desktop.","dependencies":["com.huawei.hwid"],"removal":"delete","type":"oem"},{"id":"com.huawei.coauthservice","description":"HwCoAuthService\nApplock, lockscreen password, face recognition things, fingerprint, not sure if it's worth removing it.\nNeeded for manual apk installation.","removal":"caution","type":"oem"},{"id":"com.huawei.compass","description":"Huawei Compass app.\n","removal":"delete","type":"oem"},{"id":"com.huawei.contacts","description":"Huawei Contacts app\n","removal":"replace","suggestions":"contacts","type":"oem"},{"id":"com.huawei.contacts.sync","description":"Huawei Contacts sync\nMy guess (can't have the apk on hand) is this enables you to synchronise your contacts with your Huawei account.\n","removal":"delete","type":"oem"},{"id":"com.huawei.contactscamcard","description":"CamCard is a business card reader app.\n","removal":"delete","type":"oem"},{"id":"com.huawei.contentsensor","description":"ContentSensor\nLooks like this app has a lot chinese code and collects a lot stuff about user like apps installed, app info, connects to huawei hivoice sites","removal":"delete","type":"oem"},{"id":"com.huawei.controlcenter","description":"Smart Collaboration\nCollaboration between devices. Needed for Multi-Screen, Wireless Projection?\nNeeded for Super Device or Device+, safe to remove if you don't use this, it will also declutter action center.","removal":"replace","type":"oem"},{"id":"com.huawei.cryptosms.service","description":"SMS Encryption Service, looks made for China.","removal":"delete","type":"oem"},{"id":"com.huawei.def","description":"DEF\nUnused? message things:allows the app public keys on connected devices, getting status, access information","removal":"replace","type":"oem"},{"id":"com.huawei.deskclock","description":"Huawei Clock App.\n","removal":"replace","suggestions":"clocks","type":"oem"},{"id":"com.huawei.desktop.explorer","description":"From XDA thread : \"Service that is been used when you wanna use your phone as an operative system on a PC.\"\nI don't understand what does it mean.\n","removal":"delete","type":"oem"},{"id":"com.huawei.desktop.systemui","description":"Huawei desktop mode switching\nIt has also Take Screenshot Service.\nRequire HMS, has a lot tracking.","removal":"replace","type":"oem"},{"id":"com.huawei.deviceauth","description":"TrustedDeviceAuth\nI guess it's for authentication keys","removal":"replace","type":"oem"},{"id":"com.huawei.devicegroupmanage","description":"(Unused?)HwGroupManager\nIt's an app without activities, only device info things","removal":"caution","type":"oem"},{"id":"com.huawei.devicemanager","description":"Needed for Huawei account. I found only things: installing service?, Linked devices, Multi-Device management, connection code, metrics","removal":"delete","type":"oem"},{"id":"com.huawei.distributed.kms","description":"HwDistributedKeyManager\nHas random frameworks, probably needed for huawei account, authentication","removal":"delete","type":"oem"},{"id":"com.huawei.distributedpasteboard","description":"SuperHub\nHas something to History Clipboard and Drag Drop.","removal":"replace","type":"oem"},{"id":"com.huawei.dmsdp","description":"Multi-Device management\nThis service allows you to virtually expand your devices capabilities\nby connecting to other devices and using them as an extension of this device.","removal":"replace","type":"oem"},{"id":"com.huawei.dsdscardmanager","description":"Dual card management\nIt's sim card management in Huawei settings.\nAfter removing, sim card management in settings will not work.","removal":"unsafe","type":"oem"},{"id":"com.huawei.dtmfanalyzer","description":"Dtmf Analyzer\nHas something to media player to ride mode for China.","removal":"delete","type":"oem"},{"id":"com.huawei.easygo","description":"HwEasyGo\nLooks like a backdoor for WeChat app?\nEasyGoServer, error code things found, also EasyGoCallBack.\nThese app is probably backdoor or for developers.\nThis app has no permissions and no activities.","removal":"delete","type":"oem"},{"id":"com.huawei.email","description":"Huawei Email app.\n","removal":"replace","suggestions":"email_clients","type":"oem"},{"id":"com.huawei.entitlement","description":"Needed for messages Bluetooth, wifi, hotspot, Terms of Agreement, Authentication?\nOn some phone's it has some features, and on some not.","removal":"caution","type":"oem"},{"id":"com.huawei.fans","description":"Huawei Club is the official forum for Huawei users.\nIt provides the latest news and information, FAQs, and tutorials about Huawei and Honor products.","dependencies":["com.huawei.hwid"],"removal":"delete","type":"oem"},{"id":"com.huawei.fastapp","description":"Quick App Center\nComponent of AppGallery (Huawei's app store) providing Quick Apps support. Quick Apps are Javascript+CSS apps that don't need any installation. This technology has its uses but I'm personally not a huge fan on having to rely on a JS engine to run an application\nThis system app has a lot of permissions (including SEND_SMS, CAMERA, READ_EXTERNAL_STORAGE, RECORD_AUDIO... why?)\nMore information: https://www.xda-developers.com/huawei-quick-apps-alternative-google-instant-apps/\n OW2 Quick App whitepaper: https://quick-app-initiative.ow2.io/docs/Quick_App_White_Paper.pdf","removal":"replace","dependencies":["com.huawei.hwid"],"type":"oem"},{"id":"com.huawei.featurelayer.featureframework","description":"FeatureFramework\nAllows the app to check for feature updates.","removal":"replace","type":"oem"},{"id":"com.huawei.featurelayer.sharedfeature.map","description":"Huawei Map Service\nRequire FeatureFramework.\nOnly uses Chinese AMap to get location.\nUsed to show maps inside Calendar and Gallery, they will complain if you uninstall it, disable instead.","removal":"delete","type":"oem"},{"id":"com.huawei.fido.uafclient","label":"FIDO UAF Client","description":"Fido is a set of open technical specifications for mechanisms of authenticating users to online services that do not depend on passwords.\nThe UAF protocol is designed to enable online services to offer passwordless and multi-factor security by allowing users to register their device to the online service and using a local authentication mechanism such as iris or fingerprint recognition.\nSafe to remove if you don't use password-less authentification to access online servics.","web":["https://fidoalliance.org/specs/u2f-specs-1.0-bt-nfc-id-amendment/fido-glossary.html","https://fidoalliance.org/specs/fido-v2.0-rd-20170927/fido-overview-v2.0-rd-20170927.html","https://developers.google.com/identity/fido/android/native-apps"],"removal":"replace","type":"oem"},{"id":"com.huawei.filemanager","label":"Files","description":"Huawei file manager\nProbably fine to remove as long as you have another file manager.","removal":"replace","suggestions":"file_managers","type":"oem"},{"id":"com.huawei.firstbootinfo","description":"Get city info service\nGet city info service? Needed by your phone in order to sync with Huawei servers\nand check for apps updates, software updates and other access for your phone.","removal":"caution","type":"oem"},{"id":"com.huawei.game.kitserver","label":"HUAWEI Game KitServer","description":"Probably safe to remove if you don't play games\nHas window show, keybind set and more things.","removal":"replace","type":"oem"},{"id":"com.huawei.gameassistant","description":"Huawei Game Suite (HiGame).\nMobile game app store.\nhttps://club.hihonor.com/in/topic/16341/detail.htm\n","removal":"delete","type":"oem"},{"id":"com.huawei.gamebox","description":"GameCenter\nRequires HMS Core to run.\nIt's app for installing games.","dependencies":["com.huawei.hwid"],"removal":"delete","type":"oem"},{"id":"com.huawei.geofence","description":"GeofenceService.\nAllows you to do something when a user enters an area that has been defined as a trigger.\nA geofence is a virtual perimeter set on a real geographic area. Combining a user position with a geofence perimeter, \nit is possible to know if the user is inside or outside the geofence or even if he is exiting or entering the area.\n","removal":"delete","type":"oem"},{"id":"com.huawei.harmonyos.foundation","description":"foundation\nHas hwid, vehicle things, Aosp In Call Service permissions.\nDisable app instead of uninstalling, because breaks calling.\nSettings app and APK installation will become slow if you uninstall this.","removal":"caution","type":"oem"},{"id":"com.huawei.health","label":"Huawei Health","description":"Connect Huawei wearables to your phone and all sorts of stats like all fitness tracking apps.","dependencies":["com.huawei.hwid"],"removal":"delete","type":"oem"},{"id":"com.huawei.hff","description":"HFF\nHFF? It has not any code.","removal":"delete","type":"oem"},{"id":"com.huawei.hiaction","label":"HiAction","description":"No noticable consequences","removal":"delete","type":"oem"},{"id":"com.huawei.hiai","label":"HUAWEI HiAI Engine","description":"No noticable consequences","removal":"delete","type":"oem"},{"id":"com.huawei.hiassistantoversea","description":"HiVoice. Huawei's voice assistant to replace \"Hey Google\"\n","removal":"delete","type":"oem"},{"id":"com.huawei.hicar","description":"Huawei HiCar\nTo connect to Huawei HiCar, press and hold the wireless button on the steering wheel or control panel.\nMore useful in China.","dependencies":["com.huawei.hwid"],"removal":"delete","type":"oem"},{"id":"com.huawei.hicard","label":"HiCard","description":"Huawei Cards. No noticable consequences","removal":"delete","type":"oem"},{"id":"com.huawei.hicloud","description":"Huawei cloud features\n","removal":"delete","type":"oem"},{"id":"com.huawei.hidisk","description":"Huawei File Manager app.\n","removal":"replace","suggestions":"file_managers","type":"oem"},{"id":"com.huawei.hifolder","description":"Huawei Online Cloud folder service\nhttps://consumer.huawei.com/en/mobileservices/mobilecloud/\n","removal":"delete","type":"oem"},{"id":"com.huawei.hilink.framework","description":"AI Life Service\nUsed for smart devices and to run smart scenes.\nIt's more useful in China.","removal":"delete","type":"oem"},{"id":"com.huawei.himovie","label":"Huawei Video Player","description":"Bloated on newer versions. Can be safely removed if you don't want it.","removal":"replace","suggestions":"video_players","type":"oem"},{"id":"com.huawei.himovie.overseas","label":"HUAWEI Video","description":"Huawei stock video application. Has a lot of trackers.","removal":"replace","suggestions":"video_players","type":"oem"},{"id":"com.huawei.himovie.partner1","description":"HiMoviePlayerPlus\nWeird app without any code and there's nothing in Main Activity.","removal":"delete","type":"oem"},{"id":"com.huawei.himovie.partner2","description":"HiMoviePlayerPlus\nWeird app without any code and there's nothing in Main Activity.","removal":"delete","type":"oem"},{"id":"com.huawei.hiskytone","description":"SkyTone Overseas Data Service.","dependencies":["com.huawei.hwid"],"removal":"delete","type":"oem"},{"id":"com.huawei.hisuite","description":"Can be uninstalled via settings. App to connect with the desktop program to Backup/Restore, flash firmware, etc.\nhttps://consumer.huawei.com/en/support/hisuite/","removal":"delete","type":"oem"},{"id":"com.huawei.hitouch","description":"Huawei HiTouch\nAssistant capable to recognize the objects in a photo and to search them through various shopping sites.\nhttps://consumer.huawei.com/uk/support/faq/have-you-tried-the-new-hitouch-assistant/\n","removal":"delete","type":"oem"},{"id":"com.huawei.hiview","label":"hiview","description":"Provides info on exposure and so on in the gallery","removal":"delete","type":"oem"},{"id":"com.huawei.hiviewtunnel","label":"HiViewTunnel","description":"This displays details/attributes of pictures in the gallery (ISO, exposure time, etc.).","removal":"delete","type":"oem"},{"id":"com.huawei.hms5gkit.agentservice","label":"HwModemKitAgentService","description":"Something to do with 5G? For some reason, this is also installed on a non-5G device. Can be uninstalled from phone settings so should be safe if you don't have 5G.","removal":"caution","type":"oem"},{"id":"com.huawei.honorclub.android","description":"Honor Club\nHuawei's social media app. Safe to remove.","removal":"delete","type":"oem"},{"id":"com.huawei.hwasm","label":"FIDO UAF ASM","description":"FIDO UAF Autenthicator-Specific Module.\nSee 'com.huawei.fido.uafclient' for FIDO explaination.\nThe UAF Authenticator-Specific Module (ASM) is a software interface on top of UAF authenticators which gives a standardized way for FIDO UAF clients to detect and access the functionality of UAF authenticators and hides internal communication complexity from FIDO UAF Client.","web":["https://fidoalliance.org/specs/fido-uaf-v1.0-ps-20141208/fido-uaf-asm-api-v1.0-ps-20141208.html"],"removal":"replace","type":"oem"},{"id":"com.huawei.hwblockchain","label":"HwBlockChain","description":"Probably blockchain related. No noticable consequences when removed","removal":"delete","type":"oem"},{"id":"com.huawei.hwddmp","description":"HwDDMP\nHas a lot frameworks.\nUninstalling breaks calling app even if you disable.\nIt does not cause bootloop but removing is highly not recommended.\nNot only breaks the dialer app, but causes lag in whole system too.","removal":"unsafe","type":"oem"},{"id":"com.huawei.hwdetectrepair","label":"Smart diagnosis","description":"(Discontinued?) Useless features and run in background.\n","removal":"delete","type":"oem"},{"id":"com.huawei.hwdiagnosis","description":"HwDiagnosis\nNo activities, only code about diagnosis and logs\nAlso device info and is send to Huawei servers.","removal":"delete","type":"oem"},{"id":"com.huawei.hwdockbar","label":"Multi-Window","description":"Probably fine to remove if you're not using Huawei Multi-Window features.","removal":"caution","type":"oem"},{"id":"com.huawei.hwesimservice","description":"HwESIMService\nIf you use eSIM do not remove it.","removal":"replace","type":"oem"},{"id":"com.huawei.hwid","label":"HMS Core","description":"Huawei Mobile Services\nHuawei’s alternative to Google Play Services. Needed to access advanced Huawei features.\nA Huawei ID is required to access services, like Themes, Mobile Cloud, HiCare, Huawei Wear, Huawei Health.","web":["https://www.xda-developers.com/huawei-hms-core-android-alternative-google-play-services-gms/","https://github.com/0x192/universal-android-debloater/issues/477"],"removal":"replace","required_by":["com.huawei.fastapp"],"warning":"This may break apps like InPost Mobile","type":"oem"},{"id":"com.huawei.hwireader","description":"App for adding by user 'Books'. This app has ads and bad privacy.","removal":"delete","type":"oem"},{"id":"com.huawei.hwpanpayservice","label":"HwPanPayService","description":"Payment related service.","removal":"delete","type":"oem"},{"id":"com.huawei.hwpolicyservice","description":"HwPolicyService\nAnother debugging, logs app.","removal":"delete","type":"oem"},{"id":"com.huawei.hwread.dz","description":"App for adding by user 'Books'. This app has ads and bad privacy.","removal":"delete","type":"oem"},{"id":"com.huawei.hwsearch","label":"Petal Search","description":"Huawei search widget. Used for finding apps/apks on serveral online sources (introduced after Google Mobile Services Ban).","removal":"delete","type":"oem"},{"id":"com.huawei.hwstartupguide","description":"A one-time setup app that is no longer needed","removal":"delete","type":"oem"},{"id":"com.huawei.hwusbearphoneupdate","description":"Huawei digital earphones come with built-in software that can be updated regularly to enhance your audio experience.","removal":"replace","type":"oem"},{"id":"com.huawei.hwvoipservice","label":"MeeTime Service","description":"Voice over IP service for Huawei MeeTime (com.huawei.meetime). Only works with EMUI 9.1+ Huawei phones.\nHuawei claims they use E2EE but this wasn't verified and there is no whitepaper, so don't trust them.\nThey also collect metadata.","web":["https://consumer.huawei.com/en/support/content/en-us00956296/"],"removal":"replace","required_by":["com.huawei.meetime"],"suggestions":"instant_messaging_apps","type":"oem"},{"id":"com.huawei.hwvplayer.youku","description":"Video Youku\nVideo Player on Chinese Huawei. Has some ads and bad privacy.","dependencies":["com.huawei.hwid"],"removal":"delete","type":"oem"},{"id":"com.huawei.iaware","description":"App Prioritizer. Prioritizes apps to avoid slowdown. Up to you but there is apparently no noticeable difference when deleted?\nSystem will start lagging a little bit (you will notice it while scrolling and on some animations, for example scrolling installed apps), battery consumption will become slightly higher and battery stats will disappear soon. At the same time, logcat will be full of errors like: PowerKit: PG Server is not found. calling pid xxxx.","removal":"caution","type":"oem"},{"id":"com.huawei.iconnect","label":"Device connection service","description":"Hidden menu 'install PCAssistant' when connected to PC? I have never seen it. Safe to remove.","removal":"delete","type":"oem"},{"id":"com.huawei.ihealth","description":"MotionService package, it's required for actions like shaking the phone to shut off the alarm, ecc.\n","removal":"delete","type":"oem"},{"id":"com.huawei.imedia.dolby","description":"Dolby Atmos\nOptimize sound automatically for exceptional audio quality.","removal":"replace","type":"oem"},{"id":"com.huawei.imedia.sws","label":"HUAWEI Histen","description":"Audio 3D effects found in the settings. Safe to remove if you don't use these settings.","removal":"delete","type":"oem"},{"id":"com.huawei.imonitor","description":"imonitor\nHidden logs founded in app.\nDetects app launch anomaly and runs safe mode.","removal":"delete","type":"oem"},{"id":"com.huawei.ims","description":"HwImsService\nVoLTE(Voice over LTE) calls. IP multimedia subsystem.\nhttps://en.wikipedia.org/wiki/Voice_over_LTE\nUninstalling app breaks sim calling even if you disable.\nIt does not cause bootloop but removing is highly not recommended.","removal":"unsafe","type":"oem"},{"id":"com.huawei.indexsearch","description":"Index search works without it.","removal":"delete","type":"oem"},{"id":"com.huawei.indexsearch.observer","description":"Index search works without it.","removal":"delete","type":"oem"},{"id":"com.huawei.indiacalendar","description":"Indian Calendar\nHUAWEI Calendar","removal":"replace","type":"oem"},{"id":"com.huawei.intelligent","description":"Huawei Assistant. Shopping recommendations\n","removal":"delete","type":"oem"},{"id":"com.huawei.internetaudioservice","description":"Smart headset control\nHas a lot logs and frameworks to mmitest.","removal":"delete","type":"oem"},{"id":"com.huawei.languagedownloader","description":"Allows you to load the system languages when changing them","removal":"replace","type":"oem"},{"id":"com.huawei.lbs","description":"Location Based Services\nLocation-based services (LBS) are applications or services that utilize location data from a user's device, such as a smartphone or GPS, to provide relevant information, directions, or recommendations tailored to their specific location. These services rely on a combination of technologies, including Global Positioning System (GPS), Wi-Fi, cellular networks, and sensors, to determine a user's position accurately.\nNOTE: Can cause bootloops on some devices.\nThe primary goal of LBS is to offer personalized and context-aware experiences based on a user's geographical location. This can include services like:\n1. Navigation and Mapping, 2. Location-Based Advertising, 3. Social Networking, 4. Emergency Services, 5. IoT Asset Tracking\nhttps://forum.huawei.com/enterprise/en/location-based-services/thread/703168442200375296-667213855346012160","removal":"caution","type":"oem"},{"id":"com.huawei.lives","description":"HiLives is jointly developed by Huawei and certified third-party service providers to provide lifestyle services for you.","dependencies":["com.huawei.hwid"],"removal":"delete","type":"oem"},{"id":"com.huawei.livewallpaper.artflower","description":"Live wallpapers","removal":"delete","type":"oem"},{"id":"com.huawei.livewallpaper.flowersbloom","description":"Live wallpapers","removal":"delete","type":"oem"},{"id":"com.huawei.livewallpaper.mountaincloud","description":"Live wallpapers","removal":"delete","type":"oem"},{"id":"com.huawei.livewallpaper.naturalgarden","description":"Live wallpapers","removal":"delete","type":"oem"},{"id":"com.huawei.livewallpaper.paradise","description":"Live wallpapers","removal":"delete","type":"oem"},{"id":"com.huawei.livewallpaper.ripplestone","description":"Live wallpapers","removal":"delete","type":"oem"},{"id":"com.huawei.localbackup","description":"Huawei Backup\nhas a lot permissions and can backup your data","removal":"delete","type":"oem"},{"id":"com.huawei.magazine","description":"Magazine unlock. Downloads wallpapers for your lock screen.\n","removal":"delete","type":"oem"},{"id":"com.huawei.manufacture.wificonnect","description":"Wifi Connect\nNeeded for WiFi connection?","removal":"caution","type":"oem"},{"id":"com.huawei.maps.app","label":"Petal Maps","description":"Huawei map and navigation app with HMS (Huawei Mobile Services) trackers.\n","web":["https://play.google.com/store/apps/details?id=com.huawei.maps.app","https://beta.pithus.org/report/d15349e7a998306012462484f270f93794141113d6680fa8512931c270adf830"],"removal":"replace","suggestions":"maps","type":"oem"},{"id":"com.huawei.mediacontroller","description":"Can be disabled via Settings. Will disable the media controls in quick settings. Won't remove them however.","removal":"delete","type":"oem"},{"id":"com.huawei.meetime","description":"MeeTime (https://consumer.huawei.com/en/support/content/en-us00956296/). Voice and video calling application by Huawei. Only workds with EMUI 9.1+ Huawei phones. Huawei claims they use E2EE but this wasn't verified and there is not Whitepaper so don't trust them. They also collect metadata. There is no advantages to use this app instead of the reputed open-source Signal app.","removal":"delete","dependencies":["com.huawei.hwvoipservice"],"type":"oem"},{"id":"com.huawei.mirror","description":"Huawei Mirror app.\nMirror like \"Glass\"","removal":"delete","type":"oem"},{"id":"com.huawei.mirrorlink","description":"Huawei Mirror app. \nMirror like \"Glass\"\n\nHuawei mirrorlink implementation\nUsed to connect your phone to a car (with https://mirrorlink.com/ support) in order to provide audio streaming, GPS navigation...\n","removal":"delete","type":"oem"},{"id":"com.huawei.mmiautotest","description":"MMIAutoTest\nHidden testing hardware things.","removal":"delete","type":"oem"},{"id":"com.huawei.mmitest","description":"MMITest\nHidden hardware tests. Safe to remove.","removal":"delete","type":"oem"},{"id":"com.huawei.motionservice","description":"Gesture Service\nNot very useful gestures available in settings.","removal":"replace","type":"oem"},{"id":"com.huawei.msdp","description":"Multimodal Sensor Data Platform\nHas Device Status, Movement, Spatial Aware.\nUses location, airlink permissions.","removal":"delete","type":"oem"},{"id":"com.huawei.multimedia.audioengine","description":"Audio Engine, probably not needed.\nhttps://developer.huawei.com/consumer/en/audioengine","removal":"delete","type":"oem"},{"id":"com.huawei.multimedia.hivideoplayengine","description":"hivideoplayengine\nHwVideoKitImpl, sdk tools for developers.","removal":"delete","type":"oem"},{"id":"com.huawei.music","description":"Huawei Music app. Fat music player developed by Huawei (137MB. Seriously?).\n","removal":"replace","suggestions":"music_apps","type":"oem"},{"id":"com.huawei.mycenter","label":"My Center","description":"Huawei Member Center\nGives reward offers and news to Huawei users. Very intrusive app: 68 permissions inclusing CAMERA, ACCESS_FINE_LOCATION, REQUEST_INSTALL_PACKAGES. Can run in the background and just after boot even if you haven't unlock your phone yet. Phones home.","web":["https://beta.pithus.org/report/3af49c621aefeef0dca86a4f79b5f007d73698fa979d3ba1ac7d6f1ccaea9cdf"],"removal":"delete","type":"oem"},{"id":"com.huawei.nb.service","description":"Service for discovering nearby devices by GPS","removal":"caution","type":"oem"},{"id":"com.huawei.nearby","description":"HwNearby\nNeeded for Huawei Share features. (com.huawei.android.instantshare)\nHuawei Share features may not work after remove.\nNeeded to show a preview of recently opened apps in task manager. I agree, makes no sense, but that's what it is.","dependencies":["com.huawei.android.instantshare"],"removal":"replace","type":"oem"},{"id":"com.huawei.notepad","description":"Notepad\nLooks so bloated with Hi Voice, Update using AppGallery, Location.\nIt has a few tracking components, but not too many.","removal":"delete","type":"oem"},{"id":"com.huawei.numberidentity","description":"Designed to block unwanted calls, but only works properly in China","removal":"delete","type":"oem"},{"id":"com.huawei.ohos.collaborationcenter","description":"Sometimes ohos apps is configuration app before building.\nIn code I found it's for app cast transfer.\nDisable instead of uninstalling if you don't want to lose the widget.","removal":"delete","type":"oem"},{"id":"com.huawei.ohos.famanager","description":"Service Center\nProvides a space for you to view and manage services.\nThis app and its underlying services (which provide searches and AI-based services). A lot tracking.\nDisable instead of uninstalling if you don't want to lose the widget.","removal":"delete","type":"oem"},{"id":"com.huawei.ohos.hiwindow","description":"HiWindow\nHave loading, waiting png files\ncollaboration devices, hardware things, screen projection on activities shows Window Shell.\nAfter remove you will lose animations and loading screen.\nDisable instead of uninstalling if you don't want to lose the widget.","removal":"delete","type":"oem"},{"id":"com.huawei.ohos.inputmethod","label":"Celia Keyboard","description":"Huawei default Keyboard.","removal":"replace","warning":"Make sure to have another keyboard app installed before uninstalling it","suggestions":"keyboards","type":"oem"},{"id":"com.huawei.ohos.security.privacycenter","description":"It's shell to app 'com.huawei.security.privacycenter'?\nDisable instead of uninstalling if you don't want to lose the widget.","removal":"delete","type":"oem"},{"id":"com.huawei.omacp","description":"Provisioning message\nUsed for provisioning APN settings to devices via SMS. I think you shouldnt touch it if you want sms messages.","removal":"caution","type":"oem"},{"id":"com.huawei.onehopsvcclient","description":"OneHopSvcClient\nTagService, HarmonyTag.","removal":"delete","type":"oem"},{"id":"com.huawei.onehopsvchost","description":"OneHopSvcHost\nTriggerservice opens hardware things by secret code.","removal":"delete","type":"oem"},{"id":"com.huawei.parentcontrol","description":"Parental controls functions.\nIt seems Huawei can prevent to remove packages. Uninstalling (or even disabling) this package returns an error: Failure [DELETE_FAILED_INTERNAL_ERROR] (not allowed to disable this package).\n See https://github.com/0x192/universal-android-debloater/issues/51","removal":"delete","type":"oem"},{"id":"com.huawei.pcassistant","description":"HiSuite service. Used by HiSuite PC software.\nHiSuite enables you to backup your data and restore them from/to your phone.\nhttps://consumer.huawei.com/en/support/hisuite/\n","removal":"delete","type":"oem"},{"id":"com.huawei.pengine","description":"HUAWEI Intelligent Suggestion will collect and analyze screens from selected apps to provide personalized services.","removal":"delete","type":"oem"},{"id":"com.huawei.permissioncontroller","description":"Can be disabled via Settings as well","removal":"delete","type":"oem"},{"id":"com.huawei.permissioncontroller.overlay","description":"Found help app permissions - only link to Google site in code, overlay to permission things like buttons, dialogs, etc.\nBut it's not needed?","removal":"caution","type":"oem"},{"id":"com.huawei.phoneservice","label":"HiCare","description":"Provides you common online services including customer services, issue feedback, user guides, service centers and self-service.","removal":"delete","type":"oem"},{"id":"com.huawei.photos","label":"Gallery","description":"Huawei Gallery app.\nNote: The official camera app refuses to open photos in another gallery (you can't review your picture from the camera app)\n","removal":"replace","suggestions":"gallery","type":"oem"},{"id":"com.huawei.powergenie","label":"Power Genius","description":"Task killer app in EMUI 9+ (Android 9+).\nTask killer apps tend to do more harm than help as they clear cached RAM for no good reason, removing the battery and time savings involved in caching. In addition to the obvious issues with background functionality like notifications. However, system may start lagging a little bit (you will notice it while scrolling and on some animations, for example scrolling installed apps), battery consumption will become slightly higher and battery stats will disappear soon. At the same time, logcat will be full of errors like: PowerKit: PG Server is not found. calling pid xxxx.","removal":"caution","type":"oem"},{"id":"com.huawei.printservice","label":"Default Print Service","description":"Print service for HUAWEI.","removal":"caution","type":"oem"},{"id":"com.huawei.privatespace","description":"Privacy Space for files","removal":"replace","type":"oem"},{"id":"com.huawei.profile","description":"Profile Services\nIt's an app needed for HUAWEI ID, depends on HMSCore, also has analytics and trackers.","removal":"delete","type":"oem"},{"id":"com.huawei.rcsserviceapplication","label":"Huawei RCS","description":"RCS = Rich Communication Service.\nProvides sending messages to another Huawei phone.\nHidden RCS Chats with 49 permissions.","removal":"delete","type":"oem"},{"id":"com.huawei.recsys","description":"HwIntelligentRecSystem\nis a smart app that provides personalized services on HiBoard? My phone dont have Hiboard. Safe to remove.\nhttps://consumer.huawei.com/en/support/content/en-us00434788/","removal":"delete","type":"oem"},{"id":"com.huawei.regservice","description":"RegService\nRegister for celluar network services, not needed.","removal":"delete","type":"oem"},{"id":"com.huawei.remotepassword","description":"RemotePassword\nremotepassword: errors, invalid, null, errors generally\nalso subscribe info, device info. Better remove that.","removal":"delete","type":"oem"},{"id":"com.huawei.ridemode","description":"Uses google maps website to track your ride when RideMode is Enabled.","removal":"delete","type":"oem"},{"id":"com.huawei.runningtestii","description":"Testing hardware things like Audio, Camera, VideoPlayer, etc.","removal":"delete","type":"oem"},{"id":"com.huawei.sarlevel","description":"Mobile phone SAR\nHidden SAR Level Activity.","removal":"delete","type":"oem"},{"id":"com.huawei.scanner","description":"AI Lens. Shop for objects that you take a picture of. This de-clutters the camera interface by removing the AI Lens button on the top left corner and does not break the AR Measure app.","removal":"delete","type":"oem"},{"id":"com.huawei.scenepack","description":"Travel Assistant provides you with travel tips, voice guides, and other related services.","removal":"delete","type":"oem"},{"id":"com.huawei.screenrecorder","description":"Stock screen recorder\nUseful only in pre-Android Pie for having system audio recording without root\nNot sure if it's an app, but I can't find it on my phone. Safe to remove.","removal":"replace","type":"oem"},{"id":"com.huawei.search","label":"HUAWEI HiSearch","description":"Allows you to search through settings, files, contacts and notes while keeping a record of your search history.\nHi Search is really annonying because it's triggered as soon as you wipe down from the middle part of the home.","removal":"delete","type":"oem"},{"id":"com.huawei.searchservice","description":"Fusion Search Service\nNeeded for searching? Found out that it uses `com.huawei.hwddmp` this app is for debugging errors\nalso found trackers components that connects to Huawei servers unnecessarily.","removal":"delete","type":"oem"},{"id":"com.huawei.secime","description":"Huawei Secure IME\nSecure keyboard.","removal":"delete","type":"oem"},{"id":"com.huawei.security.privacycenter","description":"Protect your privacy by preventing apps from accessing sensitive data stored within images,\nsuch as location info and time stamps. This restriction does not apply to system apps such as Gallery and Cloud.\nI don't think this app values privacy a lot.\nNeeded for Permission Manager to open.","removal":"caution","type":"oem"},{"id":"com.huawei.securitymgr","label":"PrivateSpace","description":"Lets you store private information in a hidden space within your device that can only be accessed with your fingerprint or password.\nThis is the password vault or manager too.","web":["https://consumer.huawei.com/en/support/content/en-us00754246/"],"removal":"replace","type":"oem"},{"id":"com.huawei.securitypluginbase","description":"HwSecurityPluginBase\nAntivirus Service supports bad antiviruses, HwLog, not needed","removal":"delete","type":"oem"},{"id":"com.huawei.securityserver","description":"HwSecurityServer\nNot needed app without code and useless assets to spying apps.\nNeeded for face unlock, black screen will be shown if you remove this package (?). Doesn't apply to all devices","removal":"caution","type":"oem"},{"id":"com.huawei.skytone","description":"SkyTone Data Service. Frameworks to 'com.huawei.hiskytone' SkyTone Overseas Data Service.","dependencies":["com.huawei.hiskytone"],"removal":"delete","type":"oem"},{"id":"com.huawei.smarthome","description":"AI Life\nRequire Logging in to your Huawei ID, HMSCore.\nIt's app that lets your control and manage routers and audio accessories.","dependencies":["com.huawei.hwid"],"removal":"delete","type":"oem"},{"id":"com.huawei.smartlocation","description":"Huawei Indoor Positioning Services\nChinese Tracking Location.","removal":"delete","type":"oem"},{"id":"com.huawei.smartshot","description":"Smart screenshots\nNeeded for screenshots.","removal":"caution","type":"oem"},{"id":"com.huawei.sos","description":"SOS\nEmergency things.","removal":"delete","type":"oem"},{"id":"com.huawei.soundrecorder","description":"Sound Recorder app.","removal":"replace","type":"oem"},{"id":"com.huawei.spaceservice","description":"Huawei Geofence Services\nUses Chinese Location.","removal":"delete","type":"oem"},{"id":"com.huawei.stylus.floatmenu","description":"AI Lens. Shop for objects that you take a picture of. This de-clutters the camera interface by removing the AI Lens button on the top left corner and does not break the AR Measure app.\n \nFloating menu with M-Pen feature.\n","removal":"delete","type":"oem"},{"id":"com.huawei.suggestion","description":"HiSuggestion\nHw intelligence permissions found and logs, also maybe has suggestions on widget.","removal":"delete","type":"oem"},{"id":"com.huawei.synergy","description":"Huawei Cloud & Network Synergy.\nSeems to be related to B2B (Business To Business) cloud stuff.\nhttps://www.huawei.com/en/press-events/news/2016/10/Cloud-Network-Synergy-Whitepaper\n","removal":"delete","type":"oem"},{"id":"com.huawei.systemdebug","description":"SystemDebug\nTesting system things.","removal":"delete","type":"oem"},{"id":"com.huawei.systemmanager","description":"System Manager\nHuawei's stock phone cleaner. Consumes a lot of battery power for useless 'security' checks.\nSafe to remove unless you need any anti-virus (while there are better ones to be found). NOTE: breaks the functionality of closing and cleaning background apps.\nThis is more than a phone cleaner, you will lose a lot of settings like battery and notifications management if you remove this.","removal":"caution","type":"oem"},{"id":"com.huawei.systemserver","description":"Huawei System Services\nIt depends on (com.huawei.systemmanager).\nNeeded for navigation with a fingerprint reader that is on Mate 10, but fingerprint unlock will still work if you remove it.","removal":"caution","type":"oem"},{"id":"com.huawei.tips","description":"HUAWEI Feature Advisor\nPeriodically gives you notifications on how to use certain features on your phone.\n","removal":"delete","type":"oem"},{"id":"com.huawei.tipsove","description":"Tips\nHuawei Tips to HiCar, AI Life and more.","removal":"delete","type":"oem"},{"id":"com.huawei.tmecustomize","description":"TME\nTme Activity. Hidden carrier config sim.\nI think it's useless but you need to test.","removal":"replace","type":"oem"},{"id":"com.huawei.trustagent","description":"Smart unlock feature.\nEnables you to unlock your phone with a Bluetooth device, like a smart band. \nWhen a compatible Bluetooth device is detected, you can unlock your phone with a simple swipe (without a password).\n","removal":"delete","type":"oem"},{"id":"com.huawei.trustcircle","description":"Device authentication service\nThe app's usage is unknown, but it can be removed if apps such as com.huawei.hwid, com.huawei.appmarket are also removed. This is what I found while exploring the APK: Huawei Mobile Services?, managing installation and updates of AppGallery.\nHuawei HWID account may be inaccessible without this app.","removal":"delete","type":"oem"},{"id":"com.huawei.trustedthingsauth","description":"HwTrustedThingsAuth\nOnly name app found in code, has permissions:\nBluetooth, execute_reg, use_auth.","removal":"delete","type":"oem"},{"id":"com.huawei.trustspace","description":"TrustSpace useless security to payment. Settings > Security > TrustSpace\nHas something to unrooting rooted phone.","removal":"delete","type":"oem"},{"id":"com.huawei.userguide","description":"User Guide\nRequire install a PDF reader app to work.","removal":"delete","type":"oem"},{"id":"com.huawei.vassistant","description":"HiVoice app\nHuawei voice assistant (like Siri or Google assistant)\nHuge privacy risk. Keep in mind that the app keeps the microphone *on* non-stop.\nIs now Celia (https://consumer.huawei.com/en/emui/celia/)\n","removal":"delete","type":"oem"},{"id":"com.huawei.vdrive","description":"Driving Mode uses HiVoice to provide voice services when you are driving.","removal":"delete","type":"oem"},{"id":"com.huawei.videoeditor","description":"Huawei Video editor\nIncludes 2 ads trackers. Interacts with Huawei cloud. Pithus analysis: https://beta.pithus.org/report/19ef8cfb02f3853128603a140b4602db57ddf729a728b1ea6998e8b20752138f","removal":"delete","type":"oem"},{"id":"com.huawei.vrservice","description":"Huawei VR Service used for Huawei VR.","removal":"delete","type":"oem"},{"id":"com.huawei.wallet","label":"HUAWEI Wallet","description":"Formerly, Huawei Pay, is a mobile payment and e-wallet service for Huawei devices that offers the same services as Apple Pay, Samsung Pay etc.","web":["https://consumer.huawei.com/en/mobileservices/huawei-wallet/"],"removal":"delete","type":"oem"},{"id":"com.huawei.wallet.sdk.walletsdk","label":"WalletSDK","description":"SDK for HUAWEI Wallet","removal":"delete","type":"oem"},{"id":"com.huawei.watch.sync","description":"Huawei Watch sync function\nIs it only used to sync Huawei watch ?\nSafe to remove according to several users\n","removal":"delete","type":"oem"},{"id":"com.huawei.waudio","description":"waudio\nWi-Fi Speaker, Speaker playback, WLAN speaker\nThis app is only for Chinese users because I found a Chinese app list that supports it\nand depends on AI Life app probably.","removal":"delete","type":"oem"},{"id":"com.huawei.webview","label":"Huawei WebView","description":"Allows Android apps to display web contents within the app itself, based on Chrome.","removal":"caution","warning":"Make sure to have another Webview before uninstalling it or some apps may not work properly and crash.","suggestions":"webviews","type":"oem"},{"id":"com.huawei.welinknow","description":"Link Now\nRequire login to Huawei ID Account.\nSome features is not available for some countries.\nIt's app for meeting and collects a lot data from users.","removal":"delete","type":"oem"},{"id":"com.huawei.wifieapsimplmn","description":"PredefinedEapSim\nNeeded for WiFi to work properly? WiFi may not work (?)","removal":"caution","type":"oem"},{"id":"com.huawei.wifiprobqeservice","label":"HwWifiproBqeService","description":"Needed for WiFi to work properly? WiFi may not work (?)","removal":"caution","type":"oem"},{"id":"com.hxy.ramtest","description":"ram test\nHidden ram test. Tests ram size.","removal":"delete","type":"oem"},{"id":"com.hy.system.fontserver","description":"Font Server\nNeeded for fonts?","removal":"caution","type":"oem"},{"id":"com.ibimuyu.lockscreen","description":"Dynamic theme service lockscree\nDynamic theme service lockscreen? Some people recommend removing that.","removal":"replace","type":"oem"},{"id":"com.idea.questionnaire","description":"Qusetionnaire\nLogs and testing things.","removal":"delete","type":"oem"},{"id":"com.iflytek.inputmethod.miui","description":"Chinese Keyboard to Miui China, replace Keyboard before remove.","removal":"delete","type":"oem"},{"id":"com.iflytek.speechsuite","description":"Default voice input method from iflytek, a big chinese company (https://en.wikipedia.org/wiki/IFlytek)\nIFLytek was implicated in human rights violations : \nhttps://asia.nikkei.com/Economy/Trade-war/US-sanctions-8-China-tech-companies-over-role-in-Xinjiang-abuses\nArchive: https://web.archive.org/save/https://asia.nikkei.com/Economy/Trade-war/US-sanctions-8-China-tech-companies-over-role-in-Xinjiang-abuses\n","removal":"delete","type":"oem"},{"id":"com.ims.dm","description":"IMS DM\nHidden network debugging","removal":"delete","type":"oem"},{"id":"com.infinix","description":"Only has something to launcher folders and has freeze png files.","removal":"caution","type":"oem"},{"id":"com.infinix.xshare","description":"XShare\nIts app for Transfer & Share files, also has ads.","removal":"delete","type":"oem"},{"id":"com.iqoo.aftersale.engineermode","description":"Testing things. This app is for testing phone components.","removal":"delete","type":"oem"},{"id":"com.iqoo.engineermode","description":"Factory test\nTesting things, AT Commands - no one uses them.","removal":"delete","type":"oem"},{"id":"com.iqoo.powersaving","description":"Battery\nPower-saving app. May not be a good idea to remove it.","removal":"caution","type":"oem"},{"id":"com.iqoo.secure","description":"i Manager\nRemoving this removes the traffic speed indicator in the notification bar. Be cautious about removing it.","removal":"caution","type":"oem"},{"id":"com.iqoo.user.engineermode","description":"Factory test\nIt's for testing phone components.","removal":"delete","type":"oem"},{"id":"com.iqoo.website","description":"iQOO.com\nShows iQOO website.","removal":"delete","type":"oem"},{"id":"com.itc.autotest","description":"ITC_Base\nHidden camera testing, also its Chinese.","removal":"delete","type":"oem"},{"id":"com.itel.TWSService","description":"This app is used for connection to TECNO Hipods.","removal":"replace","type":"oem"},{"id":"com.jrdcom.Elabel","label":"Regulatory & safety","description":"Useless data collection and security. Also Chinese things, PDF View.","removal":"caution","type":"oem"},{"id":"com.jrdcom.Elabel.a_overlay","description":"Useless overlay code for this bloatware app","removal":"delete","type":"oem"},{"id":"com.jrdcom.Elabel.overlay","description":"","removal":"caution","type":"oem"},{"id":"com.jrdcom.filemanager","label":"File Manager","description":"A file manager with ads","web":["https://play.google.com/store/apps/details?id=com.jrdcom.filemanager"],"removal":"caution","type":"oem"},{"id":"com.jrdcom.filemanager.a_overlay","description":"","removal":"caution","type":"oem"},{"id":"com.jrdcom.urlreservedapp1","description":"TCL My Sites shortcut\nStub for mysites.glance.com.","removal":"delete","type":"oem"},{"id":"com.kidoz.lenovo","label":"Lenovo Kid's Account","description":"Partnered with COPPA Certified (https://cert.privo.com/#/companies/kidoz18) KIDOZ Inc.\nProvides kids friendly relevant services based on their 'usage behaviour'.\nAccording to EFF sends device model, brand, country, timezone, screen size, view events, click events, logtime of events, and a unique “KID ID information to Kidoz.","web":["https://beta.pithus.org/report/af6c3674c3bdcacda3590eb657fef61c1b3b44100c7c6ae051309d5196104efa","https://www.virustotal.com/gui/file/af6c3674c3bdcacda3590eb657fef61c1b3b44100c7c6ae051309d5196104efa/detection","https://www.eff.org/deeplinks/2023/11/low-budget-should-not-mean-high-risk-kids-tablet-came-preloaded-sketchyware","https://www.privo.com/kidoz-case-study","https://kidoz.net/privacy-policies"],"removal":"delete","type":"oem"},{"id":"com.kidsedu","description":"Another Kids Mode.","removal":"delete","type":"oem"},{"id":"com.kikaoem.hw.qisiemoji.inputmethod","description":"Kika Keyboard\nThird party keyboard. Installs on some Huawei devices.","removal":"delete","type":"oem"},{"id":"com.knox.vpn.proxyhandler","label":"KnoxVpnPacProcessor","description":"Samsung Knox allows business and personal content to \"securely\" coexist on the same handset.\nThis package handles proxies alongside KNOX.","removal":"delete","type":"oem"},{"id":"com.kyocera.ecomode","description":"Custom power-saving mode","removal":"unsafe","type":"oem"},{"id":"com.lbe.security.miui","description":"Permission manager\nLets you monitor apps permission requests.\n","removal":"unsafe","type":"oem"},{"id":"com.lbe.security.miui.customizedregion.overlay","description":"Something about WiFi calling \"vowifi\", \"volte\", \"notification on keyguard\"\nIt's unused.","removal":"delete","type":"oem"},{"id":"com.ldmnq.launcher3","description":"Built in launcher\nFull of ads but not safe to disable unless you have another launcher","removal":"replace","type":"oem"},{"id":"com.lenovo.agent0","label":"OCZ_ProvisionAgent","description":"Not sure what it does.","web":["https://beta.pithus.org/report/9ebdba931604f90815b8014cbba12b91521811ac49a292dab438006"],"removal":"delete","type":"oem"},{"id":"com.lenovo.csdkplatform","label":"PlatformApk","description":"CSDK = Commercial Software Development Kit (CSDK) for details see official video.","web":["https://videos.emea.lenovo.com/lenovo-commercial-software","https://beta.pithus.org/report/130d7076019d7a4519a04c80dbd80fa3734542dc44ec1f5fba464569fb3f4adc"],"removal":"delete","type":"oem"},{"id":"com.lenovo.dagent","label":"OCZ_DeployAgent","description":"Not sure what it does.","web":["https://beta.pithus.org/report/d5bd860fb7c42078618118213913ea6d58fec2a51904728f2f429840386d0c70"],"removal":"delete","type":"oem"},{"id":"com.lenovo.dsa","label":"OCZ_DeployServiceApp","description":"Dynamic System Analysis (DSA) collects and analyzes system information to aid in diagnosing system problems.","web":["https://support.lenovo.com/us/en/solutions/LNVO-DSA","https://beta.pithus.org/report/2e4a0b3fa9ea4fb56a7bd90d1d2c9eb4cd7fffe481943e0f6bfb8a23554cb921"],"removal":"delete","type":"oem"},{"id":"com.lenovo.launcher","label":"LenovoLauncher","description":"Lenovo's default device launcher.","web":["https://beta.pithus.org/report/3cdd48fb6a9c94435fb1b46b41a19c553b96ae889aaa2d7285f28cd64d12363e"],"removal":"replace","warning":"Make sure have installed another launcher before debloating this app","suggestions":"launchers","type":"oem"},{"id":"com.lenovo.launcher.provider","label":"LenovoLauncherProvider","description":"Related to lenovo launcher(com.lenovo.launcher)?\nExactly not sure what it does.","web":["https://beta.pithus.org/report/c94afd8d85adb4c139ca6e78cd0fe643ebe0d0046480f2b12235e698eeccaa51"],"removal":"caution","type":"oem"},{"id":"com.lenovo.leos.appstore","description":"Lenovo Application Center 应用中心\nLenovo chinese app store","removal":"delete","type":"oem"},{"id":"com.lenovo.leos.cloud.sync","description":"SYNCit\nLenovo cloud","removal":"delete","type":"oem"},{"id":"com.lenovo.levoice.caption","description":"AI Live Caption\nCaption Settings.","removal":"delete","type":"oem"},{"id":"com.lenovo.levoice.trigger","description":"Wake up with voice","removal":"delete","type":"oem"},{"id":"com.lenovo.levoice_agent","description":"Voice unlock screen\nVoice unlock screen? Another chinese accessibility things.","removal":"delete","type":"oem"},{"id":"com.lenovo.levoice_notes","description":"AI Notes\nAnother AI things lenovo.","removal":"delete","type":"oem"},{"id":"com.lenovo.lsf","label":"Lenovo ID","description":"Lenovo ID adds an option in Settings>Accounts where you can login to a Lenovo ID account.\nFeatures include \"exclusive features directly from Lenovo and our partners\" and \"syncing users information across devices\"\nlsf = Lenovo Service Framework","web":["https://beta.pithus.org/report/f181581838c8898634160e5cceada5fd3e4032b6ad9eae39ff788e8cc2922db7"],"removal":"delete","type":"oem"},{"id":"com.lenovo.lsf.device","label":"Device Service","description":"lsf = Lenovo Service Framework.\nExactly not sure what it does.","web":["https://beta.pithus.org/report/1cbd510ef561561b3d850583e9467f16b9ae2d94280c9b2a50bcb8cfbad6ccf9"],"removal":"delete","type":"oem"},{"id":"com.lenovo.lsf.user","description":"Lenovo ID adds an option in Settings>Accounts where you can login to a Lenovo ID account.\nFeatures include \"exclusive features directly from Lenovo and our partners\" and \"syncing users information across devices\"\nlsf = Lenovo Service Framework.","removal":"delete","type":"oem"},{"id":"com.lenovo.ocpl","label":"OCZ_ClientDownloader","description":"Not sure what it does.","web":["https://forums.lenovo.com/t5/Lenovo-Android-based-Tablets-and-Phablets/OCZ-ClientDownloader/m-p/5090852?page=1#5396079","https://beta.pithus.org/report/8a1884d7604d3c3ab7559d6dcffe88b77243e85bee38ff100f1331043b6d5e6f"],"removal":"delete","type":"oem"},{"id":"com.lenovo.ota","label":"System Update","description":"App for your Over-The-Air (OTA) system update.","removal":"caution","type":"oem"},{"id":"com.lenovo.tab4_8plus","label":"Lenovo TAB4 8 Plus","description":"App that shows TAB4 8 Plus device specifications.","removal":"delete","type":"oem"},{"id":"com.lenovo.ue.device","label":"UserExperience","description":"User Experience Program\nAnalytics stuff. Displays an annoying notification after every reboot prompting you to join this user experience program.","web":["https://beta.pithus.org/report/54cc80e774c0106e2b74b7b8d50981ad2108dad2f8d4b2c6a5115e14256807f6"],"removal":"delete","type":"oem"},{"id":"com.lenovo.wallpaper","label":"Wallpapers","description":"Wallpaper manager app for lenovo devices.","removal":"delete","type":"oem"},{"id":"com.lenovo.weather.theme.dreamlandPad","label":"Lenovo Weather","description":"Adds widget for lenovo weather app(com.tblenovo.lewea)","web":["https://beta.pithus.org/report/aff85db4566c53f4b0668f603aa99dfcae13dd165835cca4d6d0b59265145f18"],"removal":"delete","type":"oem"},{"id":"com.lenovo.weathercenter","description":"Widget Weather\nLenovo ZUI Weather center","removal":"delete","type":"oem"},{"id":"com.lge.LGSetupView","description":"Setup View\nLG first setup (related to com.android.LGSetupWizard). \nThe first time you turn your device on, a Welcome screen is displayed. It guides you through the basics of setting up your device.\n","removal":"delete","type":"oem"},{"id":"com.lge.NfcSettings","description":"NFC settings\nLikely in the settings.","removal":"caution","type":"oem"},{"id":"com.lge.android.atservice","description":"Software Update\nIt's probably the only component of software updates.","removal":"caution","type":"oem"},{"id":"com.lge.animal.resource","description":"AnimalResource\nAnother thing for avatars.","removal":"delete","type":"oem"},{"id":"com.lge.app.floating.res","description":"Multitasking framework\nAllows you to use multitasking features like multiple apps in one screen.\nDoes not remove screen pinning feature.\nI don't know if this removes the floating windows feature that you have to enable with ADB (to make it look more like a desktop)","removal":"replace","type":"oem"},{"id":"com.lge.appbox.client","description":"LG Application manager\nInstalls/Updates LG related apps?\n","removal":"delete","type":"oem"},{"id":"com.lge.appwidget.runningtaskbar","description":"Recent Task App Widget.","removal":"delete","type":"oem"},{"id":"com.lge.arcamera","description":"ARCamera\nThis application requires google play services for ar. Not sure what for it is but probably for VR.","removal":"delete","type":"oem"},{"id":"com.lge.auto_generated_rro_product__","description":"It has important configs. Better don't risk.","removal":"unsafe","type":"oem"},{"id":"com.lge.bluetoothsetting","description":"Needed for Bluetooth.","removal":"caution","type":"oem"},{"id":"com.lge.bnr","description":"LG Backup\nCan backup your mobile devices LG Home screen, device settings, apps, and contacts to your computer.\nhttps://www.lg.com/us/support/help-library/lg-android-backup-CT10000025-20150104708841\n","removal":"delete","type":"oem"},{"id":"com.lge.bnr.launcher","description":"LG Mobile Switch Launcher\nThis doesn't remove the default launchers.\nIt is most likely to backup/restore the user's launcher configuration.\n","removal":"delete","type":"oem"},{"id":"com.lge.calligraphydictionary","description":"Calligraphy dictionary\nLooks useless.","removal":"delete","type":"oem"},{"id":"com.lge.camera","description":"Camera\nStock camera app.","removal":"replace","type":"oem"},{"id":"com.lge.camerasolution","description":"Needed for Stock camera app.","removal":"replace","type":"oem"},{"id":"com.lge.cic.eden.service","description":"Memories album\nGallery automatically creates a Memories album with pictures and videos saved in the phone. \nMemories is a virtual album of pictures saved in the phone or SD card.\nSource : https://www.lg.com/hk_en/support/product-help/CT30019000-1433767985158-others\n","removal":"delete","type":"oem"},{"id":"com.lge.cinemagraph","description":"Cine shot\nI think it's an optional app.","removal":"delete","type":"oem"},{"id":"com.lge.clock","description":"LG Clock app\n","removal":"replace","type":"oem"},{"id":"com.lge.cmas","description":"Emergency Alerts\nit's only for alerts so it's safe to remove.","removal":"replace","type":"oem"},{"id":"com.lge.covertools","description":"Dual Screen Tool\nNeeded for dual-screen I guess.","removal":"delete","type":"oem"},{"id":"com.lge.displayfingerprint","description":"It's fingerprint testing. Secret code 346437.","removal":"delete","type":"oem"},{"id":"com.lge.drmservice","description":"DRM Service\nProbably required for some forms of DRM; disabling might break things like Netflix streaming, which relies on DRM to function.\nhttps://en.wikipedia.org/wiki/Digital_rights_management","removal":"delete","type":"oem"},{"id":"com.lge.dsmanager","description":"Dual Screen Updater\nIf you dont use dual screen feature then it's safe to remove?","removal":"caution","type":"oem"},{"id":"com.lge.dualscreenfirmware","description":"Shows error dialog for dual-screen firmware update.","removal":"delete","type":"oem"},{"id":"com.lge.easyhome","description":"LG EasyHome\nEasyHome is a more simplified version of the Home screen that you can choose to use on your phone.\nIt displays the Home screen like a remote control device. T\nSource : https://www.lg.com/us/mobile-phones/VS985/Userguide/048.html\n","removal":"delete","type":"oem"},{"id":"com.lge.effect","description":"It's for lock screen effect.","removal":"delete","type":"oem"},{"id":"com.lge.ellievision","description":"Smart Cam & AI Tips\nDelete if not using default camera.","removal":"replace","type":"oem"},{"id":"com.lge.eltest","description":"ELTest\nDevice hardware tests settings\n","removal":"delete","type":"oem"},{"id":"com.lge.email","description":"LG Email app\n","removal":"delete","type":"oem"},{"id":"com.lge.entitlementcheckservice","description":"It's for miracast and wifi hotspot probably.","removal":"caution","type":"oem"},{"id":"com.lge.equalizer","description":"Equalizer settings.","removal":"replace","type":"oem"},{"id":"com.lge.eula","label":"Terms of Use for LG apps","description":"LG EULA (Terms of Use) accessible in the settings.\nEULA = End User License Agreement.\nNeeded by LG HD Audio Recorder or it will force close (maybe used by some other stock app?)","web":["https://en.wikipedia.org/wiki/End-user_license_agreement"],"removal":"caution","type":"oem"},{"id":"com.lge.eula.downloader","label":"Privacy Policy Update","description":"Not tested but probably safe to remove.","removal":"caution","type":"oem"},{"id":"com.lge.eulaprovider","label":"License Provider","description":"Needed by com.lge.eula.\nNeeded by LG HD Audio Recorder or it will force close (maybe used by some other stock app?)","removal":"caution","type":"oem"},{"id":"com.lge.exchange","description":"It looks like the Microsoft outlook/email in the logo. Believe this is some sort of microsoft integration.\nI don't 100% remember if I was able to add accounts to the phone still (eg. Nextcloud), I need to test that soon.","removal":"replace","type":"oem"},{"id":"com.lge.faceglance.trustagent","description":"Face Recognition\nRemove if you don't need it. If you want security I don't think this is a good idea to use it.","removal":"replace","type":"oem"},{"id":"com.lge.filemanager","description":"Stock file manager\n","removal":"replace","type":"oem"},{"id":"com.lge.floatingbar","description":"LG Floating bar\nLets you put shortcuts to apps or features, as well as quick access to contacts and music player controls on a \"floating bar\" on the Home screen.\nhttps://www.neowin.net/news/lg-v30-closer-look-floating-bar/\n","removal":"delete","type":"oem"},{"id":"com.lge.fmradio","description":"FM radio app\n","removal":"delete","type":"oem"},{"id":"com.lge.friendsmanager","description":"LG Friends Manager (https://play.google.com/store/apps/details?id=com.lge.friendsmanager)\nWTF ? Completely useless app.\nNot sure but I think it enables you to download an app for a friend LG user.\n","removal":"delete","type":"oem"},{"id":"com.lge.gallery.aodimagewidget","description":"Always-on display image\nLG Always-on display image","removal":"delete","type":"oem"},{"id":"com.lge.gallery.collagewallpaper","description":"LG Collage Wallpapers\nAllows you to create patchwork wallpaper from several photos.\nhttps://www.lg.com/uk/support/product-help/CT00008356-20150332136499-others\n","removal":"delete","type":"oem"},{"id":"com.lge.gallery.studio","description":"GalleryStudio\nIt's needed for gallery app probably.","removal":"replace","type":"oem"},{"id":"com.lge.gallery.vr.wallpaper","description":"LG 360 Image Wallpaper\nProvides VR (360°) wallpapers.","removal":"delete","type":"oem"},{"id":"com.lge.gamepad","description":"LG Game Pad\nIt's probably for TV. Not useful for games.","removal":"caution","type":"oem"},{"id":"com.lge.gametools","description":"GameTools\nNot tested it's for probably useful for games.","removal":"caution","type":"oem"},{"id":"com.lge.gametools.gamerecorder","description":"Screen recording\nScreen recording for game tools.","removal":"replace","type":"oem"},{"id":"com.lge.gametuner","description":"Settings/features for games, such as resolution and frame rate limiting.\nA little side note, any games installed in the work profile can't use gametuner (maybe if you install this package into the work profile it'll work)","removal":"replace","type":"oem"},{"id":"com.lge.gcuv","description":"GCUV\nNot 100% sure but @siraltus from XDA thinks it refers to \"Gauce Components\" which seems to be the LG's version of CSC \n(carrier sales code - automatic carrier-specific customization).\nIt gets run on first boot after factory reset, sets up the ROM features based on which carrier and country code is specified \nin the build.prop, and then gets frozen so it doesn't reconfigure things on subsequent boots.\nIt's basically the only person to mention \"Gauce components\" on the web (other than restricted LG webpages when using Google dorks).\nhttps://forum.xda-developers.com/tmobile-lg-v10/development/rom-lg-v10-h901-10c-debranded-debloated-t3277305/page12/page12\n","removal":"delete","type":"oem"},{"id":"com.lge.gdec.client","description":"Another component of OTA Updates but here I found it's for App Updates.","removal":"caution","type":"oem"},{"id":"com.lge.gestureanswering","description":"Answer me 2.0\nAllows you to bring the phone to your ear to answer an incoming call automatically.\nhttps://www.lg.com/us/mobile-phones/VS980/Userguide/109-1.html\n","removal":"delete","type":"oem"},{"id":"com.lge.gnss.airtest","description":"GNSS Air Test\nGNSS test, used to test... GNSS. Not needed, and GPNSS will continue to work.\nNOTE : GNSS = Global Navigation Satellite System and is the standard generic term for satellite navigation systems.\nThis term includes e.g. the GPS, GLONASS, Galileo, Beidou and other regional systems.\n","removal":"delete","type":"oem"},{"id":"com.lge.gnsslogcat","description":"GNSS Logcat\nUsed to dump GNSS logs. \n","removal":"delete","type":"oem"},{"id":"com.lge.gnsslogsetting","description":"GNSS LOG LEVEL SETTING. It's for debug.","removal":"delete","type":"oem"},{"id":"com.lge.gnsspostest","description":"GNSS Position test\nGNSS test again.\n","removal":"delete","type":"oem"},{"id":"com.lge.gnsstest","description":"GNSS Test\nWoh ! Why does LG need so many GNSS test packages?! \n","removal":"delete","type":"oem"},{"id":"com.lge.hifirecorder","description":"LG Audio Recorder\n","removal":"delete","type":"oem"},{"id":"com.lge.homeselector","description":"LG Home selector\nThis is the settings menu for the home launcher (present in the settings app as \"Home launcher\")\nIf you remove this app, the Home screen settings menu is gone from settings app. (not needed if you use external launcher)\n You can still switch between installed launchers, the package name is a bit misleading.\n","removal":"delete","type":"oem"},{"id":"com.lge.hotspotlauncher","description":"LG Mobile Hotspot\nProvides hotspot feature enabling you to share the phone’s 4G data connection with any Wi-Fi capable devices.\n","removal":"replace","type":"oem"},{"id":"com.lge.hotspotprovision","description":"Needed for hotspot 3G?","removal":"caution","type":"oem"},{"id":"com.lge.ia.task.incalagent","description":"InCalAgent\nRelated to interface while you're in a call. Seems also related to tasks list stuff.\nCan someone tell me what happens when you delete it ? I think it is safe.\n","removal":"delete","type":"oem"},{"id":"com.lge.ia.task.informant","description":"I only found that it's for feed favorite contacts and this app uses Korean language. Hidden, useless.","removal":"delete","type":"oem"},{"id":"com.lge.ia.task.smartcare","description":"LIA SmartDoctor Engine\nNeeded by SmartDoctor (com.lge.phonemanagement) ?\n","removal":"delete","type":"oem"},{"id":"com.lge.ia.task.smartsetting","description":"SmartSetting\nTurns on/off or changes features, settings and more according to where you are or what you do.\nhttps://www.lg.com/us/support/help-library/lg-android-smart-settings-CT10000025-20150103623722\n","removal":"delete","type":"oem"},{"id":"com.lge.icecontacts","description":"Emergency and video calling things.","removal":"replace","type":"oem"},{"id":"com.lge.iftttmanager","description":"LG Smart settings\nIFTTT = “if this, then that.”. Smart Settings can be seens as IFTTT.\nSome events automatically triggers actions.\n","removal":"delete","type":"oem"},{"id":"com.lge.ime","description":"LG Stock Keyboard\nDo not remove if you don't have an alternate keyboard available. Personally, I keep the stock keyboard just in case the keyboard app crash/fails (this happened to me once) locking me out of entering password.\n","removal":"caution","type":"oem"},{"id":"com.lge.ime.solution.handwriting","description":"Handwriting feature on the LG keyboard\n","removal":"delete","type":"oem"},{"id":"com.lge.ime.solution.text","description":"XT9 \nText predicting and correction for the LG keyboard.\nFor your culture (if you're young) : https://en.wikipedia.org/wiki/XT9\n","removal":"replace","warning":"On LG G6 (and maybe on other LG phones) removing this may cause the LG keyboard to stop inputing characters. Make sure to use another keyboard before removing this package.","type":"oem"},{"id":"com.lge.ims","description":"LG IMS\nIt's not a good app because it has a lot of debugging stuff.\nBreaks calls.","removal":"caution","type":"oem"},{"id":"com.lge.ims.chatbotinfoprovider","description":"It's a component of (com.lge.ims), not very useful because it's for debugging app.","removal":"delete","type":"oem"},{"id":"com.lge.ims.rcsprovider","description":"Needed for IMS, RCS but com.lge.ims = debugging app.","removal":"delete","type":"oem"},{"id":"com.lge.imsvt","description":"Video Call, another component of com.lge.ims = debugging app.","removal":"delete","type":"oem"},{"id":"com.lge.launcher2","description":"LG Home (v2)\nStock launcher\nIt's basically the home screen, the way icons apps are organized and displayed.\nDON'T REMOVE THIS IF YOU DIDN'T INSTALL ANOTHER LAUNCHER!\nNOTE : Yeah there is another package described as \"launcher\". Normally, you only have one of them on your phone. \n","removal":"replace","type":"oem"},{"id":"com.lge.launcher2.theme.optimus","description":"\"Optimus\" theme for the LG launcher (v2)\n","removal":"caution","type":"oem"},{"id":"com.lge.launcher3","description":"LG Home (v3)\nStock launcher\nIt's basically the home screen, the way icons apps are organized and displayed.\nDON'T REMOVE THIS IF YOU DIDN'T INSTALL ANOTHER LAUNCHER!\nNOTE : Yeah there is 3 packages described as \"launcher\". Normally, you only have one of them on your phone. \n","removal":"replace","type":"oem"},{"id":"com.lge.leccp","label":"LG Connectivity Service","description":"It has only Alert Dialogs.","removal":"delete","type":"oem"},{"id":"com.lge.lgaccount","description":"LG Account\nEnables you to create and manage your completely useless LG account.\n","removal":"delete","type":"oem"},{"id":"com.lge.lgbroadcastradioservice","description":"Broadcast radio? No one needs that.","removal":"delete","type":"oem"},{"id":"com.lge.lgcontentsetting","description":"Wallpaper & theme\nNeeded for theme settings.","removal":"replace","type":"oem"},{"id":"com.lge.lgdataphone","description":"It's for test and debug Data connection.","removal":"delete","type":"oem"},{"id":"com.lge.lgdmsclient","description":"Actual \"Software Update\" App client\nI think you won't receive updates without this.\n","removal":"caution","type":"oem"},{"id":"com.lge.lgdrm.permission","description":"Handle permissions for LG DRM (com.lge.drmservice).\nWhy does LG need a whole package for this?","removal":"replace","type":"oem"},{"id":"com.lge.lgfmservice","description":"Wifi Hotspot service\nREMINDER : Hotspot enable you to share the phone’s 4G data connection with any Wi-Fi capable devices.\nIt is not the Hotspot feature. Only the widget ! \n\nDual SIM status widget\nProbably only present in dual sim LG phone variants. Does not remove the persistent notification or dual SIM functionality.\n\nService menus. I believe if you remove the last one the secret code you can dial doesn't work anymore (who needs it anyway..?)\n\nLG support App remote access\nYou probably don't want that to happen\n\nLAOP test [MORE INFO NEEDED]\nI don't know what LAOP is. I could not find information about it. It's a test so it's probably fine. I have removed it.","removal":"replace","type":"oem"},{"id":"com.lge.lgfota.permission","description":"FOTA Test.","removal":"delete","type":"oem"},{"id":"com.lge.lginstallservies","description":"LG Install Service\nUsed by LG to install some of its apps on the phone. Not needed unless you use the LG apps manager.\n","removal":"delete","type":"oem"},{"id":"com.lge.lgmapui","description":"LGMapUI\nUser Interface (UI) for displaying location tracking reccord on the Health app (com.lge.lifetracker) ? \n","removal":"delete","type":"oem"},{"id":"com.lge.lgworld","description":"LG SmartWorld\nLG Store. Enables you to install LG apps, theme, keyboard layout, fonts...\n","removal":"delete","type":"oem"},{"id":"com.lge.lifetracker","description":"LG Health (https://play.google.com/store/apps/details?id=com.lge.lifetracker)\nAccording to users reviews, it is a very bad activity tracking app. \nPrivacy wise, you should never use this kind of thing obviously. \nhttps://www.lg.com/us/support/help-library/lg-android-lg-health-CT30013120-20150103629401\n","removal":"delete","type":"oem"},{"id":"com.lge.livemessage","description":"Draw chat\nNot needed for chats.","removal":"delete","type":"oem"},{"id":"com.lge.lms2","description":"Needed for LG account.","removal":"caution","type":"oem"},{"id":"com.lge.lockscreensettings","description":"LockScreenSettings\nLock Screen Settings.","removal":"unsafe","type":"oem"},{"id":"com.lge.mirrorlink","description":"MirrorLink\nEnables you to connect your phone to a car to provide audio streaming, GPS navigation...\nhttps://www.lg.com/ca_en/support/product-help/CT30014940-1440413573040-others\nhttps://mirrorlink.com/\n","removal":"delete","type":"oem"},{"id":"com.lge.mlt","description":"LG MLT\nRun in background all the time and probably serves purpose to help LG remote support. The thing is this acts as a good spyware. \nIt tries to track all your activity and logs GPS position together with the details gathered, and that includes calls, apps starting etc...\nAll data is collected and placed on /mpt partition, it seems not to be per reboot, but actually kept during flash and upgrades.\n#\nhttps://forum.xda-developers.com/showthread.php?t=2187920\n","removal":"delete","type":"oem"},{"id":"com.lge.mtalk.sf","description":"Voice Mate Speech Pack\nVoice Mate (now Q Vocie) is the LG Personal assistant (https://en.wikipedia.org/wiki/Voice_Mate)\nThis package provides speech pack. Is it the main Q-voice package ? I don't think so but I need confirmation.\n","removal":"delete","type":"oem"},{"id":"com.lge.music","description":"Stock music player\n","removal":"delete","type":"oem"},{"id":"com.lge.musiccontroller","description":"Music controller widget also Test activity.","removal":"delete","type":"oem"},{"id":"com.lge.musicpicker","description":"Audio\nIt has Drm Popup, Music Picker, Audio Preview.","removal":"delete","type":"oem"},{"id":"com.lge.musicshare","description":"LG Audio Share \nEnables you to connect two devices so that you can share the sound from music or video files with another LG devices.\nhttps://www.lg.com/hk_en/support/product-help/CT30007700-20150123957406-others\n","removal":"delete","type":"oem"},{"id":"com.lge.myplace","description":"My Place\nAnalyses the place you stay the most and recognises it as My Place (or your home) automatically.\nhttps://www.lg.com/uk/support/product-help/CT00008356-1433767701724-setting\n","removal":"delete","type":"oem"},{"id":"com.lge.myplace.engine","description":"My Places Engine\nNeeded by com.lge.myplace. See above.\n","removal":"delete","type":"oem"},{"id":"com.lge.networksettings","description":"Sim network settings\nIt can be found in settings.","removal":"unsafe","type":"oem"},{"id":"com.lge.nextcapture","description":"Needed for screenshots?","removal":"caution","type":"oem"},{"id":"com.lge.onehandcontroller","description":"Needed for One-Handed Controller, Miniview.","removal":"delete","type":"oem"},{"id":"com.lge.operator.hiddenmenu","description":"Operator HiddenMenu\nHidden tests.","removal":"delete","type":"oem"},{"id":"com.lge.penprime","description":"Pen settings\nNo one using them.","removal":"delete","type":"oem"},{"id":"com.lge.phonemanagement","description":"Smart Doctor App\nEnables you to shut down idle apps and delete temporary files.\nLets you also see phone battery, mobile data, apps, network status and usage patterns.\nOn the paper it seems good but in practise, Android handle 8+ handles very well idles apps. \nhttps://www.lg.com/ca_en/support/product-help/CT20098088-20150129256824-others\n","removal":"delete","type":"oem"},{"id":"com.lge.pickme","description":"PickMe\nNeeded for camera app or face unlock?","removal":"caution","type":"oem"},{"id":"com.lge.privacylock","description":"LG Content lock\nYou can lock the LG Gallery with a password or pattern. When connected to a PC, Content Lock prevents file previews.\nhttps://www.lg.com/us/support/help-library/lg-g4-content-lock-CT10000027-1432774759427\n","removal":"delete","type":"oem"},{"id":"com.lge.provider.lockscreensettings","description":"Needed for lock screen.","removal":"unsafe","type":"oem"},{"id":"com.lge.provider.signboard","description":"Needed for Always on display.\nNOTE: May crash Settings app after removal.","removal":"caution","type":"oem"},{"id":"com.lge.provider.systemui","description":"Not sure if iths important app.","removal":"caution","type":"oem"},{"id":"com.lge.qhelp","description":"Quick Help\nApp which provides you with support articles (FAQ section that walks you through most of the major features of the phone).\nYou can request support via email or request a call from LG.\nhttps://www.lg.com/us/support/help-library/lg-android-quick-help-CT10000026-20150103624836\n","removal":"delete","type":"oem"},{"id":"com.lge.qhelp.application","description":"Quick Help application\nI think this package is the real Quick Help app. The package above only provides help contents IMO.\n","removal":"delete","type":"oem"},{"id":"com.lge.qmemoplus","description":"LG QuickMemo+\nAllows you to capture screen shots and use them to create memos. You can also insert a reminder, location information, image, video, and audio.\nhttps://www.lg.com/us/support/help-library/lg-android-quickmemo-CT10000025-20150103629575\n","removal":"delete","type":"oem"},{"id":"com.lge.quicktools","description":"Quick Tools\nQuick Tools widget configure.","removal":"delete","type":"oem"},{"id":"com.lge.rcs.sharedsketch","description":"I have no idea what it is, maybe some drawing program related to rcs. I removed it.","removal":"replace","type":"oem"},{"id":"com.lge.remote.lgairdrive","description":"LG AirDrive\nLets you to control files in your device via a wireless connection. \nTo use it, you need to sign in to your LG account on both the PC and mobile device.\nhttps://www.lg.com/africa/support/product-help/CT20080025-1436354408798-others\n \nLG AirDrive settings\nSee package below.\n","removal":"delete","type":"oem"},{"id":"com.lge.remote.setting","description":"LG AirDrive\nLets you to control files in your device via a wireless connection. \nTo use it, you need to sign in to your LG account on both the PC and mobile device.\nhttps://www.lg.com/africa/support/product-help/CT20080025-1436354408798-others\n \nLG AirDrive settings\nSee package above.\n","removal":"delete","type":"oem"},{"id":"com.lge.screenplus.touchpad","description":"Screen+\nDesktop Touchpad.","removal":"delete","type":"oem"},{"id":"com.lge.screenplus.uiservice","description":"Screen+\nI don't know what Screen+ probably connects to PC screen.","removal":"delete","type":"oem"},{"id":"com.lge.sdencryption","description":"Encryption devices like SD card.","removal":"delete","type":"oem"},{"id":"com.lge.seamlesswallpaper.circular.reactive.darkspace","description":"Live wallpapers resources.","removal":"replace","type":"oem"},{"id":"com.lge.seamlesswallpaper.circular.reactive.space","description":"Live wallpapers resources.","removal":"replace","type":"oem"},{"id":"com.lge.secondlauncher","description":"It's needed for wallpaper choose and multi-app.","removal":"replace","type":"oem"},{"id":"com.lge.servicemenu","description":"Hidden testing hardware things.","removal":"delete","type":"oem"},{"id":"com.lge.signboard","description":"Always On Display.\nProbably a battery killer without an OLED screen.\nDisabling will remove the connected menu in the settings app.","removal":"replace","type":"oem"},{"id":"com.lge.sizechangable.weather","label":"LG Weather","description":"Not sure if it only manages Music widget for the launcher or also for the lockscreen.\nWeather widget for the home screen.","removal":"delete","type":"oem"},{"id":"com.lge.sizechangable.weather.platform","label":"LG Weather Service","description":"Provide weather data for the weather app/widget.","removal":"delete","type":"oem"},{"id":"com.lge.sizechangable.weather.theme.optimus","description":"\"Optimus\" theme for the weather app/widget.\n","removal":"caution","type":"oem"},{"id":"com.lge.smartdoctor.webview","label":"SmartDoctorWebview","description":"A WebView is acomponent that allows Android apps to display content from the web directly inside an application.","removal":"caution","warning":"Make sure to have another Webview before uninstalling it or some apps may not work properly and crash.","suggestions":"webviews","type":"oem"},{"id":"com.lge.smartenabler","description":"It's for OTA Updates.","removal":"caution","type":"oem"},{"id":"com.lge.smartshare","description":"SmartShare \nFeature that uses DLNA technology to stream multimedia contents between DLNA devices.\nDLNA is a non-profit trade organisation which defines standards that enable devices to share stuff with each other.\nBasically LG provides a way to stream multimedia contents from your phone to your smart TV (or via a DLNA plugin)\nhttps://www.lg.com/ca_en/support/product-help/CT31903570-1428542236040-file-media-transfer-pictures-music-etc\n","removal":"delete","type":"oem"},{"id":"com.lge.smartshare.provider","description":"Provider for Smart Share. \nNeeded by com.lge.smartshare.\nREMINDER : content providers help an application manage access to data stored by itself, stored by other apps, \nand provide a way to share data with other apps. They encapsulate the data, and provide mechanisms for defining data security\n","removal":"delete","type":"oem"},{"id":"com.lge.smartsharepush","description":"Smart Share Push\nObviously related to Smart Share but I don't know its exact purpose. \n","removal":"delete","type":"oem"},{"id":"com.lge.snappage","description":"Snap Page\nPart of the QuickMemo+ app, lets you capture the text/images/URL from a web page without grabbing ads.\nIt’s much like instapaper or pocket app, but it works locally, like reading mode on some browsers, saving only the body of the article. \nhttp://www.lg.com/us/mobile-phones/g4/display\n","removal":"delete","type":"oem"},{"id":"com.lge.springcleaning","description":"Smart cleaning\nDisplays the space in use and free space in your phone and allows you to selectively clean up your files.\nhttps://www.lg.com/us/mobile-phones/VS986/Userguide/339.html\n","removal":"delete","type":"oem"},{"id":"com.lge.sui.widget","description":"Removing this package causes the clock app to crash when trying to set an alarm for a specific date instead of a day of the week.","removal":"replace","type":"oem"},{"id":"com.lge.sync","label":"LG Bridge Service","description":"Used to backup, restore, update your LG phone, and transfer files wirelessly between computer and LG phone.\nYou will need to install LG Bridge software on your PC.\nNOTE: causes noticeable battery drain.","removal":"delete","type":"oem"},{"id":"com.lge.systemservice","description":"System Server\nNot sure if iths important app.","removal":"caution","type":"oem"},{"id":"com.lge.task","label":"LG Tasks","description":"Task storage for LG Calendar.","removal":"replace","warning":"Removing the package will cause LG Calendar to stop working.","type":"oem"},{"id":"com.lge.theme.black","description":"LG Black theme.\nSafe to remove, but also probably pointless to do so as most theme packages are just data containers.\nMake sure you don't delete the package for the theme you're currently using, I don't know what will happen then.","removal":"caution","type":"oem"},{"id":"com.lge.theme.highcontrast","description":"LG High Contrast theme.\nSafe to remove, but also probably pointless to do so as most theme packages are just data containers.\nMake sure you don't delete the package for the theme you're currently using, I don't know what will happen then.","removal":"replace","type":"oem"},{"id":"com.lge.theme.titan","description":"LG Titan theme (labelled 'Platinum' in the theming app).\nSafe to remove, but also probably pointless to do so as most theme packages are just data containers.\nMake sure you don't delete the package for the theme you're currently using. I don't know what will happen then.","removal":"caution","type":"oem"},{"id":"com.lge.theme.white","description":"LG White theme.\nSafe to remove, but also probably pointless to do so as most theme packages are just data containers.\nMake sure you don't delete the package for the theme you're currently using, I don't know what will happen then.","removal":"replace","type":"oem"},{"id":"com.lge.themeinstaller","description":"Needed to install themes.","removal":"caution","type":"oem"},{"id":"com.lge.themeservice","description":"Needed to apply themes.","removal":"caution","type":"oem"},{"id":"com.lge.touchcontrol","label":"Touch Control Areas","description":"I have never seen this menu in the settings app. I say it's safe to remove. I can't think of any use case for this setting, it just allows you to change where you're allowed to touch the screen","removal":"caution","type":"oem"},{"id":"com.lge.updatecenter","description":"LG Update Center\nProvide Android upgrade and LG updates (Settings --> System --> Update Center)\nI believe you won't receive any updates if this packages is deleted.","removal":"replace","type":"oem"},{"id":"com.lge.video.vr.wallpaper","description":"Video Wallpaper\nLG 360° VR Wallpapers\n","removal":"delete","type":"oem"},{"id":"com.lge.videoplayer","label":"LG Video","description":"LG Video Player","removal":"replace","suggestions":"video_players","type":"oem"},{"id":"com.lge.videostudio","label":"Quick Video Editor","description":"Allows you to create and edit video files using the videos (and photos) stored on the phone.","web":["https://www.lg.com/us/mobile-phones/VS980/Userguide/281.html"],"removal":"delete","type":"oem"},{"id":"com.lge.voicecare","description":"LG Voice care\nAllows you to use your device if the touch screen or display is damaged. \nYou must agree to location-based information use and personal information collection to use Voice Care. \nhttps://www.lg.com/hk_en/support/product-help/CT20136018-20150122834174-others\n","removal":"delete","type":"oem"},{"id":"com.lge.vrplayer","description":"LG VR player\nEnables you to watch 360° pictures/videos.\n","removal":"delete","type":"oem"},{"id":"com.lge.wallpaperpicker","description":"Wallpaper\nNeeded for wallpaper preview.","removal":"caution","type":"oem"},{"id":"com.lge.wapservice","description":"Icon looks like email configuration. I'd say it's safe to remove. Probably related to the stock email app.","removal":"replace","type":"oem"},{"id":"com.lge.wernicke","description":"QVoice Engine\nNeeded by Q-voice (the LG Q Voice voice assistant) to work.\n","removal":"delete","type":"oem"},{"id":"com.lge.wernicke.nlp","description":"Natural-language processing for LG intelligent assistant.\nUsed to understand what a human is saying when they speak.\nNeeded by QVoice\n","removal":"delete","type":"oem"},{"id":"com.lge.wfd.spmirroring.source","description":"Provide wifi-direct feature\nNote : Wifi-direct is Wi-Fi standard enabling devices to easily connect with each other without requiring a wireless access point.\nIt allows allows two devices to establish a direct Wi-Fi connection without requiring a wireless router\nhttps://en.wikipedia.org/wiki/Wi-Fi_Direct\nspmirroring = ??? screen p... mirroring ?\n","removal":"replace","type":"oem"},{"id":"com.lge.wfds.service.v3","description":"Wifi-direct service (v3)\nSee above.\n","removal":"replace","type":"oem"},{"id":"com.lge.wifi.p2p","description":"LG P2p Service \nWifi-drect P2P allows the device to discover the services of nearby devices directly, without being connected to a network.\nNeeded for LG Wifi-direct feature.\nhttps://developer.android.com/training/connect-devices-wirelessly/nsd-wifi-direct\n","removal":"delete","type":"oem"},{"id":"com.lge.wifisettings","description":"WiFi settings\nIt can be found in settings.","removal":"unsafe","type":"oem"},{"id":"com.lmi.motorola.rescuesecurity","description":"Rescue Security by LogMeIn (https://www.logmeinrescue.com/)\nRemote support app. Motorola made a partnership with LogMeIn : https://www.logmeinrescue.com/customer-stories/motorola\nIt enables motorola representatives to login and remotely control the device for technical support.\n","removal":"delete","type":"oem"},{"id":"com.longcheertel.AutoTest","label":"Autotest","description":"Hidden hardware tests.","removal":"delete","type":"oem"},{"id":"com.longcheertel.cit","label":"cit test","description":"Hidden hardware tests.","removal":"delete","type":"oem"},{"id":"com.longcheertel.midtest","label":"Assemble test","description":"Hidden hardware tests.","removal":"delete","type":"oem"},{"id":"com.longcheertel.modemlog","label":"Modem log","description":"Hidden app that includes TcardLog, modem log, Tcpdump.","removal":"delete","type":"oem"},{"id":"com.longcheertel.sarauth","label":"SarUI","description":"Hidden hardware tests.","removal":"delete","type":"oem"},{"id":"com.longcheertel.secretcode","description":"Refers to the system package that handles secret codes.\nSecret codes are special sequences of numbers and symbols that can be entered on a phone's keypad to access hidden features or information.","removal":"replace","type":"oem"},{"id":"com.mediatek.apmonitor","description":"APM Service\nhas only logs","removal":"delete","type":"oem"},{"id":"com.mediatek.mt6853.gamedriver","description":"Mediatek GPU Driver","removal":"unsafe","type":"oem"},{"id":"com.mediatek.mt6886.gamedriver","description":"Mediatek GPU Driver","removal":"unsafe","type":"oem"},{"id":"com.mediatek.ppl","description":"Mobile anti-theft\nIt seems that people don't even have access to it, its app to remote phone lock, wipe data, fetch back anti-theft PIN.","removal":"delete","type":"oem"},{"id":"com.meizu.account","description":"Flyme\nLog out before remove","removal":"delete","type":"oem"},{"id":"com.meizu.account.pay","description":"Pay Center chinese payment.","removal":"delete","type":"oem"},{"id":"com.meizu.activeviewlivewallpaper","description":"Needed for live wallpapers","removal":"replace","type":"oem"},{"id":"com.meizu.alphame","description":"for digital health work","removal":"delete","type":"oem"},{"id":"com.meizu.assistant","description":"Aicy Glance meizu assistant\nProbably Chinese assistant.","removal":"delete","type":"oem"},{"id":"com.meizu.battery","description":"Needed for Battery Manager.","removal":"caution","type":"oem"},{"id":"com.meizu.callsetting","description":"Additional call settings.","removal":"delete","type":"oem"},{"id":"com.meizu.cloud","description":"Useless push service","removal":"delete","type":"oem"},{"id":"com.meizu.customizecenter","description":"Needed for themes but it's so bloated.","removal":"replace","type":"oem"},{"id":"com.meizu.dataservice","description":"A lot of permissions and collects user data.","removal":"delete","type":"oem"},{"id":"com.meizu.ems","description":"Useless frameworks.","removal":"delete","type":"oem"},{"id":"com.meizu.facerecognition","description":"Needed for face recognition, face unlock lock screen.","removal":"delete","type":"oem"},{"id":"com.meizu.flyme.easylauncher","description":"Easy Mode\nit's for launcher in easy mode.","removal":"delete","type":"oem"},{"id":"com.meizu.flyme.launcher","description":"System launcher\nNeeded for launcher.","removal":"caution","type":"oem"},{"id":"com.meizu.flyme.sdkstage","description":"It's for Night Mode settings. Not sure if it's important.","removal":"caution","type":"oem"},{"id":"com.meizu.flyme.service.find","description":"Phone locating service\nThis app will probably help you find the device.","removal":"delete","type":"oem"},{"id":"com.meizu.flyme.update","description":"System updates\nProvides System updates.","removal":"caution","type":"oem"},{"id":"com.meizu.flymelab","description":"Flyme Lab\nit's something for video window or full screen.","removal":"delete","type":"oem"},{"id":"com.meizu.media.camera","description":"Camera\nStock Camera app meizu.","removal":"replace","type":"oem"},{"id":"com.meizu.media.gallery","description":"Gallery\nMeizu gallery app.","removal":"replace","type":"oem"},{"id":"com.meizu.media.imageservice","description":"PhotoService\nProbably needed for Photos app.","removal":"replace","type":"oem"},{"id":"com.meizu.media.music","description":"Music\nStock Music app.","removal":"delete","type":"oem"},{"id":"com.meizu.media.video","description":"Video Player\nStock Video Player app","removal":"delete","type":"oem"},{"id":"com.meizu.mstore","description":"App Store\nA lot of ads components.","removal":"delete","type":"oem"},{"id":"com.meizu.mzbasestationsafe","description":"Base Station Guard\nUseless location things.","removal":"delete","type":"oem"},{"id":"com.meizu.mznfcpay","description":"Meizu chinese payment.","removal":"delete","type":"oem"},{"id":"com.meizu.mzsyncservice","description":"Flyme cloud service","removal":"delete","type":"oem"},{"id":"com.meizu.net.nativelockscreen","description":"NativeLockScreen\nI guess it's needed for lockscreen.","removal":"unsafe","type":"oem"},{"id":"com.meizu.net.search","description":"Chinese Search","removal":"delete","type":"oem"},{"id":"com.meizu.picker","description":"Aicy Touch\nAnother chinese payment.","removal":"delete","type":"oem"},{"id":"com.meizu.powersave","description":"Power Saving Mode.","removal":"caution","type":"oem"},{"id":"com.meizu.pps","description":"One Mind\nDischarge schedule in the security annex.","removal":"delete","type":"oem"},{"id":"com.meizu.safe","description":"Looks like an important app. It has more things than security.","removal":"caution","type":"oem"},{"id":"com.meizu.setup","description":"It's needed only on first-boot setup.","removal":"delete","type":"oem"},{"id":"com.meizu.share","description":"It's maybe useful for Bluetooth","removal":"caution","type":"oem"},{"id":"com.meizu.suggestion","description":"Aicy Suggestion\nOnly suggestions.","removal":"delete","type":"oem"},{"id":"com.meizu.telephonyengineermode","description":"SIM things testing also logs.","removal":"delete","type":"oem"},{"id":"com.meizu.voiceassist","description":"Aicy Voice\nAnother smart thing","removal":"delete","type":"oem"},{"id":"com.meizu.wifiadmin","description":"WLAN Assistant\nAdditional WLAN things.","removal":"delete","type":"oem"},{"id":"com.mfashiongallery.emag","description":"Wallpapers by Xiaomi\n","removal":"delete","type":"oem"},{"id":"com.mi.AutoTest","description":"Assemble test\nHidden app used by the manufacturer to test various hardware components\n","removal":"delete","type":"oem"},{"id":"com.mi.android.globalFileexplorer","description":"Xiaomi Files Manager (https://play.google.com/store/apps/details?id=com.mi.android.globalFileexplorer)\n","removal":"replace","type":"oem"},{"id":"com.mi.android.globallauncher","description":"Poco Launcher\nSystem Launcher for POCO Phone, the way icons apps are organized and displayed.\nIf you remove this package you will loose navigation gestures and recent apps view.\nhttps://play.google.com/store/apps/details?id=com.mi.android.globallauncher","removal":"caution","type":"oem"},{"id":"com.mi.android.globalminusscreen","label":"App Vault","description":"Default App Vault package\nSends notifications and can't be uninstalled through the Apps tab. Can be downloaded back from Play Store if needed.","web":["https://play.google.com/store/apps/details?id=com.mi.android.globalminusscreen"],"removal":"delete","type":"oem"},{"id":"com.mi.android.globalpersonalassistant","description":"MI Vault aka the \"assistant\" you open swiping left from MI Home\n","removal":"delete","type":"oem"},{"id":"com.mi.emapal","description":"GamePal\nIn China language game mode tracing menu not available for users. Safe to remove.","removal":"delete","type":"oem"},{"id":"com.mi.global.bbs","description":"Mi Community (https://play.google.com/store/apps/details?id=com.mi.global.bbs)\nXiaomi Forum app\n","removal":"delete","type":"oem"},{"id":"com.mi.global.pocobbs","description":"Poco Community\nIt's only a forum and community app for POCO phones.","removal":"delete","type":"oem"},{"id":"com.mi.global.pocostore","description":"POCO Store\nIt's an official POCO online store.\nhttps://play.google.com/store/apps/details?id=com.mi.global.pocostore","removal":"delete","type":"oem"},{"id":"com.mi.global.shop","description":"Mi Store (https://play.google.com/store/apps/details?id=com.mi.global.shop)\nXiaomi app store\n","removal":"delete","type":"oem"},{"id":"com.mi.globalTrendNews","description":"Can't find info about this package\nProbably used for displaying (useless) news\n","removal":"delete","type":"oem"},{"id":"com.mi.globalbrowser","description":"Mi Browser\nPrivacy nightmare. You really should use something else.\nhttps://www.xda-developers.com/xiaomi-mi-web-browser-pro-mint-collecting-browsing-data-incognito-mode/\n\nNote: Since MIUI 12, you can no longer uninstall this app. Disabling it still works fine.","removal":"delete","type":"oem"},{"id":"com.mi.globallayout","label":"HomeLayout","description":"Sets the default MIUI launcher to global layout instead of Chinese (or HyperOS) when you first launch the app or when cleaning the launcher data.\nAfter removing this app there are several replacement widgets and no phone/message at the bottom.\nBecause Chinese MUI uses other apps and package names instead of just changing the default layout in the MIUI launcher code, they just threw the app in.","removal":"caution","type":"oem"},{"id":"com.mi.globalminusscreen","description":"Xiaomi App Vault\nhttps://play.google.com/store/apps/details?id=com.mi.android.globalminusscreen&gl=US","removal":"delete","type":"oem"},{"id":"com.mi.health","label":"Mi Fitness","description":"Formerly, Heart Rate and Mi Health, is a health monitor app that can be synchronized in the cloud.","removal":"replace","type":"oem"},{"id":"com.mi.healthglobal","description":"Mi Health. Xiaomi's health and fitness app. Tracks sleep, step count, BMI, and menstruation cycle. Includes heart rate monitoring support using the camera.\nhttps://www.xda-developers.com/mi-health-xiaomi-fitness-app/\nhttps://www.xda-developers.com/xiaomi-mi-health-app-gets-heart-rate-monitoring-support-using-camera/","removal":"replace","type":"oem"},{"id":"com.mi.liveassistant","description":"Mi Live Assistant\nI don't really know what it is. Maybe an old name for \"com.mi.android.globalpersonalassistant\"\n","removal":"delete","type":"oem"},{"id":"com.mi.setupwizardoverlay","description":"Weird package related to the SetupWizard (the menu which assists you to setup your phone for the first time)\nA user said he needed to remove this package to be able to properly apply a dark theme to the Settings app.\n","removal":"delete","type":"oem"},{"id":"com.mi.webkit.core","label":"MiWebView","description":"Discontinued. Xiaomi alternative to Google WebView\nIt is a system component for the Android operating system that allows Android apps to display content from the web directly inside an application. It's based on Chrome.","removal":"caution","warning":"Make sure to have another Webview before uninstalling it or some apps may not work properly and crash.","suggestions":"webviews","type":"oem"},{"id":"com.microfountain.rcs.service","description":"MicroFountain RCS Service\nChinese RCS Chats.","removal":"delete","type":"oem"},{"id":"com.microsoft.deviceintegrationservice","description":"Device Integration Service\nMight be needed for Microsoft Link to Windows (com.microsoft.appmanager) which is preinstalled with OxygenOS 14.\nAfter Uninstalling, no issues occurred.","removal":"delete","type":"oem"},{"id":"com.microsoftsdk.crossdeviceservicebroker","description":"Cross Device Broker\nMight be needed for Microsoft Link to Windows (com.microsoft.appmanager) which is preinstalled with OxygenOS 14.\nAfter Uninstalling, no issues occurred.","removal":"delete","type":"oem"},{"id":"com.mifavor.callsetting","description":"Additional call features and WiFi Calling.","removal":"replace","type":"oem"},{"id":"com.mig.play.games","label":"Funmax","description":"Some kind of third-party game center bundled by Xiaomi.\nHas lots of permissions by default.","removal":"delete","type":"oem"},{"id":"com.milink.service","label":"UniPlay Service","description":"MIUI screen casting service.\nIf removed, you'll have to use Android's native casting services which can be accessed through a 3rd party app.","removal":"caution","type":"oem"},{"id":"com.mipay.wallet","description":"Mi Pay (https://play.google.com/store/apps/details?id=com.mipay.in.wallet)\nContactless NFC-based mobile payment system that supports credit, debit and public transportation cards in China.\nhttps://www.mi-pay.com/\n#\n.in = Mi Pay for India\n.id = My Pay for Indonesia\n","removal":"delete","type":"oem"},{"id":"com.mipay.wallet.id","description":"Mi Pay (https://play.google.com/store/apps/details?id=com.mipay.in.wallet)\nContactless NFC-based mobile payment system that supports credit, debit and public transportation cards in China.\nhttps://www.mi-pay.com/\n#\n.in = Mi Pay for India\n.id = My Pay for Indonesia\n","removal":"delete","type":"oem"},{"id":"com.mipay.wallet.in","label":"Mi Pay","description":"Contactless NFC-based mobile payment system that supports credit, debit and public transportation cards in China.\nhttps://www.mi-pay.com/\n#\n.in = Mi Pay for India\n.id = My Pay for Indonesia","web":["https://play.google.com/store/apps/details?id=com.mipay.in.wallet"],"removal":"delete","type":"oem"},{"id":"com.mitv.download.service","description":"DownloadService\nUnknown app that has many mitv frameworks.\nBut safe to remove.","removal":"delete","type":"oem"},{"id":"com.mitv.milinkservice","description":"MiLink\nMiui screen casting service.","removal":"replace","type":"oem"},{"id":"com.mitv.tvhome.atv","description":"PatchWall\nIs it a spyware app? People recommend removing it.","removal":"delete","type":"oem"},{"id":"com.mitv.tvhome.michannel","description":"Mi Channel\nSame app like a PatchWall.","removal":"delete","type":"oem"},{"id":"com.mitv.videoplayer","description":"Mi TV Video Player\nIt's video player with google ads.","removal":"replace","type":"oem"},{"id":"com.miui.accessibility","label":"Mi Ditto","description":"Accesibility feature. Dictation (TTS) and speech output, \nmaking mobile devices more convenient for people who have difficulties using conventionally designed smartphones.","removal":"delete","type":"oem"},{"id":"com.miui.analytics","description":"Xiaomi Analytics\nThis app is shady. According to a guy who tried to reverse engineer the app, Xiaomi Analytics can replace any (signed?) package \nthey want silently on your device within 24 hours. Maybe that no longer the case now but... you don't want analytics anyway.\nSource : http://blog.thijsbroenink.com/2016/09/xiaomis-analytics-app-reverse-engineered/\n","removal":"delete","type":"oem"},{"id":"com.miui.android.fashiongallery","description":"Mi Wallpaper Carousel (https://play.google.com/store/apps/details?id=com.miui.android.fashiongallery)\nA lockscreen customization app. Displays a new photo every on your lock screen every time you turn ON your screen.\n","removal":"delete","type":"oem"},{"id":"com.miui.antispam","description":"MIUI Antispam \nspam phone numbers filter (blacklist).\nSuspicious analytics inside and has access to internet. Cloud backup possible.\nAt quick glance it is not a private antispam app.\nCan someone check what data are collected/transfered?\n","removal":"delete","type":"oem"},{"id":"com.miui.aod","description":"Always-on display and Lock screen editor\nSafe to remove if you don't use \"Always-on display\" and \"Lock screen editor\" features in settings.\nRequired for the fingerprint scanner if you want without double-tapping or pressing the power button.","removal":"replace","type":"oem"},{"id":"com.miui.audioeffect","description":"AudioEffect from Xiaomi (https://developer.android.com/reference/android/media/audiofx/AudioEffect)\nUsed by the equalizer (to be confirmed)\n","removal":"delete","type":"oem"},{"id":"com.miui.audiomonitor","label":"karaoke","description":"Voice Call Recorder (only in MIUI dialer app). Unused on Global, EEA MIUI","removal":"replace","suggestions":"call_recorders","type":"oem"},{"id":"com.miui.backup","label":"MIUI Backup","description":"Local Backup/Restore feature (Settings > Additional Settings > Local backups)\nIt seems this app can communicate with Mi Drop\nThis app has 73 permissions and can obviously do everything it wants.","removal":"delete","type":"oem"},{"id":"com.miui.bugreport","description":"Mi Feedback\nUsed to send bug report to devs\n","removal":"delete","type":"oem"},{"id":"com.miui.calculator","label":"Calculator","description":"MIUI Calculator\n","web":["https://play.google.com/store/apps/details?id=com.miui.calculator"],"removal":"replace","suggestions":"calculators","type":"oem"},{"id":"com.miui.carlink","label":"CarWith","description":"Not supported for most cars and only Chinese.","removal":"delete","type":"oem"},{"id":"com.miui.catcherpatch","description":"Needed for Application Extension Service (com.miui.contentcatcher).","removal":"delete","type":"oem"},{"id":"com.miui.cit","label":"CIT","description":"Hardware tests\nCan be launched via secret codes, lets you run hardware tests.","web":["https://web.archive.org/web/20220520051328/https://twitter.com/fs0c131y/status/933037531066785797","https://c.mi.com/thread-1744085-1-0.html"],"removal":"delete","type":"oem"},{"id":"com.miui.cleaner","label":"Cleaner","description":"Shady Xiaomi cleaner app developed by Cheetah mobile which has previously been caught in ad fraud and user data theft with this app in 2018 (previously called com.miui.cleanmaster and banned from the PlayStore). This \"new\" app is still full of trackers\n","web":["https://play.google.com/store/apps/details?id=com.miui.cleaner","https://www.gadgets360.com/apps/news/banned-security-app-clean-master-by-cheetah-mobile-collected-user-private-data-report-2189633","https://beta.pithus.org/report/f7f7ee425a8dc928db75105bd8f52e9b02f11dec3b398aac9fef1d42809d8ec1"],"removal":"replace","suggestions":"cleaners","type":"oem"},{"id":"com.miui.cleanmaster","label":"Mi Cleaner","description":"Shady Xiaomi cleaner app developed by Cheetah mobile which has previously been caught in ad fraud and user data theft in 2018. The app has been banned from the PlayStore and then reintroduced under the package name 'com.miui.cleaner'.\n","web":["https://www.gadgets360.com/apps/news/banned-security-app-clean-master-by-cheetah-mobile-collected-user-private-data-report-2189633"],"removal":"replace","suggestions":"cleaners","type":"oem"},{"id":"com.miui.cloudbackup","description":"Mi Cloud backup\nNeeded for Xiaomi cloud backup.\n","removal":"delete","type":"oem"},{"id":"com.miui.cloudservice","label":"Mi Cloud","description":"Dependency for synchronizing data with Xiaomi Cloud, including photos, contacts, messages, etc.\nThis feature is essential for Xiaomi phone users in China, as Xiaomi Cloud is their primary cloud storage service.","removal":"replace","type":"oem"},{"id":"com.miui.cloudservice.sysbase","description":"Another Mi Cloud dependency \n","removal":"delete","type":"oem"},{"id":"com.miui.compass","description":"Mi Compass\nI think you understand its purpose...\n","removal":"delete","type":"oem"},{"id":"com.miui.contentcatcher","label":"Application Extension Service","description":"It's a password manager in settings, requires Mi Account to Autofill and it syncs to your account.","removal":"delete","type":"oem"},{"id":"com.miui.contentextension","description":"Taplus\nIt's disabled on default in settings.\nIt allows you to analyze images and text by pressing and holding items on your screen to get contextual info.","removal":"delete","type":"oem"},{"id":"com.miui.core","label":"MIUI SDK","description":"It is obiously needed for MIUI to work correctly. FYI, it manages the MIUI Analytics service (`tracking.miui.com`), autoinstall MIUI apps updates (?). Related to GetApps 'com.xiaomi.market' China Mi App Store), DropboxManager where is temp clean code, a lot of logs. Found things related to Quick Apps 'com.miui.hybrid', DataUpdateManager (related to micloud?). Telocation update, app looks like tracking everything. Can be disabled and uninstalled with MIUI 13.0.6, MIUI 14\nNOTE: uninstalling this package causes the Settings app to crash when searching in Settings, so better disable this app instead.\nIf you want to test, disable this app and open an issue if anything isn't working. Another weird thing is normal user can disable this app using hidden Settings to manage app by in Google Play.\nNo effects after disable, but not sure if its required for Miui updates or gestures.","web":["https://github.com/0x192/universal-android-debloater/issues/632"],"removal":"caution","warning":"Removing this package causes the Settings app to crash when searching in Settings. May also cause bootloops in MIUI < 13.0.6","type":"oem"},{"id":"com.miui.core.internal.assistant","description":"Needed for Chinese Mi AI (com.miui.voiceassist).","removal":"delete","type":"oem"},{"id":"com.miui.core.internal.editor.services","description":"I only found: config_enableHapticTextHandle true\nWhat is this mean? it's used to permission MIUIOP 10008 in some apps.\nMIUIOP = miui optimization? it's useless or important?\nNo effects after removing, it's probably a feature of Touch Assistant, but its used on 'com.miui.core'?","removal":"caution","type":"oem"},{"id":"com.miui.core.internal.services","description":"I only found: array name= config_deviceSpecificSystemServices\nitem: com.miui.me.server.auto_install.InstallService\nWhat does this mean? I have to check com.miui.core what it does and it's for autoinstall miui apps updates from GetApps 'com.xiaomi.market'.","removal":"caution","type":"oem"},{"id":"com.miui.daemon","label":"System Daemon","description":"Previously, MiuiDaemon.\nCollects a lot of data and sends them to China.","web":["https://web.archive.org/web/20210923050136/https://twitter.com/fs0c131y/status/938872347087564800"],"removal":"delete","type":"oem"},{"id":"com.miui.easygo","description":"For me, this app means nothing.\nA lot of useless code that you can't use.","removal":"delete","type":"oem"},{"id":"com.miui.enbbs","description":"Xiaomi Forums old package.\nNow com.mi.global.bbs.\n","removal":"delete","type":"oem"},{"id":"com.miui.extraphoto","description":"Bokeh\nThe document mode of the Xiaomi camera (for taking IDs), deleting it doesn't affect the editing of albums.","removal":"delete","type":"oem"},{"id":"com.miui.face","description":"MIUI Biometric\nFace Unlock feature\n","removal":"replace","type":"oem"},{"id":"com.miui.face.overlay.miui","description":"MiuiBiometricResOverlay\nFace Unlock feature will be unavailable.","dependencies":["com.miui.face"],"removal":"replace","type":"oem"},{"id":"com.miui.fm","description":"MIUI FM Radio app\n","removal":"replace","suggestions":"radios","type":"oem"},{"id":"com.miui.fmservice","description":"FM Radio Service\nNeeded by com.miui.fm to work correctly\n","removal":"delete","type":"oem"},{"id":"com.miui.freeform","label":"Floating windows","description":"You can make apps appear above other applications.","web":["https://forum.xda-developers.com/android/miui/floating-windows-miui-12-t4125661"],"removal":"caution","warning":"Removing the package may cause the floating windows to stop working on some devices.","type":"oem"},{"id":"com.miui.gallery","label":"Xiaomi Gallery","description":"Previously, MIUI Gallery.\nIt has several trackers and sometimes connects to Mi Cloud, even if an account is not added.","removal":"replace","warning":"Removing will break the send screenshot feature (swipe 3 fingers to show the screenshot preview)","suggestions":"gallery","type":"oem"},{"id":"com.miui.global.packageinstaller","label":"Xiaomi Package Installer","description":"The non-AOSP package installer present in the Xioami phones. It also checks the installed app for viruses.\nThe AOSP package installer is also present in Xioami EU ROMs.","removal":"unsafe","warning":"Removal causes bootloop on Xiaomi EU ROMs.","type":"oem"},{"id":"com.miui.greenguard","label":"Security Guard Service","description":"The app includes three different antivirus brands built in that the user can choose from to keep their phone protected: Avast, AVL, and Tencent. \nUpon selecting the app, the user selects one of these providers as the default Anti-Virus engine to scan the device.\nIt the app that scans an app before installing it\nNOTE: A vulnerability was found in 2019","web":["https://research.checkpoint.com/2019/vulnerability-in-xiaomi-pre-installed-security-app/"],"removal":"delete","type":"oem"},{"id":"com.miui.guardprovider","label":"System security components","description":"Guard Provider security app\nThe app includes 3 different antivirus brands built in that the user can choose (Avast, AVL and Tencent). \nThis app notably performs a virus scan of any apps you want to install. \nA serious vulnerability was found in 2019.\nYou may want to remove this app from a privacy stance.","web":["https://research.checkpoint.com/2019/vulnerability-in-xiaomi-pre-installed-security-app/","https://beta.pithus.org/report/797a7e405bc8e767deebbbcab3e06a19b05156de44292c918b582dff3078d7b8"],"removal":"caution","warning":"Removing this package will very likely break any app installation/update.\nBefore removing, disable security things in MIUI security app.\nOn HyperOS this module is responsible for showing the 'Install via USB' prompt when installing apps via ADB (enabled through developer options, requires Mi account the first time).","type":"oem"},{"id":"com.miui.home","label":"Xioami System Launcher","description":"Formerly, MIUI System Launcher\nIt's basically the home screen, the way icons and apps are organized and displayed.","web":["https://web.archive.org/web/20220926221620/https://libreddit.spike.codes/r/Xiaomi/comments/o6vk5z/miui_12125_and_android_11_gestures/"],"removal":"unsafe","warning":"If you remove this package on devices based on MIUI 12+ with Android 11+, you will loose navigation gestures and recent apps view even with a 3rd party launcher.\nMake sure you've installed another launcher before you disable","suggestions":"launchers","type":"oem"},{"id":"com.miui.huanji","description":"Lets you transfer your contacts, messages, personal files, all the installed apps (but not it's data). Also all the settings (app + system) from an Android phone to a Xiaomi phone.\nThe two phones will establish a direct Wi-Fi connection.","web":["https://play.google.com/store/apps/details?id=com.miui.huanji"],"removal":"delete","type":"oem"},{"id":"com.miui.hybrid","description":"Quick Apps\nIt's basically an app which shows you ads and tracks you...\nFunny thing, Xiaomi's Quick Apps was reportedly being blocked by Google Play Protect.\nhttps://www.androidpolice.com/2019/11/19/xiaomi-quick-apps-flagged-blocked-google-play-protect/\n#\nReverse engineering of the app : \nhttps://medium.com/@gags.gk/reverse-engineering-quick-apps-from-xiaomi-a1c9131ae0b7\nSpoiler : you really should delete this package.\n","removal":"delete","type":"oem"},{"id":"com.miui.hybrid.accessory","description":"Xiaomi Hybrid Accessory\nSmartphone accessories support for Quick Apps (com.miui.hybrid)\n","removal":"delete","type":"oem"},{"id":"com.miui.klo.bugreport","description":"KLO Bugreport\nThis app registers system failures and Android applications errors and sends bugs to Xiaomi servers.\n","removal":"delete","type":"oem"},{"id":"com.miui.maintenancemode","description":"Maintenance Mode\nThe maintenance mode of the cell phone, the cell phone maintenance time into an empty user data system mode, to ensure the safety of cell phone data.\nAlso has child mode. Not useful if you are not in China.","removal":"delete","type":"oem"},{"id":"com.miui.mediaeditor","description":"Xiaomi Gallery Editor\nExtension for MIUI Gallery that's used to edit photos and videos.","removal":"replace","type":"oem"},{"id":"com.miui.mediafeature","description":"MediaFeature合集\nIs this something for media? Unused app, probably for China.","removal":"delete","type":"oem"},{"id":"com.miui.mediaviewer","description":"Media viewer\nOld? Mi Video.","removal":"replace","type":"oem"},{"id":"com.miui.metoknlp","description":"Network location provider\nUseless, only for China, have analytics things.","removal":"delete","type":"oem"},{"id":"com.miui.micloudsync","label":"MiCloudSync","description":"Dependency for synchronizing data with Xiaomi Cloud, including photos, contacts, messages, etc.\nThis feature is essential for Xiaomi phone users in China, as Xiaomi Cloud is their primary cloud storage service.","removal":"delete","type":"oem"},{"id":"com.miui.miinput","description":"Removing this package breaks \"Gesture shortcuts\" under \"Additional settings\".","removal":"replace","type":"oem"},{"id":"com.miui.miservice","label":"Services & feedback","description":"Used to send feedbacks (and data) to Xiaomi. Integration in Wechat\nSeems to be able to launch 'Baidu location service'\nHas too many permissions, runs in the background all the time and can be removed without issue.","removal":"delete","type":"oem"},{"id":"com.miui.mishare.connectivity","description":"Mi Share\nUnified file sharing service between Xiaomi, Oppo, Realme and Vivo devices using Wifi-direct\nSettings -> Connection & sharing -> Mi Share\nFYI : Wifi direct allows 2 devices to establish a direct Wi-Fi connection without requiring a wireless router.\n","removal":"replace","type":"oem"},{"id":"com.miui.misound","label":"Earphones","description":"Provides the sounds section in Settings and is needed for the equalizing\nSome people removed this package but I personaly don't think it's worth it. This package isn't really an issue\n(no dangerous permissions and does not run in the background all the time)\nYou can still remove it. You'll be just fine if you really don't need it.","removal":"delete","type":"oem"},{"id":"com.miui.miwallpaper","description":"Mi Wallpaper \nRemoving this might make it impossible to set a lock or home wallpaper, resulting in a black solid wallpaper.\nNote: it may also result in longer boot times (~15s) because the system try to call miwallpaper during boot","removal":"caution","type":"oem"},{"id":"com.miui.miwallpaper.earth","label":"Super wallpapers - Earth","description":"Live/animated Xiaomi wallaper","removal":"delete","type":"oem"},{"id":"com.miui.miwallpaper.geometry","label":"SuperWallpaperGeometry","description":"Live/animated Xiaomi wallaper","removal":"delete","type":"oem"},{"id":"com.miui.miwallpaper.mars","label":"SuperWallpaperMars","description":"Live/animated Xiaomi wallaper","removal":"delete","type":"oem"},{"id":"com.miui.miwallpaper.moon","label":"The Moon Super wallpapers","description":"Live/animated Xiaomi wallaper","removal":"delete","type":"oem"},{"id":"com.miui.miwallpaper.saturn","label":"SuperWallpaperSaturn","description":"Live/animated Xiaomi wallaper","removal":"delete","type":"oem"},{"id":"com.miui.miwallpaper.snowmountain","label":"SuperWallpaperSnowmountain","description":"Live/animated Xiaomi wallaper","removal":"delete","type":"oem"},{"id":"com.miui.miwallpaper.overlay","description":"App that doesn't do anything, no code. Safe to remove.\n You will need to remove it twice.","removal":"delete","type":"oem"},{"id":"com.miui.miwallpaper.overlay.customize","description":"App that doesn't do anything, no code. Safe to remove.","removal":"delete","type":"oem"},{"id":"com.miui.miwallpaper.overlay.lundun","description":"app that doesnt do anything, no code. Safe to remove.\n You will need remove it 2 times.","removal":"delete","type":"oem"},{"id":"com.miui.miwallpaper.overlay.qr","description":"Useless app to default lock wallpaper path? Probably unused. Safe to remove.","removal":"delete","type":"oem"},{"id":"com.miui.miwallpaper.telcel.overlay","description":"Useless app to telcel wallpaper? Probably unused. Safe to remove.","removal":"delete","type":"oem"},{"id":"com.miui.msa.global","description":"Main System Ads\nAnalyzation of user behaviors to show you ads. Yeah Xiaomi phones has ads...\nhttps://www.theverge.com/2018/9/19/17877970/xiaomi-ads-settings-menu-android-phones\n","removal":"delete","type":"oem"},{"id":"com.miui.newhome","description":"Content Service\nA lot bloated.\nOnly useful in China.","removal":"delete","type":"oem"},{"id":"com.miui.newmidrive","description":"Mi Drive (Chinese version)\nLets you upload and sync your files on the (Mi) Cloud.\nAlways run in background\n","removal":"delete","type":"oem"},{"id":"com.miui.nextpay","label":"Smart cards Web Extention","description":"Only for Chinese users.","removal":"delete","type":"oem"},{"id":"com.miui.notes","label":"MIUI Notes","description":"MIUI's note-taking app.","removal":"replace","suggestions":"note_taking_apps","type":"oem"},{"id":"com.miui.notification","label":"Notifications","description":"Notifications are working without this app.\nIt is possible to access the app notification settings by long pressing on the notification without the app.\nIt embeds a tracking statistics service\n(usage tracking : `id`,`pkgName`,`latestSentTime`,`sentCount`,`avgSentDaily`,`avgSentWeekly)","removal":"caution","warning":"notification settings in the settings menu will be broken without this package. The app is mandatory to enable notifications of apps that have been disabled before.","type":"oem"},{"id":"com.miui.otaprovision","description":"OtaProvision\nUseless, only for China, have analytics things.","removal":"delete","type":"oem"},{"id":"com.miui.packageinstaller","description":"Package installer\nHardcoded in Xiaomi China Rom.\nCauses BOOTLOOP on Chinese ROM When remove.\nIt's weird when after a month this app is gone from my phone for no reason and Android enabled stock package installer.","removal":"replace","type":"oem"},{"id":"com.miui.personalassistant","description":"Seems to be App Vault on some phones (https://play.google.com/store/apps/details?id=com.mi.android.globalpersonalassistant)\nhttps://c.mi.com/thread-1017547-1-0.html\n","removal":"delete","type":"oem"},{"id":"com.miui.phone.carriers.customized.overlay","description":"Something about WiFi calling \"vowifi\", \"volte\", \"notification on keyguard\"\nIt's unused.","removal":"delete","type":"oem"},{"id":"com.miui.phone.carriers.overlay","description":"Preferred network type to Vodafone 5G/4G/3G/2G auto.\nIt's unused.","removal":"delete","type":"oem"},{"id":"com.miui.phone.carriers.overlay.h3g","description":"Preferred network type to h3g 5G/4G/3G/2G auto.\nIt's unused.","removal":"delete","type":"oem"},{"id":"com.miui.phone.carriers.overlay.vodafone","description":"Preferred network type to Vodafone 5G/4G/3G/2G auto.\nIt's unused.","removal":"delete","type":"oem"},{"id":"com.miui.phrase","label":"Frequent phrases","description":"MIUI context menu tool for pre-made text responses while chatting. It has access to the Internet, is linked to MiCloud and contains a weird CloudTelephonyManager class in its code.","removal":"caution","warning":"Disabling causes a crash when touching the \"Frequent phrases\" item. (The crash occurs in the application that invoked the context menu with this item)","type":"oem"},{"id":"com.miui.player","label":"Mi Music","description":"Mi Music player\n","web":["https://play.google.com/store/apps/details?id=com.miui.player"],"removal":"replace","suggestions":"music_apps","type":"oem"},{"id":"com.miui.powerkeeper","description":"Battery and Performance\n(aggressive) MIUI power management (https://dontkillmyapp.com/xiaomi)\nThat's a weird app that also contains a DRM Manager and a service related to Cloud Backup\nHas obviously a lot of dangerous permissions.\nI guess removing this package will decrease the battery performance. Is it that noticeable? Can someone try?\nNOTE: REMOVING THIS PACKAGE CAUSES A BOOTLOOP ON THE REDMI PAD.\nTo not get bootloop, log out from Mi Account.","removal":"caution","type":"oem"},{"id":"com.miui.privacycomputing","description":"MIUI Privacy Components\nUnknown app from Miui China.\nThere's something about keys, key status code.","removal":"delete","type":"oem"},{"id":"com.miui.providers.weather","description":"Provider for MI Weather app (com.miui.weather)\nContent providers encapsulate data, providing centralized management of data shared between apps.\nhttps://developer.android.com/guide/topics/providers/content-providers.html","removal":"replace","type":"oem"},{"id":"com.miui.qr","description":"MUI Qr code scanner\n","removal":"replace","suggestions":"barcode_scanners","type":"oem"},{"id":"com.miui.rom","description":"Core package of MIUI\nDO NOT REMOVE THIS\n","removal":"unsafe","type":"oem"},{"id":"com.miui.screenrecorder","description":"Mi Screen Recorder\n","removal":"replace","suggestions":"screen_recorders","type":"oem"},{"id":"com.miui.screenshot","description":"MIUI Screenshot\nScreenshots will not work.","removal":"caution","type":"oem"},{"id":"com.miui.securityadd","label":"Xiaomi System Service Plugin","description":"Related to the MIUI Security app","web":["https://github.com/0x192/universal-android-debloater/issues/641"],"removal":"caution","warning":"Removing the app may cause bootloops in MIUI version below 13.","type":"oem"},{"id":"com.miui.securitycenter","label":"Xiaomi Security","description":"Provides \"protection and optimization tools\": App lock, Data usage, Security scan, Cleaner, Battery saver, Blocklist and other features. It is mostly a front-end (UI).","web":["https://beta.pithus.org/report/f8c24ccfc526389ff9084505c60fba3d3463565f92e2015190e2974b370e7c4e","https://github.com/0x192/universal-android-debloater/issues/641"],"removal":"caution","warning":"Removing the app may cause bootloops. It does not cause bootloops in Redmi Pad or MIUI 13 onwards, but you will lose some functionality like the battery status/usage page, as well as the app usage/removal page.","type":"oem"},{"id":"com.miui.securitycenter.securitycenter_phone_overlay.config.overlay","description":"'Security tools' name app only found","removal":"delete","type":"oem"},{"id":"com.miui.securitycore","label":"Security Core Component","description":"Core features of the \"com.miui.securitycenter\".\nProvides Enterprise Mode, Dual App, Second Space, Fingerprint Add, Gesture Settings. It also shows annoying notifications when launching a new app.","web":["https://github.com/0x192/universal-android-debloater/issues/641"],"removal":"caution","warning":"Removing the app may cause bootloops. On Miui 13 and above, it doesn't cause bootloops, and the performance appears very similar.","type":"oem"},{"id":"com.miui.securityinputmethod","label":"Mi Secure Keyboard","description":"A useless keyboard used to secure your password when logging in.","removal":"replace","suggestions":"keyboards","type":"oem"},{"id":"com.miui.settings.rro.device.hide.statusbar.overlay","description":".webp files, and one config for me it means nothing. Only Chinese.","removal":"delete","type":"oem"},{"id":"com.miui.settings.rro.device.type.overlay","description":"I found only PNG files and it's Chinese. Only Chinese.","removal":"delete","type":"oem"},{"id":"com.miui.smsextra","label":"com.miui.smsextra","description":"Dependency for MIUI Messaging (MIUI SMS app misleadingly called `com.android.mms`)\nYou can remove it if you don't use the default SMS app (and you shouldn't). Run in the background once the phone is booted, has access to the internet and interact with Cloud Manager.","removal":"delete","type":"oem"},{"id":"com.miui.spock","label":"Spock","description":"Analytics app which constantly runs in the background.\nSends identifiable data to Xiaomi servers.\nIt leaks system version, device model, exact firmware build + some few mysterious IDs.","web":["https://www.virustotal.com/gui/file/70400d0055e1924966fb8367cafddc175dee914bbdc227342c9dd86fb3aa829f/details"],"removal":"delete","type":"oem"},{"id":"com.miui.sysopt","description":"SysoptApplication\nStrange app with no permissions. By looking at the code it seems to be some kind of debug app.\nThe app doesn't seem to do any interesting stuff.\n","removal":"delete","type":"oem"},{"id":"com.miui.system","description":"Called 'MIUI System Launcher' but it's not the launcher itself (com.miui.home is)\nThis package is another core MIUI app you can't remove. It centralizes a lot of default configuration values\n","removal":"unsafe","type":"oem"},{"id":"com.miui.system.overlay","description":"App without code and safe to remove.","removal":"delete","type":"oem"},{"id":"com.miui.systemAdSolution","description":"Spyware which analyses user behavior for targeted ads. Yeah Xiaomi phones has ads...\nhttps://www.theverge.com/2018/9/19/17877970/xiaomi-ads-settings-menu-android-phones\n","removal":"delete","type":"oem"},{"id":"com.miui.systemui.carriers.overlay","description":"Important overlay to LTE connection.","removal":"unsafe","warning":"Removing the package breaks LTE connection.","type":"oem"},{"id":"com.miui.systemui.devices.overlay","description":"The empty space between the status bar and the edges of the screen\nElements at edges ignore screen fillets and cutouts when removed.","removal":"replace","type":"oem"},{"id":"com.miui.systemui.overlay.devices.android","description":"Config doze Component 'com.miui.aod' and ext media ready notification 'Tap to safely remove device'.","removal":"caution","type":"oem"},{"id":"com.miui.thirdappassistant","description":"Third party app problems\nIt's boring app.","removal":"delete","type":"oem"},{"id":"com.miui.touchassistant","description":"Quick Ball/Touch Assistant\nTouch assistant with a combination of five unique shortcuts which aimed to give easy and quick access to functions and apps you use frequently.\n","removal":"delete","type":"oem"},{"id":"com.miui.translation.kingsoft","description":"Translation stuff by Kingsoft (https://en.wikipedia.org/wiki/Kingsoft)\n","removal":"delete","type":"oem"},{"id":"com.miui.translation.xmcloud","label":"com.miui.translation.xmcloud","description":"Translation stuff. Does not impact global translation for non-chinese users.","removal":"delete","type":"oem"},{"id":"com.miui.translation.youdao","description":"Translation stufff by Youdao (https://en.wikipedia.org/wiki/Youdao)\n","removal":"delete","type":"oem"},{"id":"com.miui.translationservice","label":"com.miui.translationservice","description":"Translation stuff. Does not impact global translation for non-chinese users.","removal":"delete","type":"oem"},{"id":"com.miui.tsmclient","description":"Smart cards\nOnly for Chinese.","removal":"delete","type":"oem"},{"id":"com.miui.tv.analytics","description":"Analytics\nWeird analytics app with a lot random stuff found in resources.","removal":"delete","type":"oem"},{"id":"com.miui.uireporter","description":"UIReporter\nThis Chinese app has some secret code: 847, 1130.","removal":"delete","type":"oem"},{"id":"com.miui.userguide","description":"Xiaomi User guide\n","removal":"delete","type":"oem"},{"id":"com.miui.video","label":"Mi Video","description":"Mi Video with a different package name.\nHas a lot of ads, tracking.","removal":"replace","suggestions":"video_players","type":"oem"},{"id":"com.miui.videoplayer","label":"Mi Video","description":"Has a lot of ads, tracking.","web":["https://play.google.com/store/apps/details?id=com.miui.videoplayer"],"removal":"replace","suggestions":"video_players","type":"oem"},{"id":"com.miui.videoplayer.overlay","description":"Mi Video overlay\nOverlays are usually themes.","removal":"delete","type":"oem"},{"id":"com.miui.vipservice","description":"My services\nCustomer support maybe not be available for users.","removal":"delete","type":"oem"},{"id":"com.miui.virtualsim","description":"Mi Roaming\nIt enables users to connect to roaming data on-demand via virtual SIM technology.\nhttps://alertify.eu/xiaomi-mi-roaming/\n","removal":"delete","type":"oem"},{"id":"com.miui.voiceassist","label":"Mi AI","description":"Chinese voice assist.","removal":"delete","type":"oem"},{"id":"com.miui.voiceassistoverlay","description":"Overlay to Mi AI 'com.miui.voiceassist'.\nThe overlay won't show up when you trigger it, which makes the voice-to-command features largely inaccessible.","removal":"delete","type":"oem"},{"id":"com.miui.voicetrigger","label":"Wake with voice","description":"Not needed if you removed Chinese Mi AI.","removal":"delete","type":"oem"},{"id":"com.miui.vpnsdkmanager","description":"MiuiVpnSdkManager\nVpn to game service?","removal":"delete","type":"oem"},{"id":"com.miui.vsimcore","description":"Virtual Sim core service\n","removal":"delete","type":"oem"},{"id":"com.miui.wallpaper.overlay","description":"App that doesnt do anything, no code. Safe to remove.\n You will need remove it 2 times.","removal":"delete","type":"oem"},{"id":"com.miui.wallpaper.overlay.customize","description":"App that doesnt do anything, no code. Safe to remove.","removal":"delete","type":"oem"},{"id":"com.miui.weather2","label":"Weather","description":"Weather app By Xiaomi.","web":["https://play.google.com/store/apps/details?id=com.miui.weather2"],"removal":"replace","suggestions":"weather_apps","type":"oem"},{"id":"com.miui.wmsvc","label":"WMService","description":"Runs at boot and has access to internet + GPS\nI quickly looked at the decompiled code and saw some unsanitized SQL inputs, which is BAD! (vulnerable to SQL injection)\nTries to get your android unique Google advertising ID from Google Play Services.\nFeeds and launches the spying/analytics app \"com.miui.hybrid\".\nDoesn't seem to do anything important, only tracking.","removal":"delete","type":"oem"},{"id":"com.miui.yellowpage","description":"Yellow Page from MIUI.\nREMINDER : Yellow pages contain phone numbers of companies and services. They are provided by Xiaomi partners or businesses themselves.\n","removal":"delete","type":"oem"},{"id":"com.miui.zman","description":"Mi Secure sharing\nProvides an option in the settings of the Xiaomi Gallery to automatically remove location and metadata from images \nyou want to share. This do not remove metadata of the picture in the gallery but only the shared copy.\nThere's also a \"Secure sharing\" watermark that shows up when you share photos on WeChat without metadata.\nThe question is does this really remove all EXIF tags? Can someone test?\nThis is a useful app anyway but do not forget that all your photos/vidoes taken with the Xiaomi camera are still geo-tagged \n(+ all others exif tags) by default. \nWhat you can do is at least revoke the GPS permission to the camera.\nFOSS alternative to this app : \nhttps://f-droid.org/fr/packages/com.jarsilio.android.scrambledeggsif/\nhttps://f-droid.org/fr/packages/de.kaffeemitkoffein.imagepipe/\n","removal":"replace","type":"oem"},{"id":"com.miuix.editor","label":"textaction","description":"This application is responsible for displaying the text action toolbar in MIUI. Some applications, such as Telegram, use a custom text action toolbar, but most applications this standard toolbar.","removal":"delete","warning":"If removed, it will fall back to the Android's standard text action toolbar.","type":"oem"},{"id":"com.mmigroup.fmradio","description":"App for Hardware Testing Things.","removal":"delete","type":"oem"},{"id":"com.mobeam.barcodeService","description":"The Beaming Service enables your device to beam (relay) barcodes, as found on digital coupons, event tickets, library cards, loyalty \ncards and membership cards to 1D red laser and Image based scanners prevalent at nearly every retail store and checkout stand around the world.\nMobeam is a 3-party (https://mobeam.com/)\n","removal":"delete","type":"oem"},{"id":"com.mobile.iroaming","label":"Data Store","description":"Only useful if you need roaming mobile data when travelling overseas. Has a lot of dangerous permissions and phone home to Vivo domains.","web":["https://beta.pithus.org/report/d7cfa53942159a0e9c1bf3643b5f38496daee4c0225e8155249db9fdc979187c"],"removal":"caution","type":"oem"},{"id":"com.mobiletools.systemhelper","label":"SystemHelper","description":"Not available for users, has something about dual sim, App Info.","removal":"delete","type":"oem"},{"id":"com.modemdebug","description":"Hidden running in the background debugs for data traffic and all that, not useful for the average person.","removal":"delete","type":"oem"},{"id":"com.monotype.android.font.felbridge","description":"Felbridge FlipFont\nChanges the user interface font on your phone.","removal":"delete","type":"oem"},{"id":"com.monotype.android.font.manroperegular","description":"ManropeRegular FlipFont\nChanges the user interface font on your phone.","removal":"delete","type":"oem"},{"id":"com.monotype.android.font.mfinancehkbold","description":"MfinancehkBold FlipFont\nChanges the user interface font on your phone.","removal":"delete","type":"oem"},{"id":"com.monotype.android.font.myinghei18030m","description":"Myinghei18030m FlipFont\nChanges the user interface font on your phone.","removal":"delete","type":"oem"},{"id":"com.monotype.android.font.myuppymediumtc","description":"Myuppymediumtc FlipFont\nChanges the user interface font on your phone.","removal":"delete","type":"oem"},{"id":"com.monotype.android.font.oxaniumregular","description":"OxaniumRegular FlipFont\nChanges the user interface font on your phone.","removal":"delete","type":"oem"},{"id":"com.monotype.android.font.rajdhanimedium","description":"RajdhaniMedium FlipFont\nChanges the user interface font on your phone.","removal":"delete","type":"oem"},{"id":"com.monotype.android.font.samsungone","description":"Samsung One font\n","removal":"delete","type":"oem"},{"id":"com.monotype.android.font.samsungsans","description":"SamsungSans font\nFont","removal":"delete","type":"oem"},{"id":"com.monotype.android.font.syndor","description":"Syndor FlipFont\nChanges the user interface font on your phone.","removal":"delete","type":"oem"},{"id":"com.motorola.VirtualUiccPayment","label":"Virtual UICC Payment","description":"SIM Card Payment also NFC. Only useful in China by hidden image files.\nUICC stands for Universal Integrated Circuit Card.\nIt is the physical and logical platform for the USIM and may contain additional USIMs and other applications.\n(U)SIM is an application on the UICC.\nI guess this package provides support for NFC payments.\nNote: The term SIM is widely used to refer to both SIMs and UICCs in the industry and among consumers.","web":["https://bluesecblog.wordpress.com/2016/11/18/uicc-sim-usim/","https://blog.velosiot.com/euicc-and-esim-are-they-the-same-thing"],"removal":"delete","type":"oem"},{"id":"com.motorola.actions","label":"Moto Actions & Gestures","description":"Allows you to perform specific gestures to perform certain tasks. Frontend to change settings provided by \"com.motorola.moto\".","web":["https://play.google.com/store/apps/details?id=com.motorola.actions","https://beta.pithus.org/report/5c26c2865ec9692efba4377598f8130c25f66706901144f49438230a11590f01"],"removal":"delete","type":"oem"},{"id":"com.motorola.actions.overlay","label":"com.motorola.actions.overlay","description":"Overlay package for \"com.motorola.actions\".","removal":"delete","type":"oem"},{"id":"com.motorola.aiservices","label":"Moto AI Services","description":"Service to supply artificial intelligence models to Motorola apps. Not sure where the AI services are integrated.","web":["https://play.google.com/store/apps/details?id=com.motorola.aiservices","https://beta.pithus.org/report/effeb339cfeb3d8fcbf2023b6ccdec77e012d9417ee0579cf998bcb090741362"],"removal":"delete","type":"oem"},{"id":"com.motorola.android.connectivity.resources.overlay","description":"Configs to bad wifi better keep it.","removal":"unsafe","type":"oem"},{"id":"com.motorola.android.coresettingsext.overlay.avatrn","description":"This package is part of Motorola's custom overlay for system settings, specifically tailored for the Motorola Edge 2024 (codename 'avatrn').","removal":"caution","type":"oem"},{"id":"com.motorola.android.coresettingsext.overlay.doubletap","description":"Double tap to put display to sleep","removal":"delete","type":"oem"},{"id":"com.motorola.android.coresettingsext.overlay.dubai","description":"Needed for support other refresh rate value?","removal":"caution","type":"oem"},{"id":"com.motorola.android.fmradio","label":"FMRadioService","dependencies":["com.motorola.fmplayer"],"description":"Required for Motorola FM Radio","removal":"delete","type":"oem"},{"id":"com.motorola.android.fota","label":"Software update","description":"Required for OTA updates which are ssential part of keeping your device secure and up to date with regular security patchs.\nFOTA = firmware over the air","removal":"caution","warning":"Breaks OTA updates","type":"oem"},{"id":"com.motorola.android.jvtcmd","label":"JavaTcmdHelper","description":"tcmd = commandes types. Seems to be a tools wich help find Java commands types.\nUseless for normal user.","removal":"delete","type":"oem"},{"id":"com.motorola.android.launcher.overlay.animation.scale","description":"Overlay to transition_anim_scale? Maybe unused, not sure.","removal":"caution","type":"oem"},{"id":"com.motorola.android.launcher.overlay.motolauncheroverlayna","description":"This package is part of Motorola's custom launcher overlay, specifically tailored for their devices.","removal":"replace","type":"oem"},{"id":"com.motorola.android.launcher.overlay.retail.global","description":"This package is part of Motorola's custom launcher overlay designed for retail or global versions of their devices.","removal":"replace","type":"oem"},{"id":"com.motorola.android.nativedropboxagent","label":"NativeDropBoxAgent","description":"It is not related to Cloud Dropbox company but to Android logging. It is used during development.","web":["https://stackoverflow.com/questions/4434192/dropboxmanager-use-cases","https://beta.pithus.org/report/b7376e9ca607e856c9b39eb93e5aa420dab7f32424b61c5325f55faa03d2a97f"],"removal":"delete","type":"oem"},{"id":"com.motorola.android.networkstack.overlay.mcc460","description":"This package is part of Motorola's custom network stack overlay, specifically related to the network configuration for a particular Mobile Country Code (MCC), in this case, MCC 460 (which corresponds to China).","removal":"caution","type":"oem"},{"id":"com.motorola.android.networkstack.tethering.overlay.motCommon","description":"This package is part of Motorola's custom overlay for the network stack, specifically related to tethering and hotspot functionalities.","removal":"replace","type":"oem"},{"id":"com.motorola.android.overlay.common","description":"It has some important configs. Better don't risk.","removal":"unsafe","type":"oem"},{"id":"com.motorola.android.overlay.crystaltalkai","description":"Enable crystal talk AI settings? I've never seen that.","removal":"delete","type":"oem"},{"id":"com.motorola.android.overlay.deviceconfig.manaus","description":"Random configurations to basic things(powersaving, network, camera, etc.).","removal":"unsafe","type":"oem"},{"id":"com.motorola.android.overlay.gabutton","description":"\"Double press on power behavior\" 0\nNot sure if useful, maybe it's another gesture.","removal":"caution","type":"oem"},{"id":"com.motorola.android.overlay.lhbm","description":"Not sure if its for high brightness mode, still unknown\n'config_udfps_local_hbm_supported' 'true'","removal":"caution","type":"oem"},{"id":"com.motorola.android.overlay.lpptoga","description":"This package is part of Motorola's custom overlay and is likely associated with specific visual or functional enhancements tailored for Motorola devices.","removal":"replace","type":"oem"},{"id":"com.motorola.android.overlay.payjoy","label":"com.motorola.android.overlay.payjoy","description":"Overlay for 'com.payjoy.access'","removal":"delete","type":"oem"},{"id":"com.motorola.android.overlay.qcom.common","description":"This package is part of Motorola’s custom overlay and is associated with Qualcomm (Qcom) related functionality or optimizations.","removal":"replace","type":"oem"},{"id":"com.motorola.android.overlay.wfd","description":"It has config to enable wifi display.","removal":"unsafe","type":"oem"},{"id":"com.motorola.android.providers.chromehomepage","label":"com.motorola.android.providers.chromehomepage","description":"Seems to provide the \"Home\"-button functionality in Chrome.","web":["https://forum.xda-developers.com/android/apps-games/app-chrome-homepage-t3695804"],"removal":"delete","type":"oem"},{"id":"com.motorola.android.providers.settings","label":"Settings storage","description":"Seems to required for device settings.","removal":"unsafe","warning":"Some of the device settings will crash continuously.","type":"oem"},{"id":"com.motorola.android.providers.settings.auto_generated_rro_product__","label":"com.motorola.android.providers.settings.auto_generated_rro_product__","description":"RRO = runtime resource overlay.\nUsed for various system settings customizations.","web":["https://www.phonecheck.com/blog/what-is-android-auto-generated-rro","https://beta.pithus.org/report/ecc311a58af6143697c69fba7d1387892f778386db96303ddf518411a7f41598"],"removal":"delete","type":"oem"},{"id":"com.motorola.android.providers.settings.overlay.dppcamera","description":"This package is part of Motorola's custom settings overlay, specifically related to settings for the camera application.","removal":"replace","type":"oem"},{"id":"com.motorola.android.providers.settings.overlay.dppnone","description":"This package is part of Motorola's custom settings overlay, potentially dealing with settings or configurations that are applied when certain features or options are not in use.","removal":"replace","type":"oem"},{"id":"com.motorola.android.provisioning","label":"OMA client provisioning","description":"It is a protocol specified by the Open Mobile Alliance (OMA).\nIt is used by carrier to send \"configuration SMS\" which can setup network settings (such as APN).\nIn my case, it was automatic and I never needed configuration messages. I'm pretty sure that in France this package is useless.\nMaybe it's useful if carriers change their APN... but you still can change it manually, it's not difficult.\nNote : These special \"confirguration SMS\" can be abused.","web":["https://www.zdnet.fr/actualites/les-smartphones-samsung-huawei-lg-et-sony-vulnerables-a-des-attaques-par-provisioning-39890045.html","https://www.csoonline.com/article/3435729/sms-based-provisioning-messages-enable-advanced-phishing-on-android-phones.html"],"removal":"replace","type":"oem"},{"id":"com.motorola.android.settings.diag_mdlog","description":"Diag_mdlog is a small proprietary Qualcomm program which can store DIAG logs on the filesystem.\nNo longer in Android 10 image\n","removal":"delete","type":"oem"},{"id":"com.motorola.android.settings.modemdebug","label":"Modem Debug Settings","description":"Provide modem debug settings menu ?\nNo longer in Android 10 image","removal":"delete","type":"oem"},{"id":"com.motorola.android.settings.overlay.eqs","description":"Overlay to refresh rate change in settings.\nNot sure if it's unused.","removal":"caution","type":"oem"},{"id":"com.motorola.android.settings.overlay.fps.display","description":"Overlay to fps display.","removal":"replace","type":"oem"},{"id":"com.motorola.android.settings.overlay.global","description":"This package is part of Motorola's custom settings overlay and is designed for global settings that apply across different regions or device configurations.","removal":"replace","type":"oem"},{"id":"com.motorola.android.settings.overlay.lra","description":"This package is part of Motorola’s custom settings overlay, likely related to specific regional or device configurations.","removal":"replace","type":"oem"},{"id":"com.motorola.android.settings.overlay.power.bottom","description":"This package is part of Motorola’s custom settings overlay, specifically related to power management settings.","removal":"replace","type":"oem"},{"id":"com.motorola.android.systemui.overlay.att","description":"This overlay needed for (com.motorola.attvowifi) I guess. It's for WiFi Calling.","removal":"replace","type":"oem"},{"id":"com.motorola.android.systemui.overlay.sprint","description":"Needed for Moto Stats WiFi, it's useless.","removal":"delete","type":"oem"},{"id":"com.motorola.android.systemui.overlay.tmo","description":"Needed for Moto Stats WiFi, it's useless.","removal":"delete","type":"oem"},{"id":"com.motorola.android.systemui.overlay.usc","description":"Needed for Moto Stats WiFi, it's useless.","removal":"delete","type":"oem"},{"id":"com.motorola.android.systemui.overlay.vzw","description":"Needed for Moto Stats WiFi, it's useless.","removal":"delete","type":"oem"},{"id":"com.motorola.appdirectedsmsproxy","label":"Motorola Message Service","description":"An Application directed SMS (or rather a Port directed SMS) is an SMS directed to a specific port.\nApps need to listen to this port to get the SMS message.\nI don't know if this package allows port directed SMS or if it just provide a proxy feature.","removal":"delete","type":"oem"},{"id":"com.motorola.appforecast","label":"Performance","description":"Seems to be always running in background.Not sure what it does.","web":["https://beta.pithus.org/report/54ae099575a10e12e59064e2999373332b0b6d2eb76b56964697f87700db1dbb"],"removal":"delete","type":"oem"},{"id":"com.motorola.audiofx","description":"/!\\ Removal causes bootloop on Moto G7 Power (Android 10)\nAudio effects\nProvide features like Equalizer, Surround sound...\n","removal":"unsafe","type":"oem"},{"id":"com.motorola.audiorecorder","description":"Audio recorder\nStock Audio recorder for Motorola\nhttps://play.google.com/store/apps/details?id=com.motorola.audiorecorder","removal":"replace","type":"oem"},{"id":"com.motorola.bach.modemstats","label":"Modem Service","description":"Statistics and events logs from the modem activity.\nResponsible for opening the network services on your Verizon phone.","web":["https://internet-access-guide.com/what-is-motorola-modem-service/"],"removal":"caution","warning":"It will adversely influence the data usage and connectivity if disabled.","type":"oem"},{"id":"com.motorola.batterycare","description":"This package is part of Motorola's battery management system, specifically related to features or optimizations for battery care.","removal":"replace","type":"oem"},{"id":"com.motorola.batterycare.overlay","description":"This package is part of Motorola’s custom battery care overlay and is likely responsible for providing Motorola-specific enhancements or visual elements related to battery management.","removal":"replace","type":"oem"},{"id":"com.motorola.batterycare.overlay.appside","description":"This package is part of Motorola's custom battery care overlay and is likely associated with specific enhancements or visual elements related to battery management on the app side of the user interface.","removal":"replace","type":"oem"},{"id":"com.motorola.blur.service.blur","description":"MOTOBLUR service\nAutomatically syncs messages, emails, social status updates, contacts and pictures from your accounts straight to your Home screen. It's a serious privacy concern:\nhttps://www.beneaththewaves.net/Projects/Motorola_Is_Listening.html","removal":"replace","type":"oem"},{"id":"com.motorola.brapps","label":"App Box","description":"Offers you a selection of applications developed by Brazilians and also apps selected for you.","web":["https://play.google.com/store/apps/details?id=com.motorola.brapps","https://beta.pithus.org/report/842d17e1944d748a7813ee8deb072224fd0665cf6c0504a0d90e46568d4c444d"],"removal":"replace","suggestions":"app_stores","type":"oem"},{"id":"com.motorola.bug2go","label":"com.motorola.bug2go","description":"Bugs reporting app that sends info about various crash reports and logs.","removal":"delete","type":"oem"},{"id":"com.motorola.callredirectionservice","label":"com.motorola.callredirectionservice","description":"Added in Android 10. Provide support for call redirection/cancellation if your Carrier supports it.","web":["https://motorola-global-portal.custhelp.com/app/answers/prod_answer_detail/a_id/140542","https://en.wikipedia.org/wiki/Call_forwarding"],"removal":"delete","type":"oem"},{"id":"com.motorola.camera2","description":"Moto Camera 2 (https://play.google.com/store/apps/details?id=com.motorola.camera)\n","removal":"replace","type":"oem"},{"id":"com.motorola.camera3","description":"Moto Camera 3\nMoto camera app\nhttps://play.google.com/store/apps/details?id=com.motorola.camera3","removal":"replace","type":"oem"},{"id":"com.motorola.camera3.consent.ai","description":"Camera consent AI\nCamera consent AI? Artificial Intelligence models? Probably additonal features to camera app","removal":"replace","type":"oem"},{"id":"com.motorola.camera3.content.ai","description":"This package is related to Motorola's camera application, specifically focusing on AI (artificial intelligence) features.","removal":"replace","type":"oem"},{"id":"com.motorola.carriersettingsext","description":"This is a WiFi calling app supported by AT&T, T-Mobile, Orange.","removal":"caution","type":"oem"},{"id":"com.motorola.ccc.devicemanagement","label":"Device Management","description":"Mobile Device Management (MDM) allows company’s IT department to reach inside your phone in the background, allowing them to ensure your device is secure, know where it is, and remotely erase your data if the phone is stolen.","web":["https://onezero.medium.com/dont-put-your-work-email-on-your-personal-phone-ef7fef956c2f","https://blog.cdemi.io/never-accept-an-mdm-policy-on-your-personal-phone/"],"removal":"delete","type":"oem"},{"id":"com.motorola.ccc.mainplm","label":"Motorola Services Main","description":"plm = Product Lifecycle Management ? No noticeable consequences after removal","removal":"delete","type":"oem"},{"id":"com.motorola.ccc.notification","label":"Motorola Notifications","description":"If you opt-in, it sends periodic product-related information, including notifications on software updates, tips & tricks, survey and information about new Motorola products and services.","web":["https://play.google.com/store/apps/details?id=com.motorola.ccc.notification"],"removal":"delete","type":"oem"},{"id":"com.motorola.ccc.ota","label":"Motorola software update","description":"Provide OTA system updates.\nOTA (Over-The-Air) updates allow manufacturers to remotely install new software updates, features and services.","removal":"caution","warning":"Breaks OTA updates","type":"oem"},{"id":"com.motorola.comcast.settings.extensions","description":"Most likely provides a special settings menu for Comcast stuff.\nI think it's installed on Xfinity branded phones.\nSafe to remove (tested only on non-Comcast phone).\n","removal":"delete","type":"oem"},{"id":"com.motorola.comcastext","label":"Activation","description":"See above. Provides special features from Comcast? Probably safe to remove (tested only on non-Comcast phone).","removal":"delete","type":"oem"},{"id":"com.motorola.config.wifi","label":"com.motorola.config.wifi","description":"Appears safe to remove.\nWPA config App\nWi-Fi not affected after removal.","removal":"delete","type":"oem"},{"id":"com.motorola.contacts.preloadcontacts","label":"Preloaded Contacts Loader","description":"Provides contacts preset by carriers.","removal":"delete","type":"oem"},{"id":"com.motorola.contacts.preloadcontacts.overlay.vzw","description":"Useless icon to Verizon Wireless app.","removal":"delete","type":"oem"},{"id":"com.motorola.coresettingsext","description":"Core Settings extension\nSafe to remove (no bootloop) but its usefulness remains unkown.\nIt's an app for random settings, but it's unused?","removal":"caution","type":"oem"},{"id":"com.motorola.dciservice","description":"It's IMS Signal tests probably. Used for statistics. Code 3243.","removal":"delete","type":"oem"},{"id":"com.motorola.demo","label":"Demo mode","description":"Enable retail demonstration mode.","web":["https://source.android.com/devices/tech/display/retail-mode"],"removal":"delete","type":"oem"},{"id":"com.motorola.demo.env","label":"com.motorola.demo.env","description":"Needed for Moto Demo Mode\nenv = environment","removal":"delete","type":"oem"},{"id":"com.motorola.discovery","label":"Moto Discovery","description":"Not sure what it does","removal":"delete","type":"oem"},{"id":"com.motorola.dolby.dolbyui","label":"Dolby Atmos","description":"Dolby helps you rich and engaging sound.","removal":"replace","type":"oem"},{"id":"com.motorola.dynamicvolume","label":"Adaptive volume","description":"Used to control the Multi-Volume feature, where apps can have their own volume level set. It can also automatically mute apps that you always mute manually.","removal":"delete","type":"oem"},{"id":"com.motorola.easyprefix","label":"Easy Prefix","description":"Auto add CSP (Service Provider code) prefix to your phone when you're abroad.\nhttps://en.wikipedia.org/wiki/List_of_country_calling_codes\nThis seems to not work correctly and it's generally not a good idea to call home (via GSM) when you're abroad.\nIt's better and cheaper to use chat apps like Signal/Wire","web":["https://play.google.com/store/apps/details?id=com.motorola.easyprefix"],"removal":"delete","type":"oem"},{"id":"com.motorola.email","label":"Moto Email","description":"Auto add CSP (Service Provider code) prefix to your phone when you're abroad.\nhttps://en.wikipedia.org/wiki/List_of_country_calling_codes\nThis seems to not work correctly and it's generally not a good idea to call home (via GSM) when you're abroad.\nIt's better and cheaper to use chat apps like Signal/Wire","web":["https://play.google.com/store/apps/details?id=com.motorola.email"],"removal":"delete","type":"oem"},{"id":"com.motorola.enterprise.adapter.service","description":"Moto Thinkshield\nAdditional security frameworks.","removal":"delete","type":"oem"},{"id":"com.motorola.enterprise.service","label":"Moto Thinkshield-MM","description":"Provides various security to Moto devices. More info needed.","web":["https://www.motorola.com/business/thinkshield/","https://beta.pithus.org/report/db140841cffe28643367bd1d595c885a02852d06136086b0ffc41aab79db5ff0"],"removal":"delete","type":"oem"},{"id":"com.motorola.entitlement","label":"Entitlement","description":"Enable WiFi tethering/hotspot functionality.\nWhat you can do is preventing the phone from notifying the carrier about when you use hotspot. It will bypass mobile carriers tethering restrictions.\nFrom an ADB shell : settings put global tether_dun_required 0","removal":"caution","type":"oem"},{"id":"com.motorola.faceunlock","label":"Moto Face Unlock","description":"Unlock your device by simply looking at the display.","web":["https://play.google.com/store/apps/details?id=com.motorola.faceunlock","https://www.ubergizmo.com/2017/03/galaxy-s8-facial-unlock-photograph/","https://www.kaspersky.com/blog/face-unlock-insecurity/21618/","https://www.freecodecamp.org/news/why-you-should-never-unlock-your-phone-with-your-face-79c07772a28/"],"removal":"replace","type":"oem"},{"id":"com.motorola.faceunlocktrustagent","label":"Motorola Face Unlock Agent","description":"Trust agent is a service that notifies the system about whether it believes the environment of the device is trusted.\nThe meaning of 'trusted' is up to the trust agent to define.\nThe system lockscreen listens for trust events, it can change its behaviour based on the trust state of the current user (e.g detection of a trusted face)","web":["https://nelenkov.blogspot.com/2014/12/dissecting-lollipops-smart-lock.html"],"removal":"replace","type":"oem"},{"id":"com.motorola.fmplayer","label":"FM Radio","required_by":["com.motorola.android.fmradio"],"description":"FM Radio for Motorola devices","web":["https://play.google.com/store/apps/details?id=com.motorola.fmplayer","https://beta.pithus.org/report/0113cb712cc43c94f904601120c9100640a65b3043a41b7efe63b340bdae4995"],"removal":"replace","suggestions":"radios","type":"oem"},{"id":"com.motorola.frameworks.singlehand","label":"com.motorola.frameworks.singlehand","description":"Provide the Single/One hand mode\nI don't know why frameworks appears in the package name because it's not only the framework.","web":["https://support.motorola.com/us/en/documents/MS116403/"],"removal":"delete","type":"oem"},{"id":"com.motorola.freeform","description":"Freeform\nRequired for window app mode.","removal":"caution","type":"oem"},{"id":"com.motorola.gamemode","label":"Moto Gametime","description":"Allows the user to block calls, quick screenshots, float social apps, etc when playing a game.","web":["https://play.google.com/store/apps/details?id=com.motorola.gamemode"],"removal":"delete","type":"oem"},{"id":"com.motorola.genie","label":"Device Help","description":"Previously Moto Help\nAn app that checks hardware status and gives the user contacts for support.","web":["https://play.google.com/store/apps/details?id=com.motorola.genie"],"removal":"delete","type":"oem"},{"id":"com.motorola.gesture","label":"Gesture navigation tutorial","description":"Gesture navigation tutorial added in Android 10.","removal":"delete","type":"oem"},{"id":"com.motorola.handwritingcalculator","description":"Handwriting calculator\nit's not needed.","removal":"delete","type":"oem"},{"id":"com.motorola.help","label":"Moto feedback","description":"Lets you rate your device and share feedback with Motorola.","web":["https://play.google.com/store/apps/details?id=com.motorola.help"],"removal":"delete","type":"oem"},{"id":"com.motorola.help.extlog","label":"Extended Log","description":"Not sure what it does","removal":"delete","type":"oem"},{"id":"com.motorola.hiddenmenuapp","label":"HiddenMenu","description":"Added in Android 10. Not sure what it does.","removal":"delete","type":"oem"},{"id":"com.motorola.imagertuning_V2","label":"Camera Tuner","description":"Applies improvements to the camera hardware so that any app that uses the camera will be improved.","web":["https://play.google.com/store/apps/details?id=com.motorola.imagertuning_V2"],"removal":"delete","type":"oem"},{"id":"com.motorola.imagertuning_lake","label":"Imager Tuning","description":"Naming convention: imagertuning_[PHONE CODENAME]\nThis is the custom camera image processing stack on Motorola devices. It's generally important for improving image quality.\nPlaystore reviews indicate that it slows down the camera app significantly for some users (probably a bug).","web":["https://play.google.com/store/apps/details?id=com.motorola.imagertuning_athene"],"removal":"replace","type":"oem"},{"id":"com.motorola.imagertuning_u","description":"This package is associated with Motorola’s image tuning features, specifically designed to enhance or adjust image quality settings on Motorola devices.","removal":"replace","type":"oem"},{"id":"com.motorola.imagetuning_V2","description":"Camera Tuner\nIt's Camera tuning Chromatix Comparison. I think it's needed for camera app.","removal":"replace","type":"oem"},{"id":"com.motorola.installer","description":"This package is used by Motorola for managing and installing system updates, custom apps, or other Motorola-specific applications.","removal":"replace","type":"oem"},{"id":"com.motorola.invisiblenet","label":"Invisible Net","description":"App for Alias ID, Shortcut Installer (most of the apps I have seen are Chinese apps).\nIt's some kind of a stub application launcher, if you open any of it's activities it goes to the play store or to the browser.","removal":"delete","type":"oem"},{"id":"com.motorola.iqimotmetrics","description":"Hidden privacy policy: network diagnostics data.","removal":"delete","type":"oem"},{"id":"com.motorola.launcher.secondarydisplay","description":"Appears to enable support for a secondary display with Moto's launcher.\nTrying to remove this packages returns \"Failure: package is non-disable\".","removal":"replace","type":"oem"},{"id":"com.motorola.launcher3","label":"Moto App Launcher","description":"A default home screen app, provides a layout and display for app icons and listing.\nWARNING: Do not remove this package if you did not switch to a 3rd-pary launcher.\nKeep in mind that removing this package will break the `recent apps` button (even from another launcher).","removal":"caution","warning":"Uninstalling this breaks the Recents screen in across launchers","type":"oem"},{"id":"com.motorola.launcherconfig","label":"Google Launcher Config","description":"It is a partner customization extension.\nHas a lot wallpapers in resources.","removal":"replace","suggestions":"launchers","type":"oem"},{"id":"com.motorola.leanbacklauncher","description":"Moto Experience Hub\nMost people hate this app.\nhttps://play.google.com/store/apps/details?id=com.motorola.leanbacklauncher","removal":"delete","type":"oem"},{"id":"com.motorola.lifetimedata","description":"It's most likely the Total Call Timer or more generally it handles info like the date of manufacture of your device, usage time since first boot etc.\nTotal Call Timer gives you the time you spent calling. I don't know how to access to these info. It may be a hidden menu and may be accessible through the dialer with a special code.","removal":"delete","type":"oem"},{"id":"com.motorola.livewallpaper3","description":"Moto interactive wallpapers\nResponds to your actions to bring your screen to life.\nhttps://play.google.com/store/apps/details?id=com.motorola.livewallpaper3","removal":"delete","type":"oem"},{"id":"com.motorola.livewallpaper3.prebuilt.chroma_plume","description":"Motorola Prebuilt themes Chroma Plume","removal":"delete","type":"oem"},{"id":"com.motorola.livewallpaper3.prebuilt.cool_bamboo","description":"Motorola Prebuilt themes Cool Bamboo","removal":"delete","type":"oem"},{"id":"com.motorola.livewallpaper3.prebuilt.lovely_peach","description":"Motorola Prebuilt themes Lovely Peach","removal":"delete","type":"oem"},{"id":"com.motorola.livewallpaper3.prebuilt.mysterious_amber","description":"Motorola Prebuilt themes Mysterious Amber","removal":"delete","type":"oem"},{"id":"com.motorola.livewallpaper3.prebuilt.romantic_wisteria","description":"Motorola Prebuilt themes Romantic Wisteria","removal":"delete","type":"oem"},{"id":"com.motorola.livewallpaper3.prebuilt.titan","description":"Motorola Prebuilt themes Cosmic journey","removal":"delete","type":"oem"},{"id":"com.motorola.livewallpaper3.prebuilt.tranquil_whale","description":"Motorola Prebuilt themes Tranquil Whale","removal":"delete","type":"oem"},{"id":"com.motorola.livewallpaper3.prebuilt.twilight_twist","description":"Motorola Prebuilt themes Twilight twist","removal":"delete","type":"oem"},{"id":"com.motorola.livewallpaper3.prebuilt.vibrant_sapling","description":"Motorola Prebuilt themes Vibrant Sapling","removal":"delete","type":"oem"},{"id":"com.motorola.mobiledesktop","description":"Ready For\nConnect to a PC to stream mobile apps and other things.\nhttps://play.google.com/store/apps/details?id=com.motorola.mobiledesktop","removal":"delete","type":"oem"},{"id":"com.motorola.mobiledesktop.core","description":"Needed for Ready For (com.motorola.mobiledesktop)\nConnect to a PC to stream mobile apps and other things.\nDISABLE this app instead of Uninstall, because of anomalies with lockscreen and clock widget.","removal":"delete","type":"oem"},{"id":"com.motorola.motcameradesktop","description":"Camera Desktop Settings\nMoto Webcam, video call effects.","removal":"delete","type":"oem"},{"id":"com.motorola.moto","description":"Moto (https://play.google.com/store/apps/details?id=com.motorola.moto)\nApp providing Moto Actions, Moto Display, and other feature families that let you customize the way you interact with your device. \nMoto Actions is another app (https://play.google.com/store/apps/details?id=com.motorola.actions). Gestures set with \"Moto\" prior will continue to work provided \"Moto Actions\" remains installed.\n","removal":"delete","type":"oem"},{"id":"com.motorola.motocare","description":"Moto Care was renamed in \"Moto Help\" and then in \"Device Help\"\nProvide support features.\nhttps://mobile.softpedia.com/blog/Moto-Care-App-Gets-Updated-Now-Called-Motorola-Help-432827.shtml\nHowever you can both have com.motorola.genie (Device Help) and this package so it's strange.\n","removal":"delete","type":"oem"},{"id":"com.motorola.motocare.internal","description":"Core stuff for the package above I guess.","removal":"delete","type":"oem"},{"id":"com.motorola.motocit","description":"CQATest\nCQA = Custom Quality Assurance\nHidden menu (accessible by typing *#*#2486#*#* in the Moto Dialer) which lets you run hardware tests.\n","removal":"delete","type":"oem"},{"id":"com.motorola.motodisplay","label":"Moto Display","description":"Displays notifications with the screen off (like the Always On Display feature from other OEMs)","web":["https://play.google.com/store/apps/details?id=com.motorola.motodisplay","https://support.motorola.com/uk/en/solution/ms108519"],"removal":"delete","type":"oem"},{"id":"com.motorola.motofpstouch","description":"Moto Power Touch.\nAllows the user to create custom actions depending on how the user presses the power button.\nhttps://play.google.com/store/apps/details?id=com.motorola.motofpstouch&gl=US","removal":"delete","type":"oem"},{"id":"com.motorola.motointelligence","label":"Moto Intelligence","description":"Not sure what it does","removal":"delete","type":"oem"},{"id":"com.motorola.motointelligence.overlay","label":"com.motorola.motointelligence.overlay","description":"Overlay for 'com.motorola.motointelligence'","removal":"delete","type":"oem"},{"id":"com.motorola.motolights","description":"Moto Lights\nPeople recommend to remove it:\nhttps://xdaforums.com/t/debloat-instructions-for-edge-40-pro-rtwo.4590155/\nMoto edge lights controller, hard to check this app but it has something to control display brightness in phone.","removal":"replace","type":"oem"},{"id":"com.motorola.motosignature.app","label":"MotoSignatureApp","description":"This app has permissions descriptions, lab, game mode launch permissions, audio monitor descriptions.\nSo it's permissions to system apps?\nGoogle search shows that it's on some lists that recommend removing this app.","removal":"delete","type":"oem"},{"id":"com.motorola.motosignature2.app","description":"Uses library 'com.motorola.motosignature', perm info to start gif maker, permlab moto trusted apps overlay and permlab monitor input.","removal":"replace","type":"oem"},{"id":"com.motorola.mototour","description":"Moto Tour\nHelps you how to use motorola phone.\nhttps://play.google.com/store/apps/details?id=com.motorola.mototour","removal":"delete","type":"oem"},{"id":"com.motorola.msimsettings","description":"Dual SIM Settings\nProvides Dual SIM feature.\n","removal":"unsafe","type":"oem"},{"id":"com.motorola.msimsettings.overlay","description":"In code found: 'channel_force_single_sim'. Looks very unused.","removal":"delete","type":"oem"},{"id":"com.motorola.mykey","description":"This package is related to Motorola’s 'MyKey' feature, which is a tool designed to manage key settings and restrictions for device usage.","removal":"replace","type":"oem"},{"id":"com.motorola.nfc","description":"Support for NFC protocol.","removal":"replace","type":"oem"},{"id":"com.motorola.nfwlocationattribution","description":"Useless Carrier Location Access. You can find it in settings location but it's not very useful.","removal":"delete","type":"oem"},{"id":"com.motorola.odm.camera3","description":"Moto camera app?","removal":"caution","type":"oem"},{"id":"com.motorola.om","description":"The package 'com.motorola.om' is associated with Motorola's operational management or device management tools.","removal":"replace","type":"oem"},{"id":"com.motorola.omadm.service","description":"Appears safe to remove.\nCarrier Provisioning Service\nProvisioning involves the process of preparing and equipping a network to allow it to provide new services to its users.\nOMADM = OMA Device Management\nBasically, it handles configuration of the device (including first time use), enabling and disabling features provided by carriers.\nhttps://en.wikipedia.org/wiki/OMA_Device_Management\nUse case seems very limited : https://www.androidpolice.com/2015/03/10/android-5-1-includes-new-carrier-provisioning-api-allows-carriers-easier-methods-of-setting-up-services-on-devices-they-dont-control/\n","removal":"delete","type":"oem"},{"id":"com.motorola.overlay.googleasi","description":"Overlay to Android System Intelligence 'com.google.android.as',\nremoval means that you cannot use the features of this app probably.","removal":"replace","type":"oem"},{"id":"com.motorola.overlay.launcher3","description":"Useless overlay config default to Launcher3 default launcher Moto (com.motorola.launcher3)","removal":"delete","type":"oem"},{"id":"com.motorola.paks","description":"ADB: Package Protected.\nMy Q Paks \nThird-party application bundles\nhttps://www.financialmirror.com/2007/10/31/motorola-packs-moto-q-9h-global-smart-device-with-third-party-applications/\n","removal":"delete","type":"oem"},{"id":"com.motorola.paks.notification","description":"Notifications from (com.motorola.paks)","removal":"delete","type":"oem"},{"id":"com.motorola.paramupdater","description":"I am not sure what this app is for.\nCollects systeminfo, time, has notification about update.","removal":"replace","type":"oem"},{"id":"com.motorola.personalize","label":"Personalise","description":"Helps you personalise your themes, icons, fonts, sounds etc...","removal":"delete","type":"oem"},{"id":"com.motorola.pgmsystem2","description":"Appears safe to remove\nPGM System\nI didn't find info about this package. \nFor Me PGM = Peak Gate Power (for MOSFET transistor) but I'm not convinced it has this meaning here.\n","removal":"delete","type":"oem"},{"id":"com.motorola.photoeditor","label":"Moto Photo Editor","description":"On Motorola phones that feature Cutout mode, you can replace the background of your photos and resize & move selected portions.","web":["https://play.google.com/store/apps/details?id=com.motorola.photoeditor"],"removal":"delete","type":"oem"},{"id":"com.motorola.programmenu","label":"Programming Menu","description":"Hidden menu (accessible by typing ##7764726 in the dialer) providing additionnal features for developers.","removal":"delete","type":"oem"},{"id":"com.motorola.ptt.prip","description":"Prip (https://play.google.com/store/apps/details?id=com.motorola.ptt.prip)\nPush-To-Talk app. Allows to you send calls over any wireless carrier’s 3G or 4G networks or a WiFi connection.\nIt offers unlimited calling between other users and Nextel phone owners, rather than universal calling credit, \nand works on a monthly subscription basis.\nhttps://prip.me/#get\nNo longer in Android 10 image\n","removal":"delete","type":"oem"},{"id":"com.motorola.rcsConfigService","description":"RCS Config Service\nNeeded for IMS, RCS. WiFi Calling.","removal":"replace","type":"oem"},{"id":"com.motorola.revoker.services","description":"It's another component of setup wizard only used on first-boot setup.","removal":"delete","type":"oem"},{"id":"com.motorola.safetycenter.resources.overlay","description":"Useless overlay to com.google.android.safetycenter.resources\nNetwork protection, ThinkShield.","removal":"delete","type":"oem"},{"id":"com.motorola.screenshoteditor","label":"Screenshot editor","description":"Moto default screenshot app that supports longer screenshots & gif.","web":["https://play.google.com/store/apps/details?id=com.motorola.screenshoteditor"],"removal":"replace","type":"oem"},{"id":"com.motorola.securevault","description":"Secure Folder\nUnnecessary tools to keep device secure.\nhttps://play.google.com/store/apps/details?id=com.motorola.securevault","removal":"delete","type":"oem"},{"id":"com.motorola.securityhub","description":"Moto Secure\nUnnecessary tools to keep device secure.\nhttps://play.google.com/store/apps/details?id=com.motorola.securityhub","removal":"delete","type":"oem"},{"id":"com.motorola.securityhubext","description":"This package is related to Motorola's Security Hub, an extension or enhancement for the device’s security features.","removal":"replace","type":"oem"},{"id":"com.motorola.settings","label":"System update","description":"Exactly not sure what it does.","removal":"caution","type":"oem"},{"id":"com.motorola.setup","label":"Setup","description":"Related to Motorola Account setup (only during first boot?)\nSafe to remove according to xda users.","removal":"delete","type":"oem"},{"id":"com.motorola.setup.auto_generated_rro_product__","description":"This package is involved in Motorola's setup process, particularly handling automatically generated resources related to the setup of the device.","removal":"caution","type":"oem"},{"id":"com.motorola.setup.auto_generated_rro_vendor__","description":"I found it's \"no sim anim\". It's another component of Motorola first-boot setup.","removal":"delete","type":"oem"},{"id":"com.motorola.setup.overlay.amx","description":"Useless component of (com.motorola.setup)","removal":"delete","type":"oem"},{"id":"com.motorola.setup.overlay.dataenable","description":"This package is used during the Motorola device setup process, specifically for managing or presenting settings related to mobile data.","removal":"caution","type":"oem"},{"id":"com.motorola.setup.overlay.gabuttonrighttop","description":"Useless component of (com.motorola.setup)","removal":"delete","type":"oem"},{"id":"com.motorola.setup.overlay.pai","description":"Useless component of (com.motorola.setup)","removal":"delete","type":"oem"},{"id":"com.motorola.setup.overlay.tracfone","description":"Useless component of (com.motorola.setup)","removal":"delete","type":"oem"},{"id":"com.motorola.slpc_sys","label":"Motorola Slpc System","description":"Would be weird if it's not related to Motorola Modality Services (https://play.google.com/store/apps/details?id=com.motorola.slpc)\nHelps your Motorola phone respond more intelligently to motion, phone orientation (e.g. face up/down) and stowed state (e.g in/out-of-pocket).\nHas a noticeable impact on battery?\nFYI : It uses location services.","web":["https://forum.xda-developers.com/moto-x-2014/help/location-modality-services-battery-t2982752"],"removal":"delete","type":"oem"},{"id":"com.motorola.smart5g","description":"Battery saving for 5G.","removal":"caution","type":"oem"},{"id":"com.motorola.spaces","description":"Family Space\nLimits time spending on app for kids.\nhttps://play.google.com/store/apps/details?id=com.motorola.spaces","removal":"delete","type":"oem"},{"id":"com.motorola.spectrum.setup.extensions","description":"Spectrum Setup\nSpectrum connectivity services. Useless and it's only setup.","removal":"delete","type":"oem"},{"id":"com.motorola.sstservice","description":"Spatial Sound\nProvides sst audio effects.\nI am not sure how to use this app.\nCode is too small, no activities only some frameworks in classes.dex","removal":"replace","type":"oem"},{"id":"com.motorola.systemserver","description":"Moto Service Experience, debugging app.","removal":"delete","type":"oem"},{"id":"com.motorola.systemui.desk","description":"DesktopSystemUI\nit's for desktop maybe when connect to screen phone.","removal":"delete","type":"oem"},{"id":"com.motorola.telprov","description":"It's needed for carrier O2 so it's useless for others.","removal":"replace","type":"oem"},{"id":"com.motorola.thermalservice","description":"Motorola Thermal Service\nTurns off phone when it's too hot.","removal":"caution","type":"oem"},{"id":"com.motorola.timeweatherwidget","label":"Moto Widget","description":"Provides time/weather widget on the home screen.","removal":"delete","type":"oem"},{"id":"com.motorola.timezonedata","description":"/!\\ Causes bootloop on Moto G7 Power (Android 9/10)\nTime Zone Data (https://play.google.com/store/apps/details?id=com.motorola.timezonedata)\nUpdate timezone when traveling to foreign countries.\n","removal":"unsafe","type":"oem"},{"id":"com.motorola.wifi.motowifimetrics","description":"Useless Wi-Fi daily stats.","removal":"delete","type":"oem"},{"id":"com.mygalaxy","label":"My Galaxy","description":"Entertainment hub and life-services application.\nLets you access videos, music and gaming and gives quick access to services such as cabs, movies, recharge, bill payment, food ordering, travel, hyper local deals and Samsung Care, among others.","web":["https://play.google.com/store/apps/details?id=com.mygalaxy"],"removal":"delete","type":"oem"},{"id":"com.nearme.atlas","label":"Secure payment","description":"More info needed.","removal":"delete","type":"oem"},{"id":"com.nearme.browser","label":"Browser","description":"Default web browser","removal":"replace","suggestions":"browsers","type":"oem"},{"id":"com.nearme.deamon","required_by":["com.nearme.statistics.rom"],"description":"Package needed by com.nearme.statistics.rom to run service in background at every boot even though the app has been uninstalled","removal":"delete","type":"oem"},{"id":"com.nearme.gamecenter","description":"Game Center\nActs like QooApp that has a game forum, news, etc. Contains trackers.","removal":"delete","type":"oem"},{"id":"com.nearme.instant.platform","label":"Quick App","description":"A lot of tracking for demo game ads","removal":"delete","type":"oem"},{"id":"com.nearme.romupdate","description":"Update Service\nProbably it's only for notifications. Anyway, better remove it if you want to remove updates too.","removal":"caution","type":"oem"},{"id":"com.nearme.statistics.rom","label":"User Experience Program","description":"Collect user data and sends them to Oppo. Intrusive and starts at boot.","web":["https://support.oppo.com/uk/answer/?aid=neu105","https://beta.pithus.org/report/5e06191ac6f8aefd39642f6341ee4897039815f5059dbe093a7bd2fe1e20c038"],"removal":"caution","warning":"Removing it may break the search feature in the settings on some ColorOS versions.","type":"oem"},{"id":"com.nearme.themespace","description":"Theme store","removal":"delete","type":"oem"},{"id":"com.nearme.themestore","description":"Themes store\n","removal":"delete","type":"oem"},{"id":"com.netflix.ninja","description":"Netflix app\nhttps://play.google.com/store/apps/details?id=com.netflix.ninja&hl=en_US","removal":"delete","type":"oem"},{"id":"com.nothing.OfflineOTAUpgradeApp","description":"Hidden offline OTA update. You can get it by typing in dialer: *#*#682*#*#","removal":"delete","type":"oem"},{"id":"com.nothing.agreement","description":"Nothing agreement privacy policy. I'm not sure when it will be needed.","removal":"delete","type":"oem"},{"id":"com.nothing.applocker","description":"App locker\nOptional app for locking apps.","removal":"delete","type":"oem"},{"id":"com.nothing.appservice","description":"Nothing System Service\nNot sure if you need this.","removal":"caution","type":"oem"},{"id":"com.nothing.bpf","description":"Needed for connection?","removal":"caution","type":"oem"},{"id":"com.nothing.camera","description":"Camera\nNothing stock camera","removal":"replace","type":"oem"},{"id":"com.nothing.cardservice","description":"NothingCardService\nWARNING: breaks Nothing Widgets when switching to dark or light mode.","removal":"replace","type":"oem"},{"id":"com.nothing.dirac","description":"Audio EQ (equalizer). Some 3rd-party music apps can use it to provide you EQ features.","removal":"replace","type":"oem"},{"id":"com.nothing.dirac.DMP","description":"Audio EQ (equalizer). Some 3rd-party music apps can use it to provide you EQ features.","removal":"replace","type":"oem"},{"id":"com.nothing.enginnerservice","description":"Hidden diagnostics without activity frameworks with Qualcomm.","removal":"delete","type":"oem"},{"id":"com.nothing.experience","description":"Useless frameworks and collection data.","removal":"delete","type":"oem"},{"id":"com.nothing.experimental","description":"Experimental features like CONNECT TO TESLA","removal":"delete","type":"oem"},{"id":"com.nothing.glyphnotification","description":"Needed for glyph notifications?","removal":"caution","type":"oem"},{"id":"com.nothing.hearthstone","description":"Nothing Widgets\nWidgets that can be found in homescreen.","removal":"replace","type":"oem"},{"id":"com.nothing.launcher","description":"Launcher\nStock launcher","removal":"caution","type":"oem"},{"id":"com.nothing.launcher.overlay.config","description":"Useless overlay to recent activity","removal":"delete","type":"oem"},{"id":"com.nothing.logkit","description":"Hidden tool for logs.","removal":"delete","type":"oem"},{"id":"com.nothing.proxy","description":"It's for battery optimization","removal":"caution","type":"oem"},{"id":"com.nothing.smartcenter","description":"Nothing X\nIt's something for earphones and testing.","removal":"delete","type":"oem"},{"id":"com.nothing.soundrecorder","description":"Sound recorder\nSound recorder app.","removal":"replace","type":"oem"},{"id":"com.nothing.systemuitool","description":"I think it's an important app","removal":"unsafe","type":"oem"},{"id":"com.nothing.thirdparty","description":"Breaks the Glyph API, Composer and other apps that rely on the Glyph feature will not work anymore.","removal":"caution","type":"oem"},{"id":"com.nothing.wallpapersstub","description":"Needed for basic wallpapers","removal":"replace","type":"oem"},{"id":"com.nothing.weather","description":"Weather\nNothing Weather","removal":"delete","type":"oem"},{"id":"com.novatek.novavis","description":"Debugs, broadcast id?","removal":"replace","type":"oem"},{"id":"com.nt.android.overlay.gmsconfig.safetycenter","description":"Useless overlay to nothing gmsconfig safetycenter","removal":"delete","type":"oem"},{"id":"com.nt.android.overlay.gmsconfig.settings","description":"Useless overlay to nothing gmsconfig settings","removal":"delete","type":"oem"},{"id":"com.nt.android.overlay.gmsconfig.settingsprovider","description":"Useless overlay to nothing gmsconfig settingsprovider","removal":"delete","type":"oem"},{"id":"com.nt.diagswitch","description":"Hidden USB Diag Switch, adb diagnostics.","removal":"delete","type":"oem"},{"id":"com.nt.facerecognition","description":"Needed for face recognition.","removal":"delete","type":"oem"},{"id":"com.nt.grantpermission","description":"Hidden grant permissions. You can get it by typing in dialer: *#*#3424*#*#","removal":"delete","type":"oem"},{"id":"com.nt.ledlighttest","description":"Led Light Test\nYou can get it by typing in dialer: *#*#533*#*#","removal":"delete","type":"oem"},{"id":"com.nt36xxxtouchscreen.deltadiff","description":"Audio test frequency(KHz) to com.vivo.bsptest.","removal":"delete","type":"oem"},{"id":"com.nttdocomo.android.felicaremotelock","description":"FeliCa Remote security things, not needed","removal":"delete","type":"oem"},{"id":"com.nttouchscreen.getdata","description":"Testing things to com.vivo.bsptest.","removal":"delete","type":"oem"},{"id":"com.nttouchscreen.mptest","description":"Novatek MP selftest\nTesting things to com.vivo.bsptest.","removal":"delete","type":"oem"},{"id":"com.nuance.swype.emui","description":"Huawei Swype functions.\nIs it the full Swype keyboard or only the Swype function on Huawei keyboard ? \nNOTE : Nuance company said it would discontinue support of the Swype keyboard app.\n","removal":"delete","type":"oem"},{"id":"com.oem.autotest","description":"Auto Test Server\nUsed to test the hardware of your device and change hidden settings.\n","removal":"delete","type":"oem"},{"id":"com.oem.euiccpartnerapp.overlay.retca","description":"This package is related to the eUICC (embedded Universal Integrated Circuit Card) partner application, specifically an OEM overlay that deals with eSIM functionality and management.","removal":"caution","type":"oem"},{"id":"com.oem.logkitsdservice","description":"Used by com.oem.oemlogkit, a shady logging app.\nDoesn't run by default, but can easily be triggered by system apps.","removal":"delete","type":"oem"},{"id":"com.oem.nfc","description":"OnePlus NFC tester\n","removal":"delete","type":"oem"},{"id":"com.oem.oemlogkit","label":"OnePlusLogKit","description":"Shady logging app that system apps can use to log WiFi traffic, Bluetooth traffic, NFC activity, GPS coordinates over time, power consumption, modem signal/data details, \"lag issues,\" and more.","web":["https://thehackernews.com/2017/11/oneplus-logkit-app.html","https://www.bleepingcomputer.com/news/security/second-oneplus-factory-app-discovered-this-one-dumps-photos-wifi-and-gps-logs/","https://web.archive.org/web/20210611122551/https://twitter.com/fs0c131y/status/930773795656396801"],"removal":"delete","type":"oem"},{"id":"com.oneplus","description":"Oneplus System\nHandles the Oneplus system framework? Possibly unsafe to disable, but please contribute information about what happens if you do.","removal":"caution","type":"oem"},{"id":"com.oneplus.accessory","description":"Oneplus Link\nI'm guessing this has to do with connecting to Oneplus accessories, like the Oneplus Buds (wireless earbuds). Might wanna keep it enabled if you use oneplus accessory devices.\nNoticed no negative effects from disable after weeks of use.","removal":"delete","type":"oem"},{"id":"com.oneplus.account","label":"OnePlus Account","description":"Enables Oneplus account login on device.\nProbably handles authentication for Oneplus apps.\n","web":["https://play.google.com/store/apps/details?id=com.oneplus.account"],"removal":"delete","type":"oem"},{"id":"com.oneplus.account.basiccolorblack.overlay","description":"Dark theme for Oneplus Account?","removal":"caution","type":"oem"},{"id":"com.oneplus.account.basiccolorwhite.overlay","description":"Light theme for Oneplus Account?","removal":"caution","type":"oem"},{"id":"com.oneplus.android.cellbroadcast.overlay","description":"Wireless emergency alerts Theme pack\nGuessing it's a pack of themes for the emergency alert UI, based on the name.","removal":"caution","type":"oem"},{"id":"com.oneplus.aod","description":"Always On Display / Ambient Display\nRuns in the background.\nWhen enabled in settings it shows clock and notifications when you raise the phone or touch the screen.\nThis is basically a lower-power lock-screen. It could in theory reduce power draw if you check notifications/clock often as OLED screens draw minimal power showing a mostly black screen(black = pixel off), but in practice the number of times you'll unintentionally trigger it will likely eat up any potential power savings and more. And if your device doesn't have an OLED screen this will draw way more power.\nMost of these power savings could be applied to your standard lock-screen simply by making your background image completely black.","removal":"delete","type":"oem"},{"id":"com.oneplus.aod.basiccolorblack.overlay","description":"Theme overlay for AOD? (Always On Display)","removal":"caution","type":"oem"},{"id":"com.oneplus.aod.basiccolorwhite.overlay","description":"Theme overlay for AOD? (Always On Display)","removal":"caution","type":"oem"},{"id":"com.oneplus.aodnotification.overlay.gold","description":"AoD notification gold","removal":"delete","type":"oem"},{"id":"com.oneplus.aodnotification.overlay.purple","description":"AoD notification purple","removal":"delete","type":"oem"},{"id":"com.oneplus.aodnotification.overlay.red","description":"AoD notification red","removal":"delete","type":"oem"},{"id":"com.oneplus.applocker","description":"Encrypts and locks apps behind password access.","removal":"caution","type":"oem"},{"id":"com.oneplus.appupgrader","description":"Built-in App Updates\nBased on the name I'm guessing it's an upater for built-in Oneplus apps?\nSeems safe to disable, but only seems to run on boot, so there's little to be gained from disabling.","removal":"delete","type":"oem"},{"id":"com.oneplus.asti","label":"OPAI","description":"App Prediction Service, SarahService, bindsarah\nIts code is too difficult to understand, but it looks like AI training.","removal":"caution","type":"oem"},{"id":"com.oneplus.backuprestore","label":"Clone Phone","description":"Lets you migrate contacts, text messages, photos, and other data from one device to another.\nCan also backup data as a compressed archive.\n","web":["https://play.google.com/store/apps/details?id=com.oneplus.backuprestore"],"removal":"replace","suggestions":"backup_apps","type":"oem"},{"id":"com.oneplus.backuprestore.remoteservice","description":"Likely a backend service for OnePlus Switch(com.oneplus.backuprestore).\nI've never seen it run in the background.\nProbably safe to disable.","removal":"delete","type":"oem"},{"id":"com.oneplus.brickmode","label":"OnePlus Zen Mode","description":"Zen Mode helps you put down your phone and enjoy your life.\nIn Zen Mode you will only be able to take photos and answer calls.","web":["https://play.google.com/store/apps/details?id=com.oneplus.brickmode"],"removal":"delete","type":"oem"},{"id":"com.oneplus.bttestmode","label":"OnePlus Bluetooth test mode","description":"Type *#*#232339#*#* in the OnePlus dialer to access this hidden test menu.","removal":"delete","type":"oem"},{"id":"com.oneplus.calculator","label":"Calculator","description":"Stock Oneplus Calculator app.","removal":"replace","suggestions":"calculators","type":"oem"},{"id":"com.oneplus.calculator.basiccolorblack.overlay","description":"Theme overlay for Oneplus Calculator app?","removal":"caution","type":"oem"},{"id":"com.oneplus.calendar.black.overlay","description":"Theme overlay for stock Calendar app?","removal":"caution","type":"oem"},{"id":"com.oneplus.calendar.white.overlay","description":"Theme overlay for stock Calendar app?","removal":"caution","type":"oem"},{"id":"com.oneplus.camera","description":"Camera\nThe stock Oneplus camera app.\n","removal":"replace","suggestions":"cameras","type":"oem"},{"id":"com.oneplus.camera.pictureprocessing","description":"Is it for image processing?","removal":"caution","type":"oem"},{"id":"com.oneplus.camera.service","label":"OnePlus Camera Service","description":"Runs in the background on some phones.\nNot sure what it does; camera functions fine without it. Could be related to photo backup?","removal":"caution","warning":"may cause a bootloop when using a custom ROM on Android 14 (in this case, Nameless AOSP). Does not affect users running OxygenOS.","type":"oem"},{"id":"com.oneplus.card","description":"Card Package\nWidget which lets you add membership card in Shelf.\nYou enter numbers for a club card or something and it'll store it and generate a barcode for you.\nShelf is a page on your home screen that allows you to take memos, add widgets, gain access to your most-used apps, and get a quick glimpse of the weather. Swipe right (from the left edge of your home screen) to reveal it.","removal":"delete","type":"oem"},{"id":"com.oneplus.card.black.overlay","description":"Theme overlay for Oneplus Card package?","removal":"caution","type":"oem"},{"id":"com.oneplus.card.white.overlay","description":"Theme overlay for Oneplus Card package?","removal":"caution","type":"oem"},{"id":"com.oneplus.carrierlocation","description":"Carrier Location Access\nRuns on boot, but not in the background beyond that.\nNot sure what this does. Could be related to detecting region to determine which radio frequencies to use?\nNoticed no ill effects from weeks of having it disabled.","removal":"replace","type":"oem"},{"id":"com.oneplus.chargingpilar","label":"Nearby Charging Stations","description":"Geolocates the phone to find OnePlus charging stations nearby. Connects to 'gateway.oneplus.net'.","web":["https://beta.pithus.org/report/8c157eeec2931d3d1140aa8c452d7afa570e04c9d51e6cd5987dbb3ec43df4f9"],"removal":"delete","type":"oem"},{"id":"com.oneplus.cloud.basiccolorblack.overlay","description":"Theme overlay for some Oneplus Cloud thing?","removal":"caution","type":"oem"},{"id":"com.oneplus.cloud.basiccolorwhite.overlay","description":"Theme overlay for some Oneplus Cloud thing?","removal":"caution","type":"oem"},{"id":"com.oneplus.collectiondata","description":"Collection data to telephony?","removal":"caution","type":"oem"},{"id":"com.oneplus.commonoverlay.android","description":"Needed for notifications? Status_bar?","removal":"caution","type":"oem"},{"id":"com.oneplus.commonoverlay.com.android.networkstack.inprocess","description":"Needed for DHCP hostname?, captive portal HTTP URLs?","removal":"caution","type":"oem"},{"id":"com.oneplus.commonoverlay.com.android.networkstack.inprocess.cn","description":"Needed for DHCP hostname?, captive portal HTTP URLs? Generate_204 Chinese Google?","removal":"caution","type":"oem"},{"id":"com.oneplus.commonoverlay.com.android.systemui","description":"Needed for wellbeing or notifications?","removal":"caution","type":"oem"},{"id":"com.oneplus.commonoverlay.com.android.wifi.resources","description":"Wi-Fi configs?","removal":"caution","type":"oem"},{"id":"com.oneplus.commonoverlay.com.google.android.networkstack","description":"Needed for DHCP hostname?, captive portal HTTP URLs?","removal":"caution","type":"oem"},{"id":"com.oneplus.commonoverlay.com.google.android.networkstack.cn","description":"Needed for DHCP hostname?, captive portal HTTP URLs? Generate_204 Chinese Google?","removal":"caution","type":"oem"},{"id":"com.oneplus.commonoverlay.com.oneplus","description":"Testing things, debugging","removal":"delete","type":"oem"},{"id":"com.oneplus.communication.data","description":"Oneplus call recorder service. Feature accessible from the stock dialer app.\n","removal":"replace","suggestions":"call_recorders","type":"oem"},{"id":"com.oneplus.config","label":"OPConfig","description":"Occasionally runs in the background.\nGuessing it might handle communication certificates and general network config for Oneplus apps.\nOnly has INTERNET and RECEIVE_BOOT_COMPLETED permissions.\n'Tips & Support' will be removed from settings.","removal":"caution","type":"oem"},{"id":"com.oneplus.contacts","label":"OnePlus Contacts","description":"The default contacts app in OnePlus","removal":"replace","suggestions":"contacts","type":"oem"},{"id":"com.oneplus.contacts.basiccolorblack.overlay","description":"Theme overlay for Oneplus Contacts?","removal":"replace","type":"oem"},{"id":"com.oneplus.contacts.basiccolorwhite.overlay","description":"Theme overlay for Oneplus Contacts?","removal":"replace","type":"oem"},{"id":"com.oneplus.coreservice","description":"Android System\nImportant system package for Oneplus phones?\nRuns in the background as part of the system.\nContains broadcast dispatch and theme handler services.\nProbably unsafe to disable, but please contribute info about what happens if you do.","removal":"caution","type":"oem"},{"id":"com.oneplus.cota","description":"Carrier Update\nRuns in the background.\ncota = Carrier OTA. Handles carrier-specific OTA updates? Probably safe to disable if you didn't get your phone from a carrier; the normal System Update(com.oneplus.opbackup) should handle the OTA updates. I can confirm that I got an OTA notification even with this disabled.","removal":"caution","type":"oem"},{"id":"com.oneplus.dataoptimization","description":"OPDataOptimization\nDoesn't contain any services and I've never seen it run.","removal":"caution","type":"oem"},{"id":"com.oneplus.deskclock","label":"Clock","description":"Clock\nThe stock Oneplus clock app.","removal":"replace","type":"oem"},{"id":"com.oneplus.deskclock.black.overlay","description":"Theme overlay for Oneplus Clock app?","removal":"caution","type":"oem"},{"id":"com.oneplus.deskclock.white.overlay","description":"Theme overlay for Oneplus Clock app?","removal":"caution","type":"oem"},{"id":"com.oneplus.diagnosemanager","description":"Logging app for diagnosis/troubleshooting when the SIM card state change. Only used on devices running OxygenOS 9 and lower. Runs at boot and triggers when SIM card state change.\n\nPithus analysis: https://beta.pithus.org/report/f4c76054795bf55012edf1f60e992b6e339085b9ca2cbe685917a62dd07492c0","removal":"replace","type":"oem"},{"id":"com.oneplus.dialer","description":"OnePlus Dialer used in OxygenOS 11 and lower.\nNote: don't forget to download another phone dialer app before removing this package.\n","removal":"replace","suggestions":"dialers","type":"oem"},{"id":"com.oneplus.dirac.simplemanager","description":"Runs in the background.\nMain Dirac service.\nAudio fidelity improvement from the Swedish company Dirac.\nAttempts to achieve a flat frequency response curve(i.e: fidelity). Should mainly improve speaker fidelity as it can be pre-calculated and stored as a corrective EQ curve, something not possible for most devices connected through the 3.5mm jack; presets only exist for a very limited number of headphones. Change for non-preset 3.5mm jack devices is just a generic EQ curve that could decrease fidelity just as likely as it could increase it.","removal":"replace","type":"oem"},{"id":"com.oneplus.dirac.simplemanager.basiccolorblack.overlay","description":"Theme overlay for Dirac?","removal":"caution","type":"oem"},{"id":"com.oneplus.dirac.simplemanager.basiccolorwhite.overlay","description":"Theme overlay for Dirac?","removal":"caution","type":"oem"},{"id":"com.oneplus.dm","description":"Subscriber Device Management\nHas only privacy online agreement activity.","removal":"delete","type":"oem"},{"id":"com.oneplus.engmode","description":"Some kind of Engineer mode? I've no clue.\nContains a bunch of activities with \"info\" in their names.\nContains an \"OpFloatViewService\", but I've never seen it run.","removal":"caution","type":"oem"},{"id":"com.oneplus.faceunlock","description":"Face Unlock\nRuns in the background as part of the system.\nUnlock your device by simply looking at the display.\nFace unlock is bad for security and privacy:\nhttps://www.ubergizmo.com/2017/03/galaxy-s8-facial-unlock-photograph/\nhttps://www.kaspersky.com/blog/face-unlock-insecurity/21618/\nhttps://www.freecodecamp.org/news/why-you-should-never-unlock-your-phone-with-your-face-79c07772a28/","removal":"delete","type":"oem"},{"id":"com.oneplus.factorymode","label":"FactoryMode","description":"Used in the factory to test devices.\nType *#808# in the OnePlus dialer to access the hidden menu.\nPotential security risk: It's possible for an app to enable root access on any device with the APK pre-installed.\nFor now, this only works in ADB, which requires local access to the device.","web":["https://web.archive.org/web/20211103134620/https://twitter.com/fs0c131y/status/930115188988182531"],"removal":"delete","type":"oem"},{"id":"com.oneplus.factorymode.specialtest","description":"Engineering Mode Special Test\nUsed in the factory to test devices.\nSee com.oneplus.factorymode","removal":"delete","type":"oem"},{"id":"com.oneplus.filemanager","label":"OnePlus File Manager","description":"Stock OnePlus file manager app.\n","web":["https://play.google.com/store/apps/details?id=com.oneplus.filemanager"],"removal":"replace","suggestions":"file_managers","type":"oem"},{"id":"com.oneplus.filemanager.black.overlay","description":"Theme overlay for Oneplus File manager app?","removal":"delete","type":"oem"},{"id":"com.oneplus.filemanager.white.overlay","description":"Theme overlay for Oneplus File manager app?","removal":"delete","type":"oem"},{"id":"com.oneplus.gallery","label":"OnePlus Gallery","description":"Occasionally runs in the background. Some old versions of the app (like for Oneplus 3 on Android 9) don't run in the background.\n","web":["https://play.google.com/store/apps/details?id=com.oneplus.gallery"],"removal":"replace","suggestions":"gallery","type":"oem"},{"id":"com.oneplus.gamespace","label":"OnePlus Games","dependencies":["com.oplus.cosa"],"description":"Occasionally runs in the background as part of the system.\nAllows you to launch your game library, check game stats(such as playtime), activate game overlay features, change performance settings to tweak game/battery performance during gaming.\nThis is the only way to access the recording buffer functionality (records the last X seconds into RAM and saves them when you tap save), so keep enabled if you need that or any of the other features.\n","web":["https://play.google.com/store/apps/details?id=com.oneplus.gamespace"],"removal":"delete","type":"oem"},{"id":"com.oneplus.gamespace.black.overlay","description":"Theme overlay for Game Space?","removal":"caution","type":"oem"},{"id":"com.oneplus.gamespace.white.overlay","description":"Theme overlay for Game Space?","removal":"caution","type":"oem"},{"id":"com.oneplus.geoiptime","description":"Sets the Timezone (it is not an NTP client). Automatically starts at boot and connects to `checkip.amazonaws.com` and `gateway.oneplus.com`.\n\nPithus analysis: https://beta.pithus.org/report/5e375a6b8da588a1490e42266f4e33975ce73207d79755a109101bd5fb07cc7c","removal":"caution","type":"oem"},{"id":"com.oneplus.iconpack.circle","description":"OnePlus Icon Pack - Round (https://play.google.com/store/apps/details?id=com.oneplus.iconpack.circle)\n","removal":"replace","type":"oem"},{"id":"com.oneplus.iconpack.oneplus","description":"OnePlus Icon Pack (https://play.google.com/store/apps/details?id=com.oneplus.iconpack.oneplus)\n","removal":"replace","type":"oem"},{"id":"com.oneplus.iconpack.oneplush2","description":"OnePlus Hydrogen Icon Pack","removal":"delete","type":"oem"},{"id":"com.oneplus.iconpack.onepluso2","description":"OnePlus Oxygen Icon Pack","removal":"delete","type":"oem"},{"id":"com.oneplus.iconpack.square","description":"OnePlus Icon Pack - Square (https://play.google.com/store/apps/details?id=com.oneplus.iconpack.square)\n","removal":"replace","type":"oem"},{"id":"com.oneplus.ifaaservice","description":"IFAA = (China’s) Internet Finance Authentication Alliance\nProvides biometric authentication for Alipay. Safe to disable if you don't use it.","removal":"delete","type":"oem"},{"id":"com.oneplus.membership","description":"Red Cable Club\nBattery drain if account added.\nhttps://play.google.com/store/apps/details?id=com.oneplus.membership","removal":"delete","type":"oem"},{"id":"com.oneplus.minidumpoptimization","description":"OPMinidumpOptimization\nRuns in the background.\nNot sure what it does, but haven't noticed any negative effects from weeks of having it disabled.","removal":"delete","type":"oem"},{"id":"com.oneplus.mms","label":"OnePlus Messages","description":"Only used on OnePlus 8 / 8 Pro according to the description.\n","web":["https://play.google.com/store/apps/details?id=com.oneplus.mms"],"removal":"replace","warning":"Make sure to install another SMS app to not lose that functionality","suggestions":"sms","type":"oem"},{"id":"com.oneplus.mms.basiccolorblack.overlay","description":"Dark theme overlay for Oneplus Messages app?","removal":"caution","type":"oem"},{"id":"com.oneplus.mms.basiccolorwhite.overlay","description":"Light theme overlay for Oneplus Messages app?","removal":"caution","type":"oem"},{"id":"com.oneplus.note","label":"OnePlus Notes","description":"OnePlus Notes app\n","web":["https://play.google.com/store/apps/details?id=com.oneplus.note"],"removal":"replace","suggestions":"note_taking_apps","type":"oem"},{"id":"com.oneplus.note.black.overlay","description":"Dark theme overlay for Oneplus Notes app?","removal":"delete","type":"oem"},{"id":"com.oneplus.note.white.overlay","description":"Light theme overlay for Oneplus Notes app?","removal":"delete","type":"oem"},{"id":"com.oneplus.nroptimization","description":"Optimization telephony?","removal":"caution","type":"oem"},{"id":"com.oneplus.odmoverlay.android","description":"Android System Theme pack\nGuessing it's a pack of themes for some Oneplus-specific system components, based on the name.","removal":"caution","type":"oem"},{"id":"com.oneplus.odmoverlay.com.android.settings","description":"Settings Theme pack\nGuessing it's a pack of themes for the settings app, based on the name.","removal":"caution","type":"oem"},{"id":"com.oneplus.odmoverlay.com.android.systemui","description":"System UI Theme pack\nGuessing it's a pack of themes for some Oneplus-specific system component, based on the name.","removal":"caution","type":"oem"},{"id":"com.oneplus.odmoverlay.com.oneplus","description":"Oneplus System Theme pack\nGuessing it's a pack of themes for Oneplus-specific system components, based on the name.","removal":"caution","type":"oem"},{"id":"com.oneplus.opbackup","description":"System Update\nRuns in the background.\nHandles things related to OTA system updates.\nSafe to disable, but probably breaks system updates.","removal":"caution","type":"oem"},{"id":"com.oneplus.opbackup.black.overlay","description":"Theme overlay for System Update?","removal":"caution","type":"oem"},{"id":"com.oneplus.opbackup.white.overlay","description":"Theme overlay for System Update?","removal":"caution","type":"oem"},{"id":"com.oneplus.opbugreportlite","label":"BugReportLite","description":"Runs in the background. Runs as part of the system, even if disabled? Disabling does remove all RAM usage tho, and hopefully removes access to user data, as disable/uninstall should sever the connection to the Android user account.\nSilently sends, every 6 hours, battery stats, kernel panics, watchdogs, ANRs and all crashes of your device to Singapore.","web":["https://www.androidpit.com/oneplus-opbugreportlite-data-collection","https://web.archive.org/web/20220520051328/https://twitter.com/fs0c131y/status/933037531066785797"],"removal":"delete","type":"oem"},{"id":"com.oneplus.opshelf","description":"OnePlus Shelf (https://play.google.com/store/apps/details?id=com.oneplus.opshelf)\nWidget panel accessible from swiping down on the top-right side of the screen allowing quick access to apps, meteo, spotify, memos...\n\nPithus analysis: https://beta.pithus.org/report/a50f166c8c2fae1204650c7af1cb287e20ad5286a89b013ada787f4b1b90fc64.","removal":"delete","type":"oem"},{"id":"com.oneplus.opsports","description":"Cricket Scores (https://play.google.com/store/apps/details?id=com.oneplus.opsports)\nLets you access and follow cricket teams and tournaments.","removal":"delete","type":"oem"},{"id":"com.oneplus.opwlb","description":"Work-Life Balance\nNot present in most Oneplus phones? This functionality might have been superseded by other similar apps, like for example Zen Mode.\nHaven't tested, but probably safe to disable.","removal":"delete","type":"oem"},{"id":"com.oneplus.opwlb.black.overlay","description":"Theme overlay for Oneplus Work-Life Balance?","removal":"caution","type":"oem"},{"id":"com.oneplus.opwlb.white.overlay","description":"Theme overlay for Oneplus Work-Life Balance?","removal":"caution","type":"oem"},{"id":"com.oneplus.orm","description":"Seems to be Oneplus' Memory Management System according to a press-kit/-release they made for Android 11 (multiple sites wrote \"ORM Memory Management System\" word-for-word).\nRuns in the background as part of the system. Runs even if disabled? Doesn't use any RAM when disabled tho, vs ~50MB when enabled.\nSeems safe to disable, haven't noticed any negative effects in weeks of use, but I assume it breaks the \"RAM Boost\" feature (which is pointless anyway IMO).\nApk file name: OPOmm, mm = Memory Management?\nHas 2 permissions: KILL_BACKGROUND_PROCESSES and SET_TIME_ZONE.\nContains 2 services: OPManagerService and BackgroundCollectorService.","removal":"replace","type":"oem"},{"id":"com.oneplus.overlay","description":"","removal":"caution","type":"oem"},{"id":"com.oneplus.productoverlay.android","description":"Android System Theme pack\nGuessing it's a pack of themes for some Android System component, based on the name.","removal":"caution","type":"oem"},{"id":"com.oneplus.productoverlay.com.android.providers.settings","description":"Settings Storage Theme pack\nGuessing it's a pack of themes for Settings Storage, based on the name.","removal":"caution","type":"oem"},{"id":"com.oneplus.productoverlay.com.oneplus","description":"Oneplus System Theme pack\nGuessing it's a pack of themes for Oneplus-specific system components, based on the name.","removal":"caution","type":"oem"},{"id":"com.oneplus.providers.media","description":"OnePlus Media Storage\nRuns in the background.\nSeems to just add recycle bin functionality to your file management (file browsers). Keep enabled if you like that function. But safe to disable if you don't want it.","removal":"delete","type":"oem"},{"id":"com.oneplus.screenrecord","description":"Screen Recorder\nThe Android 11 screen recorder with some Oneplus modifications.\nRuns the \"SystemUITileService\" when you have it as one of the quicksettings tiles, but doesn't seem to run in the background outside of that.\nDoesn't have an app icon, but you can create a shortcut to it with the Activity Launcher app (to avoid the background service).\nhttps://f-droid.org/en/packages/de.szalkowski.activitylauncher/","removal":"replace","suggestions":"screen_recorders","type":"oem"},{"id":"com.oneplus.screenrecord.black.overlay","description":"Theme overlay for Oneplus Screenrecord?","removal":"delete","type":"oem"},{"id":"com.oneplus.screenrecord.white.overlay","description":"Theme overlay for Oneplus Screenrecord?","removal":"delete","type":"oem"},{"id":"com.oneplus.screenshot","label":"Screenshot","description":"Needed for Power + Volume Down screenshot.","removal":"caution","type":"oem"},{"id":"com.oneplus.sdcardservice","description":"Needed for sdcard?","removal":"caution","type":"oem"},{"id":"com.oneplus.security","description":"Dashboard\nRuns \"WidgetViewService\" and \"SecureService\" in the background.\nManages widget data access? Noticed no apparent ill effects on disable in Android 9.","removal":"replace","type":"oem"},{"id":"com.oneplus.security.black.overlay","description":"Dark theme overlay for com.oneplus.security?","removal":"caution","type":"oem"},{"id":"com.oneplus.security.white.overlay","description":"Light theme overlay for com.oneplus.security?","removal":"caution","type":"oem"},{"id":"com.oneplus.ses","description":"OPSes\nApk file name: OPSesAuthentication.\nContains a \"SesService\", but I've never seen it run.\nRelated to Amazon SES?(Simple Email Service) https://aws.amazon.com/ses/","removal":"replace","type":"oem"},{"id":"com.oneplus.setupwizard","description":"The Oneplus portion of the first-boot setup.\nRuns on boot, but not in the background beyond that.","removal":"delete","type":"oem"},{"id":"com.oneplus.simcontacts","description":"SimContacts Manager\nRuns in the background. Manages contacts and sync to SIM? Noticed no apparent ill effects on disable in Android 9.","removal":"delete","type":"oem"},{"id":"com.oneplus.simcontacts.basiccolorblack.overlay","description":"Dark theme overlay for Oneplus SimContacts Manager app?","removal":"caution","type":"oem"},{"id":"com.oneplus.simcontacts.basiccolorwhite.overlay","description":"Light theme overlay for Oneplus SimContacts Manager app?","removal":"caution","type":"oem"},{"id":"com.oneplus.skin","description":"","removal":"caution","type":"oem"},{"id":"com.oneplus.sms.smscplugger","description":"Probably related to SMS based on the name?\nContains no services and I've never seen it run.","removal":"caution","type":"oem"},{"id":"com.oneplus.sound.tuner","description":"Dolby Atmos\nRuns in the background as part of the system. Runs even if disabled.\nSound tuning for Atmos. Breaks the Dolby Atmos sound settings menu if disabled.\nCould in theory increase loudspeaker fidelity as it can be pre-calculated and stored as a corrective EQ curve, something not possible for headphones (they'd need a unique preset for each pair of headphones).","removal":"replace","type":"oem"},{"id":"com.oneplus.soundrecorder","description":"Recorder\nOnePlus sound recording app.\nRequires turning on \"Allow modifying system settings\" to use for some reason? Probably tied to recording phone-calls.","removal":"replace","suggestions":"audio_recorders","type":"oem"},{"id":"com.oneplus.soundrecorder.black.overlay","description":"Theme overlay for Soundrecorder app?","removal":"delete","type":"oem"},{"id":"com.oneplus.soundrecorder.white.overlay","description":"Theme overlay for Soundrecorder app?","removal":"delete","type":"oem"},{"id":"com.oneplus.store","description":"OnePlus Store\nOffers getting favorites OnePlus phones in good price.\nhttps://play.google.com/store/apps/details?id=com.oneplus.store","removal":"delete","type":"oem"},{"id":"com.oneplus.telephonyoptimization","description":"OPTelephonyOptimization\nContains a service with the same name, but I've never seen it run.","removal":"caution","type":"oem"},{"id":"com.oneplus.twspods","description":"OnePlus Buds (https://play.google.com/store/apps/details?id=com.oneplus.twspods)\nCompanion app for Oneplus Buds. For updating firmware and changing settings.","removal":"delete","type":"oem"},{"id":"com.oneplus.vendoroverlay.android","description":"Overlay to extra kbytes adjust?","removal":"caution","type":"oem"},{"id":"com.oneplus.vendoroverlay.com.android.providers.settings","description":"Default backup transport Google. Not needed for backup","removal":"delete","type":"oem"},{"id":"com.oneplus.vendoroverlay.com.android.systemui","description":"It has nothing in the code.","removal":"caution","type":"oem"},{"id":"com.oneplus.vendoroverlay.com.oneplus","description":"Config to overheat?","removal":"caution","type":"oem"},{"id":"com.oneplus.vendoroverlay.com.oneplus.wifiapsettings","description":"OP_white_mode?","removal":"caution","type":"oem"},{"id":"com.oneplus.wallpaper","description":"Pack of live wallpapers from Oneplus.","removal":"replace","type":"oem"},{"id":"com.oneplus.wifiapsettings","description":"Wi-Fi Access Point Settings?\nRuns on boot.\nNoticed no change after disabling; Wi-Fi and related menus still seem fully functional.","removal":"replace","type":"oem"},{"id":"com.oneplus.wifiapsettings.basiccolorblack.overlay","description":"Dark theme for Wi-Fi Access Point Settings?","removal":"caution","type":"oem"},{"id":"com.oneplus.wifiapsettings.basiccolorwhite.overlay","description":"Light theme for Wi-Fi Access Point Settings?","removal":"caution","type":"oem"},{"id":"com.onyx.aiassistant","description":"Onyx Boox AI Assistant. Safe to remove","removal":"delete","type":"oem"},{"id":"com.onyx.android.ksync","description":"Ksync app. UNSAFE to remove because it will crash the Onyx launcher but you can safely disable it","removal":"unsafe","type":"oem"},{"id":"com.onyx.android.production.test","description":"Test app enabled by clicking 5 times on the Onyx Version, it contains tests for screen quality","removal":"delete","type":"oem"},{"id":"com.onyx.appmarket","description":"The Onyx Store app, you will not have any other way to install app if you uninstall it\nYou will need to install an alternative store via adb","removal":"caution","type":"oem"},{"id":"com.onyx.calculator","description":"The Onyx calculator app","removal":"delete","type":"oem"},{"id":"com.onyx.clock","description":"The Onyx Clock app","removal":"delete","type":"oem"},{"id":"com.onyx.dict","description":"The Onyx Dict app","removal":"delete","type":"oem"},{"id":"com.onyx.easytransfer","description":"Onyx Easy Transfer app","removal":"delete","type":"oem"},{"id":"com.onyx.gallery","description":"Onyx Gallery app","removal":"delete","type":"oem"},{"id":"com.onyx.igetshop","description":"Onyx iGetShot app, UNSAFE to remove because it will crash the Onyx Launcher but safe to DISABLE","removal":"unsafe","type":"oem"},{"id":"com.onyx.kime","description":"Onyx Kime app","removal":"delete","type":"oem"},{"id":"com.onyx.kreader","description":"Onyx KReader app, UNSAFE to remove because it will crash the Onyx Launcher can be safely disabled.\nRemember to load another reader app","removal":"unsafe","type":"oem"},{"id":"com.onyx.latinime","description":"The Onyx Keyboard. Removing it will leave withe google STT keyboard.","removal":"delete","type":"oem"},{"id":"com.onyx.mail","description":"Onyx Mail app","removal":"delete","type":"oem"},{"id":"com.onyx.musicplayer","description":"Onyx Music Player app","removal":"delete","type":"oem"},{"id":"com.onyx.voicerecorder","description":"Onyx Voice Recorder app","removal":"delete","type":"oem"},{"id":"com.oplus.account","description":"Account Center. Required for most OnePlus features. Removal gets rid of Login with OnePlus Account notification.","removal":"replace","type":"oem"},{"id":"com.oplus.aiunit","description":"AIUnit\nSystem service related to the intelligence function. It's safe to remove but also breaks the Smart Cutout, AI Editor, and potentially other gallery features.","removal":"replace","type":"oem"},{"id":"com.oplus.android.overlay.gmsconfig.common","description":"Overlay to vendor required apps?\nTheres google apps and some not important oppo apps on the lists, not needed.","removal":"delete","type":"oem"},{"id":"com.oplus.android.overlay.modules.documentsui","description":"Useless overlay to documentsui bools.xml: is_launcher_enabled true","removal":"delete","type":"oem"},{"id":"com.oplus.aod","label":"Aod","description":"Multiple sources say that AOD doesnt work without tons of extra services, but on some devices it works without extra services","web":["https://droidwin.com/remove-bloatware-debloat-oneplus-10-pro-no-root/#OnePlus_10_Pro_List_of_Bloatware_Apps"],"removal":"delete","type":"oem"},{"id":"com.oplus.appdetail","description":"Secure app installation","removal":"delete","type":"oem"},{"id":"com.oplus.appplatform","label":"App Services","description":"Might be renamed package of com.heytap.appplatform which is related to Oppo's Heytap account services. Provides a RomUpdateService. Probably not safe to remove.","web":["https://beta.pithus.org/report/2025ceb69d9379a01771de71ff00051eb0f0c7f44226a72c2066db9649b6dcd2"],"removal":"unsafe","warning":"May cause a bootloop on some phones if removed.","type":"oem"},{"id":"com.oplus.apprecover","label":"Recover system apps","description":"Provides the ability to restore system apps through settings.","removal":"delete","type":"oem"},{"id":"com.oplus.athena","label":"Athena","description":"OnePlus background process manager. Removing it will solve the notification delay you can have but will disable the virtual ram expansion feature (swap RAM to disk) and the 'close all' button in the 'recent apps' page.\nRemoving this app may deteriorate battery performance.","removal":"caution","type":"oem"},{"id":"com.oplus.atlas","label":"atlasService","description":"Separate app sound and individual app volumes.","web":["https://beta.pithus.org/report/6d0f9433431cd34a8e9aaef99b329b3623118a1699033be36032f64653dab3d0"],"removal":"caution","type":"oem"},{"id":"com.oplus.audio.effectcenter","description":"AudioEffectCenter\nDolby support for Chinese apps and some Google. It's so bloated.","removal":"delete","type":"oem"},{"id":"com.oplus.battery","description":"Battery\nNeeded for power managment in settings?\nhttps://xdaforums.com/t/how-to-fix-oneplus-11-thermal-throttling.4560085/post-89255314\nBetter test it.","removal":"caution","type":"oem"},{"id":"com.oplus.batterywarning","description":"Battery warning.\nUses accessbility, GPS and Wi-Fi.","required_by":["com.oneplus.gamespace"],"removal":"replace","type":"oem"},{"id":"com.oplus.blacklistapp","description":"Block & filter\nCall blocking app that's tied with device caller app, contains fuction to look caller location on Google Maps, potentially untrusted.","removal":"replace","type":"oem"},{"id":"com.oplus.blur","description":"All the blur effects in the system UI will be removed and replaced with completely transparent if you remove this package.","removal":"caution","type":"oem"},{"id":"com.oplus.bttestmode","description":"BTtestmode\nBluetooth SAR Signaling, Scene Test","removal":"delete","type":"oem"},{"id":"com.oplus.camera","label":"Camera","description":"Stock Oppo/Oneplus camera app","removal":"replace","suggestions":"cameras","type":"oem"},{"id":"com.oplus.cast","description":"Screencast\nPhone casting (casting phone screen to other devices).","removal":"caution","type":"oem"},{"id":"com.oplus.cosa","label":"App Enhancement Services","required_by":["com.oneplus.gamespace"],"description":"If enabled, connects to OPPO servers (icosa-service-eu.allawnos.com) every time a new app is installed. Seems to be mostly focused on gaming performance optimisation according to the settings description:\n'[...] a service that optimises phone performance for specific apps and game scenarios. [...] frame rates, battery usage, touch sensitivity, network connection, vibration and gameplay assistance features.'\n\nCan be disabled via hidden setting (Settings -> Search 'App Enhancement Services' -> App Enhancement Services).\nCannot be uninstalled but it can be disabled.","web":["https://beta.pithus.org/report/f55e935357865f4647e59c98afb5a3a46aba22a48844d80d2819d122781e3fde"],"removal":"caution","warning":"Removing this package prevents the OnePlus Game Center to detect games.","type":"oem"},{"id":"com.oplus.cota","description":"Carrier software updater?\nLooks like software updater for carrier phones.","removal":"caution","type":"oem"},{"id":"com.oplus.crashbox","label":"CrashBox","description":"Sends system failure data to developers. Automatically runs at boot.\n\nPithus analysis: https://beta.pithus.org/report/6031048af7434e9cfe3435244dd105ac70e3bfe1f25ecdcca9b2a40b356590a2","removal":"delete","type":"oem"},{"id":"com.oplus.customize.coreapp","description":"Useless frameworks","removal":"delete","type":"oem"},{"id":"com.oplus.deepthinker","label":"Intelligent Services","description":"Seems to open some common apps in the background, which may increase power consumption.","removal":"caution","warning":"Removing it on ColorOS 12.0 or above will cause the battery menu to not show the power consumption curve. This can cause a bootloop.","type":"oem"},{"id":"com.oplus.dmp","description":"Deactivate Traffic Management\nFusion search service?","removal":"delete","type":"oem"},{"id":"com.oplus.eid","description":"Eid-Service\nIt's something about card chips or online identity? Useless. China.","removal":"delete","type":"oem"},{"id":"com.oplus.encryption","description":"Private Safe\nAfter uninstalling, the option to use the file safe in the privacy settings disappears. There is also a section left in the settings that does nothing when clicked.","removal":"delete","type":"oem"},{"id":"com.oplus.engineercamera","description":"EngineerCamera\nTesting camera things.","removal":"delete","type":"oem"},{"id":"com.oplus.engineermode","description":"EngineerMode\nTesting phone things(like audio, bluetooth, etc.).","removal":"delete","type":"oem"},{"id":"com.oplus.engineermodeforflipkart","description":"Engineermode2\nThis is app for get flipkartAddress and ShowSarImeiReceiver.\nI found some info: This phone is part of Flipkart Smart Plan and not eligible for release till any instant advance amount availed is repaid.","removal":"delete","type":"oem"},{"id":"com.oplus.engineernetwork","description":"EngineerNetwork\nTesting network things.","removal":"delete","type":"oem"},{"id":"com.oplus.exserviceui","description":"Gesture Motion Services\nIs related to gesture navigation 'com.oplus.gesture'.","removal":"caution","type":"oem"},{"id":"com.oplus.exsystemservice","label":"System Service","description":"Lots of permissions. The screenshot function will stop working when the app is disabled.","removal":"caution","type":"oem"},{"id":"com.oplus.eyeprotect","description":"Eye protection mode\nSafe to remove if you dont use this feature. It can be found in the settings.","removal":"delete","type":"oem"},{"id":"com.oplus.framework.rro.oneplus","description":"A runtime resource overlay (RRO) is a package that changes the resource values of a target package at runtime. For example, an app installed on the system image might change its behavior based upon the value of a resource. Rather than hardcoding the resource value at build time, an RRO installed on a different partition can change the values of the app's resources at runtime.\nRROs can be enabled or disabled. You can programmatically set the enable/disable state to toggle an RRO's ability to change resource values. RROs are disabled by default (however, static RROs are enabled by default).\nhttps://source.android.com/docs/core/runtime/rros","removal":"caution","type":"oem"},{"id":"com.oplus.framework_bluetooth.overlay","description":"Needed to bluetooth?","removal":"caution","type":"oem"},{"id":"com.oplus.games","label":"Game Assistant","description":"Games (https://play.google.com/store/apps/details?id=com.oplus.games)\nOccasionally runs in the background as part of the system.\nAllows you to launch your game library, check game stats(such as playtime), activate game overlay features and performance settings to tweak game/battery performance during gaming.\nThis is the only way to access the recording buffer functionality (records the last X seconds into RAM and saves them when you tap save), so keep enabled if you need that or any of the other features.Note: new package name of com.oneplus.gamespace (since the merge between Oppo and OnePlus. Oplus = Oppo+OnePlus","removal":"delete","type":"oem"},{"id":"com.oplus.gesture","description":"Always-on screen gestures.","removal":"caution","type":"oem"},{"id":"com.oplus.hamlet","description":"Vision Enhance\nUseless things about color maybe to wallpaper?","removal":"delete","type":"oem"},{"id":"com.oplus.healthservice","description":"healthservice\nSome health function, needed for correct work of fitness trackers and pedometer from realme. When deleted, it does not seem to affect the operation of the phone.","removal":"delete","type":"oem"},{"id":"com.oplus.interconnectcollectkit","description":"InterconnectCollectKit\nIt may have something to do with the device's fast connection by name. BUT I see only it collects system logs and it has something to com.heytap.accessory.","removal":"delete","type":"oem"},{"id":"com.oplus.kekepay","description":"Chinese pay service? Safe to remove but no documentation found online\n[APK NEEDED]","removal":"delete","type":"oem"},{"id":"com.oplus.keyguard.clock.base","description":"The clock style available in the lock screen and the theme manager.","removal":"caution","type":"oem"},{"id":"com.oplus.keyguard.clock.gallery","description":"The clock style available in the lock screen and the theme manager.","removal":"caution","type":"oem"},{"id":"com.oplus.keyguard.clock.graffiti","description":"The clock style available in the lock screen and the theme manager.","removal":"caution","type":"oem"},{"id":"com.oplus.keyguard.clock.magazine","description":"The clock style available in the lock screen and the theme manager.","removal":"caution","type":"oem"},{"id":"com.oplus.lfeh","label":"OplusLFEHer","description":"Seems to be related to the the logging suite.\n\nPithus analysis: https://beta.pithus.org/report/0542dbdbe10fd3a868ea497ec92670619670f574bbce37d949975dc109cd316f","removal":"delete","type":"oem"},{"id":"com.oplus.linker","description":"OPSynergy\nUsed for the Synergy app to link your phone to your PC (It doesn't work well beyond controlling your phone from your PC).","removal":"delete","type":"oem"},{"id":"com.oplus.location","description":"Chinese location. Have China MCC. Useless","removal":"delete","type":"oem"},{"id":"com.oplus.locationproxy","description":"Carrier Location Services\nExtra location telemetry, not related to any GPS functions.","removal":"delete","type":"oem"},{"id":"com.oplus.logkit","label":"Feedback","description":"Logs service and bug reporting app\nSafe to remove if you don't report bugs to OEM","removal":"delete","type":"oem"},{"id":"com.oplus.mediacontroller","description":"Media Controller.\nResponsible for the Dynamic Island feature for music applications and showing the 'Now playing' notification on lockscreen.","dependencies":["com.oneplus.account","com.oplus.ocs","com.oplus.onet","com.oplus.metis","com.oplus.statistics.rom","com.coloros.assistantscreen","com.coloros.scenemode"],"removal":"caution","type":"oem"},{"id":"com.oplus.melody","description":"Wireless Earphones\nWireless headset advanced configuration related (after uninstallation some Bluetooth headset setting options will be missing items, if you do not have headset setting problems, it is recommended to uninstall)\nhttps://play.google.com/store/apps/details?id=com.oplus.melody","removal":"replace","type":"oem"},{"id":"com.oplus.metis","label":"Metis","description":"Provides system functions such as scene recognition, smart reminders, and device environment awareness. For example, it can identify nearby TVs for seamless connection or deliver contextually intelligent reminders. It operates quietly in the background.","removal":"caution","warning":"If removed, live alerts will only work when changing volume modes and personal hotspot.","type":"oem"},{"id":"com.oplus.multiapp","label":"App Cloner","description":"App Cloner. Allows to clone an app. Have access to all installed apps. Is bundled with OnePlus analytics\n\nPithus analysis: https://beta.pithus.org/report/8a1d0783debb405ebadb3fc52507de5f69ecb55f499732b7331dac74ad69ffd7","removal":"replace","type":"oem"},{"id":"com.oplus.nas","description":"NetworkAssistSys\nNeeded for network I guess by dialogs found in code. Has location & phone permission, can read your call logs. Runs in background. After disabling, no issues occured (OxygenOS 14, OnePlus 11).","removal":"replace","type":"oem"},{"id":"com.oplus.ndsf","description":"DSF\nProvides the ability to authenticate your identity when you connect your device.\nFor the 'Device Connection Security Service' to work properly, it needs to collect\ninformation about your HeyTap account to support automatic connection to different\ndevices with the same account and connect to the Internet to verify the security of your HeyTap account.","removal":"replace","type":"oem"},{"id":"com.oplus.nhs","description":"NetworkHealthService\n(Uninstallation may cause the power saving mode optimization option of turning off 5G to be disabled, only briefly, after which it will automatically return to 5G) needs more testing.","removal":"caution","type":"oem"},{"id":"com.oplus.notificationmanager","description":"Notification management will not work when uninstalled/disabled.","removal":"unsafe","type":"oem"},{"id":"com.oplus.nrMode","description":"OplusNrModeControl\nRequired for Smart 5G.","removal":"caution","type":"oem"},{"id":"com.oplus.obrain","description":"OBrain\nLog component, domestic special, the key to a MIDAS service, looking at the dex content is also to catch log.","removal":"delete","type":"oem"},{"id":"com.oplus.oca","description":"Has something related to wallet smart card","removal":"delete","type":"oem"},{"id":"com.oplus.ocar","description":"Car+\n Related to Oppo's car app [APK NEEDED]","removal":"delete","type":"oem"},{"id":"com.oplus.ocloud","description":"Its not about cloud but a lot logs.","removal":"delete","type":"oem"},{"id":"com.oplus.ocs","description":"OpenCapabilityService\nChinese unique identifier privacy hazard.","removal":"delete","type":"oem"},{"id":"com.oplus.olc","description":"Olc\nLogger, a new appearance in F.05, the domestic special, and all say that they are log core.","removal":"delete","type":"oem"},{"id":"com.oplus.omoji","description":"Omoji\nAvatar editing app.","removal":"delete","type":"oem"},{"id":"com.oplus.onet","description":"ONet\nUseless frameworks. To secure keyboard, grant permissions. It's not needed.","removal":"delete","type":"oem"},{"id":"com.oplus.onetrace","description":"OneTrace\nLogging component, may be used for feedback toolkit log capture, domestic specials.","removal":"delete","type":"oem"},{"id":"com.oplus.oscenter","description":"Useless frameworks security.","removal":"delete","type":"oem"},{"id":"com.oplus.ota","description":"Software update\nProvides System Updates.","removal":"caution","type":"oem"},{"id":"com.oplus.ovoicemanager","description":"Voice assistant related","removal":"delete","type":"oem"},{"id":"com.oplus.owkservice","description":"Weird app that has time sync and start/stop Monitor Wifi Connectivity, also has oplus statistics component to tracking events.","removal":"caution","type":"oem"},{"id":"com.oplus.padconnect","description":"It has pad settings and multi-screen connect.","removal":"replace","type":"oem"},{"id":"com.oplus.pantanal.ums","description":"Ubiquitous Manager Service\nA service required for Fluid Cloud (Dynamic Island clone) feature in OxygenOS/ColorOS/RealmeUI (Android 14). Runs in background. After disabling, no issues occurred but there's a little empty space between clock and recent notification icons on the right.","removal":"replace","type":"oem"},{"id":"com.oplus.pay","label":"Secure payment","description":"Lets you pay with your phone. Privacy issue aside, you should probably not trust their security.\nSome users cannot uninstall this app.","web":["https://www.bitdefender.com/blog/hotforsecurity/hackers-attack-oneplus-again-this-time-stealing-customer-details"],"removal":"delete","type":"oem"},{"id":"com.oplus.phonenoareainquire","description":"Number Origin\nNot yet uninstalled but is related to phone calling function, need to check deeply...","removal":"caution","type":"oem"},{"id":"com.oplus.portrait","description":"Canvas\nAn additional feature for AOD that allows you to further personalize the disabled screen. If not used, you can delete it.","removal":"delete","type":"oem"},{"id":"com.oplus.postmanservice","description":"Calibration, scan QR code test","removal":"delete","type":"oem"},{"id":"com.oplus.powermonitor","description":"Power monitor\nLogging component to upload temperature control logs.","removal":"delete","type":"oem"},{"id":"com.oplus.pscanvas","description":"Open Canvas\nSplit-screen feature, see:\nhttps://community.oneplus.com/thread/1484307668276346883.","removal":"caution","type":"oem"},{"id":"com.oplus.qualityprotect","label":"QualityProtect","description":"Probably Oppo/OnePlus analytics, no effects after disabling/deleting","web":["https://forum.xda-developers.com/t/list-of-applications-that-can-be-uninstalled-with-adb-commands.4392267/post-86573217"],"removal":"delete","type":"oem"},{"id":"com.oplus.romupdate","description":"Update Service\nIf you wanna remove it to keep ram, it's a bad idea. Better remove com.oplus.ota too if you don't plan to update system.","removal":"caution","type":"oem"},{"id":"com.oplus.safecenter","label":"Security center","description":"Enhances privacy features on Oneplus/Oppo devices","web":["https://forum.xda-developers.com/t/the-oneplus-10-pro-debloat-thread.4503969/page-2#post-87920315"],"removal":"caution","warning":"Breaks app lock feature when disabled, all other features work. Uninstalling on OxygenOS 15 makes the phone unusable!","type":"oem"},{"id":"com.oplus.sauhelper","description":"SAUHelper\nIt's very much a placeholder app with no real functionality, but it can't be uninstalled or deactivated, and it can be SUSPENDED.","removal":"delete","type":"oem"},{"id":"com.oplus.screenrecorder","description":"Screenrecorder","removal":"replace","type":"oem"},{"id":"com.oplus.screenshot","label":"Screenshot","description":"Enables quick settings screenshot option, the power+volume down screenshot feature works without it","removal":"delete","type":"oem"},{"id":"com.oplus.securitykeyboard","label":"Secure Keyboard","description":"Secure Keyboard\nKeyboard that appears only when typing a password on apps and webpages, if enabled on Keyboard and Input settings","removal":"delete","type":"oem"},{"id":"com.oplus.securitypermission","description":"Permission manager\nIf removed, in some players (and not only), it will not be possible to enable picture-in-picture capability due to lack of customization: Settings -> Manage apps -> Top of other apps.","removal":"unsafe","type":"oem"},{"id":"com.oplus.smartengine","description":"It appears to be an app used for testing, no internet access, very few intents, shouldn't have self-started, should run on demand. Some people have reported it as a support component for speedcards, some speedcards won't load after uninstalling.","removal":"caution","type":"oem"},{"id":"com.oplus.sos","label":"Emergency SOS","description":"Emergency Alert service by clicking power button 5 times. It will automatically call contacts (and/or send a SMS) you designated as emergency contacts","removal":"replace","type":"oem"},{"id":"com.oplus.statistics.rom","description":"User Experience Program\nIntrusive telemetry. Runs at boot and constantly stays in background\n\nPithus analysis: https://beta.pithus.org/report/7720549a5b4bc305a15e19b3e17ba6857a52e6e12db94006677c59f2fad84331","removal":"delete","type":"oem"},{"id":"com.oplus.stdid","label":"StdID","description":"StdID\nNeeded for tracking battery usage on per app basis. Dependency for GameSpace\n\n [MORE INFO NEEDED / APK NEEDED]","removal":"replace","required_by":["com.oneplus.gamespace","com.coloros.gamespace"],"type":"oem"},{"id":"com.oplus.stdsp","description":"Device Security Service\nit's a basic service module that helps users discover security risks in their devices and generate appropriate security policies. Detect xposed, and log suite related, needless to say, deactivate.","removal":"delete","type":"oem"},{"id":"com.oplus.subsys","description":"Its app for virtual communication. Depends on 'com.oplus.virtualcomm'.","removal":"replace","type":"oem"},{"id":"com.oplus.synergy","label":"HeySynergy","description":"HeySynergy\nProvides the screencasting feature and OPPO's PC Connect (https://connect.oppo.com/). Don't bother downloading 'PC Connect Desktop' if the 'Phone Connect' Quick Settings tile isn't available on your phone.\n\nPithus analysis: https://beta.pithus.org/report/16d9ea536683291fbffe46dedd3c655379b5fcfdb473ec1cab5290cf5af27fba","removal":"replace","dependencies":["com.heytap.mcs","com.heytap.accessory"],"type":"oem"},{"id":"com.oplus.themestore","description":"Theme Store\nTo change fonts, wallpapers, etc. of your device. Note that you can download but you can't apply unless you have registered for/sign in with HeyTap account.","removal":"delete","type":"oem"},{"id":"com.oplus.thirdkit","description":"Compatibility problem solving\nThird party related.","removal":"delete","type":"oem"},{"id":"com.oplus.trafficmonitor","label":"OnePlus Data usage","description":"Oneplus traffic monitor (monthly data usage, etc).","removal":"caution","warning":"If removed, the 'Data Usage' option under 'Mobile network' will not work. App Info screen's 'Data usage' option will also disappear.","type":"oem"},{"id":"com.oplus.uiengine","description":"Needed for themes? I found a lot of gameloading things, Proguard?, lists of apps idk for what. Remove it not bricks anything probably.","removal":"caution","type":"oem"},{"id":"com.oplus.uxdesign","description":"Wallpapers & style\nNeeded for changing wallpaper.","removal":"replace","type":"oem"},{"id":"com.oplus.vdc","description":"It's an app used to convert nearby devices into virtual hardware resources for the central device. Require internet to transfer data across devices.","removal":"replace","type":"oem"},{"id":"com.oplus.viewtalk","description":"ViewTalk\nApplication for recognizing text in an image and reading it out loud.\nDid not figure out how it works.","removal":"replace","type":"oem"},{"id":"com.oplus.vip","description":"My OPPO\nhttps://play.google.com/store/apps/details?id=com.oplus.vip","removal":"delete","type":"oem"},{"id":"com.oplus.virtualcomm","description":"It's probably virtual communications.\nAlso about these it's in the code.","removal":"replace","type":"oem"},{"id":"com.oplus.wallpapers","description":"Wallpapers\nNeeded for wallpapers.","removal":"replace","type":"oem"},{"id":"com.oplus.wifibackuprestore","label":"WifiBackupRestore","description":"Lets you backup your wifi credentials to the cloud and possibly local wifi access point backups with local backup and restore feature. This app has obviously access to your wifi credential and have the INTERNET permission.\n","web":["https://beta.pithus.org/report/76e43cf4dc55452f39d9b6117074f4072189d3c8ad9cb295a86e49438545f7aa"],"removal":"caution","warning":"Removing this package may have similar effects of removing `com.coloros.wifibackuprestore` as this could also break local wifi credential backups functionality without it. However, it's effects is only tested with ColorOS devices with similar package before Oneplus merge.","type":"oem"},{"id":"com.oplus.wifitest","description":"wifitest\nJust a testing mode accessed through phone dialing code.","removal":"delete","type":"oem"},{"id":"com.oplus.wirelesssettings","description":"DCSSDK\nWireless settings.","removal":"unsafe","type":"oem"},{"id":"com.opos.ads","description":"Chinese ads click tracking","removal":"delete","type":"oem"},{"id":"com.oppo.atlas","description":"Useless frameworks","removal":"delete","type":"oem"},{"id":"com.oppo.bttestmode","description":"Test BLE Tx and Rx channel","removal":"delete","type":"oem"},{"id":"com.oppo.camera","description":"Camera\nOppo stock camera app","removal":"replace","type":"oem"},{"id":"com.oppo.criticallog","description":"I found logs in code. This app means nothing.","removal":"delete","type":"oem"},{"id":"com.oppo.ctautoregist","description":"Has something related to IMS SMS","removal":"replace","type":"oem"},{"id":"com.oppo.customize","description":"Only Hello world! found in app also things to com.orange.aura.oobe","removal":"delete","type":"oem"},{"id":"com.oppo.em5g","description":"EM NR5G\n5G Disable Mode, Probably needed for mobile data internet.","removal":"caution","type":"oem"},{"id":"com.oppo.engineermode","description":"Engineer mode (hardware tests), uses a dialer code, safe to disable","removal":"replace","type":"oem"},{"id":"com.oppo.engineermode.camera","description":"Related to com.oppo.engineermode. This application likely doesn't work without that app.","removal":"replace","type":"oem"},{"id":"com.oppo.engineermode.network","description":"Related to com.oppo.engineermode. This application likely doesn't work without that app.","removal":"replace","type":"oem"},{"id":"com.oppo.fingerprints.fingerprintsensortest","description":"Fingerprint sensort test\n","removal":"delete","type":"oem"},{"id":"com.oppo.freefallingmonitor","description":"I found only: Hello World!. This app means nothing.","removal":"delete","type":"oem"},{"id":"com.oppo.gmail.overlay","description":"I found in code only words Vodafone email, email setup wizard. Not needed overlay","removal":"delete","type":"oem"},{"id":"com.oppo.healthservice","description":"Some health function, needed for the correct work of fitness trackers and pedometers from realme. When deleted, it does not seem to affect the operation of the phone.","removal":"delete","type":"oem"},{"id":"com.oppo.instant.local.service","description":"A lot of tracking for demo game ads","removal":"delete","type":"oem"},{"id":"com.oppo.launcher","description":"OPPO Home\nOppo stock launcher","removal":"caution","type":"oem"},{"id":"com.oppo.lfeh","description":"OppoLFEHer\nDCS service, another thing related to scanning. Bloated","removal":"delete","type":"oem"},{"id":"com.oppo.localservice","description":"It's an app without activities, and in the code, I found it's for Set Mode Daemon Point, but how can we use that? I guess it's for testing, so remove it.","removal":"delete","type":"oem"},{"id":"com.oppo.locationpicker","description":"Location works without.","removal":"delete","type":"oem"},{"id":"com.oppo.logkit","description":"Not sure what it does.\n","removal":"delete","type":"oem"},{"id":"com.oppo.logkitsdservice","description":"Hidden logs hardware without activities.","removal":"delete","type":"oem"},{"id":"com.oppo.logkitservice","description":"Probably same as \"com.oem.oemlogkit\", which is a shady logging package on Oneplus devices.","removal":"delete","type":"oem"},{"id":"com.oppo.market","description":"Oppo App Market\n","removal":"delete","type":"oem"},{"id":"com.oppo.mimosiso","description":"Empty app. I only found: hello world!","removal":"delete","type":"oem"},{"id":"com.oppo.multimedia.dolby","description":"Oppo Dolby service equalizer, but only for Chinese apps","removal":"delete","type":"oem"},{"id":"com.oppo.music","label":"Music","description":"Oppo Music app","web":["https://play.google.com/store/apps/details?id=com.oppo.music"],"removal":"replace","suggestions":"music_apps","type":"oem"},{"id":"com.oppo.nw","description":"RadioInfo\nOnly has radioinfo activity. Not needed","removal":"delete","type":"oem"},{"id":"com.oppo.operationManual","description":"Oppo User Manual","removal":"delete","type":"oem"},{"id":"com.oppo.oppopowermonitor","label":"Power monitor","description":"Battery monitoring","removal":"delete","type":"oem"},{"id":"com.oppo.ota","description":"Software update\nSoftware update OTA","removal":"caution","type":"oem"},{"id":"com.oppo.ovoicemanager","description":"Voice wake-up manager?","removal":"delete","type":"oem"},{"id":"com.oppo.partnerbrowsercustomizations","description":"Oppo Bookmarks\nOppo default browser customization. Injects Oppo bookmarks","removal":"delete","type":"oem"},{"id":"com.oppo.qualityprotect","description":"Useless logs","removal":"delete","type":"oem"},{"id":"com.oppo.quicksearchbox","description":"Single swipe from top to bottom search that has lots of Chinese in it\n","removal":"delete","type":"oem"},{"id":"com.oppo.resmonitor","description":"App has only icon and means nothing.","removal":"delete","type":"oem"},{"id":"com.oppo.rftoolkit","description":"Monitors the broadcast action events (BOOT_COMPLETED)","removal":"replace","type":"oem"},{"id":"com.oppo.securepay","description":"Payment Protection\n","removal":"delete","type":"oem"},{"id":"com.oppo.smartvolume","description":"This app means nothing. There's only logs.","removal":"delete","type":"oem"},{"id":"com.oppo.sos","description":"Emergency Alert service by clicking power button 5 times. It will automatically call contacts (and/or send a SMS) you designated as emergency contacts","removal":"replace","type":"oem"},{"id":"com.oppo.tzupdate","description":"Useless time zone calibration","removal":"delete","type":"oem"},{"id":"com.oppo.usageDump","description":"After-sales service assistance tools","removal":"delete","type":"oem"},{"id":"com.oppo.webview","description":"Oppo WebView enables Android apps to display web content within the app itself, based on Chrome.","removal":"replace","warning":"Make to have another Webview before uninstalling it or some apps may not work properly.","suggestions":"webviews","type":"oem"},{"id":"com.oppo.wifirf","description":"Wifi Rf Test","removal":"delete","type":"oem"},{"id":"com.oppo.wifitest","description":"Wi-Fi RF, RX test","removal":"delete","type":"oem"},{"id":"com.oppoex.afterservice","description":"Have something to E-warranty card Chinese.","removal":"delete","type":"oem"},{"id":"com.osp.app.signin","label":"Samsung account","description":"Lots of trackers in this app.\nHas a huge list of permissions. It is an essential app for a lot of Samsung apps (which will be removed with the default selection in this list)","web":["https://gitlab.com/W1nst0n/universal-android-debloater/-/issues/39"],"removal":"caution","warning":"Settings app will crash on Android 11. Should work fine in other versions of Android.","type":"oem"},{"id":"com.payjoy.access","label":"PayJoy Access","description":"Access is PayJoy's firmware product which OEMs optionally use to enable automatic setup (“provisioning”) and device management to enable PayJoy's Lock to work “out of the box” to minimize the number of steps for the user and store clerk to get started.","removal":"delete","type":"oem"},{"id":"com.payjoy.permission","description":"It's for lock device and probably safe mode.","removal":"delete","type":"oem"},{"id":"com.policydm","description":"Samsung security policy update (https://play.google.com/store/apps/details?id=com.policydm)\nUpdatable policy files designed to increase android security and detect malicious behaviour.\nHas nothing to do with OTA updates or Android Security patches.\nCan be removed without issue (https://gitlab.com/W1nst0n/universal-android-debloater/-/issues/15)\nSee \"com.samsung.android.spdclient\" for more information.\n","removal":"replace","type":"oem"},{"id":"com.preff.kb.xm","description":"'Emoji & Font Keyboard' on Xiaomi.\nXiaomi won't let me uninstall it. If it's same for you, consider disabling it instead.\nMake sure to install an alternate keyboard app before doing so.","removal":"replace","type":"oem"},{"id":"com.qeexo.smartshot","description":"Smart Screenshots\nSmart Screenshots? Disable it doesnt affecting normal screenshots.","removal":"delete","type":"oem"},{"id":"com.qiyi.video","description":"IQIYI (https://play.google.com/store/apps/details?id=com.qiyi.video)\nOnline video platform from Baidu (https://en.wikipedia.org/wiki/IQiyi).\nI didn't know this is currently one of the largest online video sites in the world, \nwith nearly 6 billion hours spent on its service each month, and over 500 million monthly active users.","removal":"delete","type":"oem"},{"id":"com.qorvo.uwb.vendorservice","description":"Unknown app that has something to .android.UwbApp and NFC_SET_CONTROLLER_ALWAYS_ON permission.","removal":"caution","type":"oem"},{"id":"com.qti.backupagent","description":"Backup Agent\nhidden app for chinese phones backups SMS,MMS,Contacts","removal":"delete","type":"oem"},{"id":"com.qti.primarycardcontroller","description":"Sim card things about detected or switching, some chinese phones uses qualcomm names apps instead of normal","removal":"unsafe","type":"oem"},{"id":"com.qualcomm.qti.carrierconfigure","description":"Carrier Configure, Configures carrier, only for some china phones uses qualcomm names","removal":"unsafe","type":"oem"},{"id":"com.qualcomm.qti.extsettings","description":"Needed for hotspot, some chinese phones uses qualcomm names apps","removal":"unsafe","type":"oem"},{"id":"com.qualcomm.qti.gpudrivers.pineapple.api34","description":"Qualcomm GPU Drivers","removal":"unsafe","type":"oem"},{"id":"com.qualcomm.qti.notificationservice","description":"High Battery Temperature\nWrong named package, theres only high temperature notification.\nit's app for some chinese phones","removal":"delete","type":"oem"},{"id":"com.qualcomm.qti.qs","description":"Quick settings to mobile data internet, some chinese phones uses qualcomm names apps instead of normal","removal":"unsafe","type":"oem"},{"id":"com.qualcomm.qti.securemsm.mdtp","description":"MdtpService\nOnly logs found","removal":"delete","type":"oem"},{"id":"com.qualcomm.qti.sva","description":"Qualcomm Voice Activation\nIt has a lot of things like training voice, tutorial, debug mode.","removal":"delete","type":"oem"},{"id":"com.qualcomm.qti.tetherservice","description":"Needed for tethering, some chinese phones uses qualcomm names apps instead of normal","removal":"unsafe","type":"oem"},{"id":"com.qualcomm.qti.tm","description":"Task manager\nhidden app for chinese phones","removal":"delete","type":"oem"},{"id":"com.qualcomm.qti.trustedui","description":"trusteduiservice\nTrusted UI debugging.","removal":"delete","type":"oem"},{"id":"com.reallytek.wg","description":"Calibration, Test Camera things.","removal":"delete","type":"oem"},{"id":"com.realme.as.music","description":"Realme Music app\nhttps://play.google.com/store/apps/details?id=com.realme.as.music","removal":"delete","type":"oem"},{"id":"com.realme.findphone.client2","description":"Find my phone client app\n","removal":"delete","type":"oem"},{"id":"com.realme.link","description":"RealMe Link (https://play.google.com/store/apps/details?id=com.realme.link)\nCompanion app for various realme IoT devices. Useless if you don't have a realme watch/band","removal":"delete","type":"oem"},{"id":"com.realme.logtool","description":"Hidden Logs App","removal":"delete","type":"oem"},{"id":"com.realme.movieshot","description":"Combine captions\nIt's for partial screenshot, SaveLongshot.","removal":"replace","type":"oem"},{"id":"com.realme.securitycheck","label":"SecurityAnalysis","description":"Have useless ads and security checks.\nWhy do we need more security when Play Protect is already good?","removal":"caution","type":"oem"},{"id":"com.realme.wellbeing","description":"Sleep capsule\nFeature for your phone where apps are locked at night to help you get a good night's sleep.\nNo one is probably using this.","removal":"delete","type":"oem"},{"id":"com.recognize.number","label":"Identify unknown numbers","description":"Checking the code, this app looks useful only in China.","removal":"delete","type":"oem"},{"id":"com.redteamobile.virtual.softsim","description":"It's for vsim = virtual sim","removal":"delete","type":"oem"},{"id":"com.ringclip","description":"Ringtone editing.","removal":"caution","type":"oem"},{"id":"com.rongcard.eid","label":"Eid-Service","description":"EID probably means Electronic ID. This presumably handles something related to that.\nUseful only in China.","removal":"delete","type":"oem"},{"id":"com.rongcard.eidapi","label":"aidlserverdemo","description":"EID probably means Electronic ID. This presumably handles something related to that.\nUseful only in China.\nHas a lot of logs in code.","removal":"delete","type":"oem"},{"id":"com.rsupport.rs.activity.lge.allinone","description":"LG RemoteCall service app\nUsed to get support from LG, safe to remove.","removal":"delete","type":"oem"},{"id":"com.rsupport.rsperm.ntt","description":"Remote support service\nI found only permissions and not explained buttons in the code this app.","removal":"delete","type":"oem"},{"id":"com.sagereal.lcmtest","description":"Secret code: 88. LCD, LCM Test.","removal":"delete","type":"oem"},{"id":"com.samsung.InputEventApp","description":"Hidden testing things also no activities","removal":"delete","type":"oem"},{"id":"com.samsung.SMT","label":"Samsung TTS","description":"Samsung Text-to-speech service.\nWorks with applications such as S Voice; translation apps, GPS that require Text-To-Speech (TTS) functionality and reads back text.\nWARNING: SOME VERSIONS OF THIS APP ARE VULNERABLE TO PRIVILEGE ESCALATION ATTACKS! It can allow arbitrary RCE as system (UID 1000). Identifier: CVE-2019-16253.","web":["https://galaxystore.samsung.com/detail/com.samsung.SMT","https://github.com/flankerhqd/vendor-android-cves/tree/master/SMT-CVE-2019-16253"],"removal":"replace","suggestions":"tts","type":"oem"},{"id":"com.samsung.aasaservice","description":"Sometimes, eat a LOT of battery (according to some reddit users)\nSecurity policy apps (kind of things which prevents installation of applications)\n","removal":"delete","type":"oem"},{"id":"com.samsung.accessibility","description":"Accessibility settings (useful for apps creating virtual buttons such as a pie-menu)\nWeirdly, removing this package can cause a bootloop if you set a lock code on your phone.\n Also used for clearing system cache from apps such as SD-Maid.","removal":"caution","type":"oem"},{"id":"com.samsung.accessory","description":"Samsung Accessory Service (https://play.google.com/store/apps/details?id=com.samsung.accessory)\nLets you transfer data between your Samsung phone and Samsung accessories (GALAXY Gear/Watch...) \n","removal":"delete","type":"oem"},{"id":"com.samsung.accessory.beansmgr","description":"Gear IconX Plugin (https://play.google.com/store/apps/details?id=com.samsung.accessory.beansmgr)\nAllows you to use features such as device settings and status view when connected to a Samsung Gear IconX (2018) device.\n","removal":"delete","type":"oem"},{"id":"com.samsung.accessory.safiletransfer","description":"SASystemProviders\nNeeded for Samsung Accessory Service\n","removal":"delete","type":"oem"},{"id":"com.samsung.adaptivebrightnessgo","description":"CameraLightSensor\nApp to test Adaptive Brightness.","removal":"delete","type":"oem"},{"id":"com.samsung.advancedcallservice","description":"Advanced Calling feature on an Android is a feature that allows you to make calls while using other applications with the use of CELLULAR DATA. In order for this feature to work, HD Voice must be enabled in settings.","removal":"replace","type":"oem"},{"id":"com.samsung.advp.imssettings","description":"Needed for VoLTE (Voice over LTE) https://en.wikipedia.org/wiki/Voice_over_LTE\nIMS(Ip Multimedia Subsystem) is an open industry standard for voice and multimedia communications over packet-based IP networks (VoLTE/VoIP/Wifi calling).\nNOTE: This package could be needed for messaging apps that send SMS/RCS code to verify your phone number.","removal":"replace","type":"oem"},{"id":"com.samsung.android.A10s.d01.wallpapermulti","description":"App with wallpapers to Samsung A10s.","removal":"replace","type":"oem"},{"id":"com.samsung.android.ConnectivityOverlay","description":"Needed for wifi","removal":"unsafe","type":"oem"},{"id":"com.samsung.android.ConnectivityUxOverlay","description":"Overlay to wifi icon","removal":"unsafe","type":"oem"},{"id":"com.samsung.android.MtpApplication","description":"Samsung overlay for MTP\nTalks to com.android.mtp. Needed to access your phone from a computer for file transfer.\n","removal":"caution","type":"oem"},{"id":"com.samsung.android.SettingsReceiver","description":"Samsung overlay of AOSP Settings. It has 39 permissions. Handles interactions with features controlled by the settings.\n","removal":"unsafe","type":"oem"},{"id":"com.samsung.android.accessibility.talkback","label":"TalkBack","description":"Samsung talkback in accessibility.","removal":"delete","type":"oem"},{"id":"com.samsung.android.activation","description":"Activation phone in china.","removal":"delete","type":"oem"},{"id":"com.samsung.android.adaptivebrightnessgo","description":"Samsung Adaptive Brightness\nFeature of Samsung devices that, as the name suggests, automatically adjusts the brightness level of display based on environmental lighting.\nThis feature is available even after uninstalling.","removal":"delete","type":"oem"},{"id":"com.samsung.android.aircommandmanager","label":"AirCommandManager","description":"AirCommandManager (FACM) gives you access to signature S Pen features. You can access Air command anytime you are using your phone by simply taking out the S Pen.","web":["https://www.samsung.com/global/galaxy/what-is/air-command/"],"removal":"delete","type":"oem"},{"id":"com.samsung.android.airtel.stubapp","description":"My Airtel Stub app\nMy Airtel is a customer service app designed for Airtel subscribers in Sri Lanka\nThis package isn't the app itself but only a stub\nIt's basically a non-functional empty shell which often only redirect you to the PlayStore to download the full app","removal":"delete","type":"oem"},{"id":"com.samsung.android.alive.service","description":"Content suggestions service","removal":"replace","type":"oem"},{"id":"com.samsung.android.aliveprivacy","description":"Content Suggestions is an AI-based app inside the Secure Folder, and its on-device AI-powered engine automatically suggests users to move private images of pre-selected categories to the Secure Folder. For this to happen users have to simply select specific faces or a type of image they want to tag as private and keep them secured in the private gallery of the Secure Folder. Once the initial setup is complete, the AI engine kicks in and identifies relevant images from the entire gallery. As this is an on-device AI solution, no information or image ever leaves the smartphone.","removal":"replace","type":"oem"},{"id":"com.samsung.android.allshare.service.fileshare","description":"Wi-Fi Direct\nAllows two devices to establish a direct Wi-Fi connection without requiring a wireless router.\nhttps://www.samsung.com/au/support/mobile-devices/connecting-devices-via-wifi-direct/\nhttps://en.wikipedia.org/wiki/Wi-Fi_Direct","removal":"replace","type":"oem"},{"id":"com.samsung.android.allshare.service.mediashare","label":"Nearby Service/SmartView","description":"Formerly, Samsung AllShare service.\nUsed to stream content from your phone to a Samsung smart TV.\n On devices where the display-name is 'Nearby Service', 'Smart View' will continue to work properly (it doesn't depend on Nearby Service).","web":["https://www.samsung.com/us/apps/smart-view-2/"],"removal":"delete","type":"oem"},{"id":"com.samsung.android.app.accesscontrol","description":"Samsung Interaction control\nSettings > Accessibility > Dexterity and interaction (or vol. down + home key)\nAllows you to restrict some functions of your phone (stop the phone from interacting with touch commands for instance)\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.advsounddetector","description":"Samsung Sound detectors\nUses microphone to identify recognizable sounds. For example, if it recognizes a baby's cry, it can alert you with flashing lights so \nyou know to check on your baby. Or it can notify you if it hears the doorbell ring so you know to open the door.\n#\nadv maybe refers to 'Samsung Advanced Institute of Technology' (or simply means 'advanced')\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.amcagent","description":"Advanced Management Console Agent\nEntreprise feature I guess.\n","removal":"replace","type":"oem"},{"id":"com.samsung.android.app.aodservice","description":"Always On Display\nNOTE: This package handles the clock on the lockscreen, in addition to the AOD functionality.\nWhen enabled in settings it shows clock and notifications when you raise the phone or touch the screen.\nThis is basically a lower-power lock-screen. It could in theory reduce power draw if you check notifications/clock often as OLED screens draw minimal power showing a mostly black screen(black = pixel off), but in practice the number of times you'll unintentionally trigger it will likely eat up any potential power savings and more. And if your device doesn't have an OLED screen this will draw way more power.\nMost of these power savings could be applied to your standard lock-screen simply by making your background image completely black.","removal":"replace","type":"oem"},{"id":"com.samsung.android.app.appsedge","description":"Samsung apps edge (https://www.samsung.com/global/galaxy/what-is/apps-edge/)\nDisplays your five most frequently used apps for you to access at a moment’s notice.\nBreaks Split-Screen/Multi-Window according to issue#124.","removal":"replace","type":"oem"},{"id":"com.samsung.android.app.assistantmenu","description":"Assistant menu\nDesigned for individuals with motor control or other physical impairments. \nBy using Assistant menu, you can access hardware buttons and all parts of the screen by simply tapping or swiping.\nhttps://www.samsung.com/uk/accessibility/mobile-assistant-menu/\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.camera.sticker.facear.preload","description":"Annoying Stickers/stamps of the Samsung camera app. C'mon it feels like Snapshat.\nhttps://developer.samsung.com/galaxy/stickers","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.camera.sticker.facear3d.preload","description":"Default 3D live stickers\nAnnoying Stickers/stamps of the Samsung camera app. C'mon it feels like Snapshat.\nhttps://developer.samsung.com/galaxy/stickers","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.camera.sticker.facearavatar.preload","description":"Annoying Stickers/stamps of the Samsung camera app. C'mon it feels like Snapshat.\nhttps://developer.samsung.com/galaxy/stickers","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.camera.sticker.facearframe.preload","description":"Frames sticker? \nI don't know what this sticker is and I don't have this package.\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.camera.sticker.stamp.preload","description":"Annoying Stickers/stamps of the Samsung camera app. C'mon it feels like Snapshat.\nhttps://developer.samsung.com/galaxy/stickers\nSafe to remove\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.clipboardedge","description":"Clipboard edge panel\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.clockpack","description":"Samsung Clock style\nAffects video playback in full-screen mode, breaks it","removal":"caution","type":"oem"},{"id":"com.samsung.android.app.cocktailbarservice","label":"Samsung Edge screen","description":"Enables you to open your five most used apps by simply swiping the edge of the screen.\nSwipe one of the edges of the screen to bring up information even when your device is locked (with the screen off). \nYou can also set it up to display the news or weather, for example.\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.color","description":"Color adjustment\nSamsung's adaptive super AMOLED screen optimizes the color range, saturation, and sharpness of the picture depending on what you're watching or doing. \nThis package lets you to manually customize the color settings to match your preferences.\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.colorblind","description":"This app provides colorblind accessibility features for Samsung devices, helping users with color vision deficiencies by adjusting display colors and improving visibility.","removal":"replace","type":"oem"},{"id":"com.samsung.android.app.contacts","label":"Samsung Contacts","description":"Safe to debloat if you use another contacts app.","removal":"replace","warning":"After removing the app, you will no longer be able to access Contacts from the Samsung dialer app.\nThis will also prevent the user from modifying the Safety and emergency tab within the Samsung Android native settings app. Specifically the Medical info & Emergency contacts. This information is pulled on the lockscreen by first responders or in emergency sharing mode for emergency contacts.","suggestions":"contacts","type":"oem"},{"id":"com.samsung.android.app.dofviewer","description":"Live focus\nAllows you to adjust the level of background blur in the camera app.\nFrom the Samsung Gallery, you can also select from a range of background blur shapes to add characters and shapes to a photo.\nhttps://www.samsung.com/global/galaxy/what-is/live-focus/\n","removal":"replace","type":"oem"},{"id":"com.samsung.android.app.dressroom","label":"Samsung Wallpaper and style","description":"Wallaper manager. Needed to pick up a wallpaper on Android 10+.\nHas INTERNET permission and ACCESS_MEDIA_LOCATION\nBefore Android 10, you should still be able to set a wallpaper from the Samsung gallery without this package.","removal":"caution","warning":"Removing this app will prevent you to set a new wallpaper on Android 10+ (even from the Gallery) or changing the Material You palette on Android 12+.","type":"oem"},{"id":"com.samsung.android.app.dtv.dmb","description":"TV app on Samsung phones and it's only available on Japanese or Korean variants.","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.earphonetypec","label":"Samsung USB-C","description":"Not required unless you have a Samsung DAC or Samsung Type-C earphones (for firmware updates)","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.episodes","description":"Samsung story album (https://www.samsung.com/in/support/mobile-devices/what-is-story-album-application-in-samsung-galaxy-s4/)\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.filterinstaller","description":"Filter installer\nI have no clue about the usefulness of this package. Maybe it filters apps that are not compatible with the phone.\nThis package is only triggered when you install an app (private class PackageIntentReceiver) \n","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.galaxyfinder","label":"Samsung Finder","description":"a search application that allows you to find what you want in an instant by searching the content on your Galaxy smartphone and on the web as well.","web":["https://www.samsung.com/global/galaxy/what-is/s-finder/"],"removal":"replace","warning":"Removing this will remove the search in the app drawer.","type":"oem"},{"id":"com.samsung.android.app.interactivepanoramaviewer","description":"Visual. photo virt.\nSamsung Virtual Shot Viewer enable sharing virtual shot\nSafe to remove if you don't want virtual photos.\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.interpreter","description":"Samsung Interpreter app, enables Live translation of foreign languages","removal":"replace","type":"oem"},{"id":"com.samsung.android.app.kfa","description":"KFA\nAnother app from knox, has something to decrypt.\nName apk this app is: KnoxAIFrameworkApp.","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.ledbackcover","description":"I think it enables doing things with LEDs on the cover\n\nhttps://www.samsung.com/hk_en/mobile-accessories/led-cover-for-galaxy-s10/EF-KG973CBEGWW/\nHOW IT WORKS : https://forum.xda-developers.com/galaxy-note-8/accessories/how-led-cover-t3686694","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.ledcoverdream","description":"I think it enable doing things with LEDs on the cover\nhttps://www.samsung.com/hk_en/mobile-accessories/led-cover-for-galaxy-s10/EF-KG973CBEGWW/\nHOW IT WORKS : https://forum.xda-developers.com/galaxy-note-8/accessories/how-led-cover-t3686694\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.memo","description":"Samsung Memo (was replaced by Samsung Notes app com.samsung.android.app.notes)\n","removal":"replace","suggestions":"note_taking_apps","type":"oem"},{"id":"com.samsung.android.app.mhswrappertmo","description":"Mobile Hotspot\nIs it linked to T-Mobile ? (\"tmo\" at the end of the package)\nYou can debloat this and still create hotspot.\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.mirrorlink","description":"Used to connect your phone to a car (with https://mirrorlink.com/ support) in order to provide audio streaming, GPS navigation...\nhttps://www.samsung.com/us/support/answer/ANS00048972/\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.motionpanoramaviewer","description":"Motion panorama viewer\nLets you see the result of a motion panorama\nhttps://www.samsung.com/global/galaxy/what-is/motion-panorama/\n","removal":"replace","type":"oem"},{"id":"com.samsung.android.app.multiwindow","description":"Provides manifestly the ability to display multiple apps on the screen (at the same time)\nCan someone test?\n","removal":"replace","type":"oem"},{"id":"com.samsung.android.app.news","description":"News Samsung app\nDoesn't exist anymore? \n","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.notes","label":"Samsung Notes","description":"Samsung Notes app\n","web":["https://play.google.com/store/apps/details?id=com.samsung.android.app.notes"],"removal":"replace","suggestions":"note_taking_apps","type":"oem"},{"id":"com.samsung.android.app.notes.addons","description":"Allows for marking up/drawing on notes in the Samsung Notes app (designed for use with S Pen).\nRemoval likely breaks com.samsung.android.app.notes. Safe to remove otherwise.\nMay reinstall itself automatically (removal has persisted for me this time around, and through multiple reboots).\nhttps://galaxystore.samsung.com/prepost/000005769652?appId=com.samsung.android.app.notes.addons","removal":"replace","type":"oem"},{"id":"com.samsung.android.app.omcagent","label":"Samsung Configuration update","description":"Open Market Customization Agent (AKA 'Recommended Apps')\nInstalls more Samsung \"Recommended Apps\" after device setup.","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.panel.naver.v","description":"Naver V Panel\nSpecial samsung panel for the very useless V LIVE (formerly Naver V) app (https://play.google.com/store/apps/details?id=com.naver.vapp)\nV LIVE is an app that features personal video broadcasts of South Korean celebrities\nThis panel also includes Naver Shopping (https://shopping.naver.com/)\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.parentalcare","description":"Parental control to block apps on new samsung phones","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.pinboard","description":"Samsung Scrapbook (discontinued)\nhttps://www.samsung.com/za/support/mobile-devices/how-do-i-use-the-scrapbook-memo-feature-on-my-samsung-galaxy-note3/\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.reminder","description":"Samsung bixby reminder (https://www.samsung.com/global/galaxy/apps/bixby/reminder/)\nSet up smart reminders to get notified when and where you need to. You can even link websites, videos, photos and more.\nUses wifi/data regularly.\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.routines","label":"Samsung Modes and Routines","description":"Formerly, Bixby Routines.\nAutomating actions triggered by context clues: location, time, or event.","web":["https://www.samsung.com/global/galaxy/what-is/bixby-routines/"],"removal":"caution","suggestions":"automation_apps","type":"oem"},{"id":"com.samsung.android.app.sbrowseredge","description":"Related to internet browser. For Galaxy Edge? \n","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.selfmotionpanoramaviewer","description":"Selfie panorama viewer\nLets you see the result of a selfie motion panorama\nhttps://www.samsung.com/global/galaxy/what-is/motion-panorama/\n","removal":"replace","type":"oem"},{"id":"com.samsung.android.app.settings.bixby","description":"Bixby settings (Bixby = Samsung intelligence assistant)\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.sharelive","label":"Quick Share","description":"Samsung Quick Share (from the Galaxy Store)","removal":"replace","warning":"Removing the package breaks Quick Share.","suggestions":"sharing_apps","type":"oem"},{"id":"com.samsung.android.app.simplesharing","description":"Samsung Link Sharing\nLets you share large size files by using the Samsung Cloud.\nhttps://www.samsung.com/au/support/mobile-devices/what-is-link-sharing/\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.siofviewer","description":"Live focus\nNeeded for Live Focus in camera app.","removal":"replace","type":"oem"},{"id":"com.samsung.android.app.smartcapture","label":"Samsung capture","description":"Samsung's implementation of screenshot. Show the bottom toolbar after taking a screenshot.\nLets you group widgets on OneUI 4 (?)","removal":"replace","type":"oem"},{"id":"com.samsung.android.app.smartwidget","description":"Samsung screenshot\nYou will still be able to take screenshots without this package.","removal":"replace","type":"oem"},{"id":"com.samsung.android.app.social","description":"I know this has been discontinued by Samsung but that it.\nSurely a social app like Samsung Members (com.samsung.oh)\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.soundpicker","description":"Lets you select a sound for alarm/ringtone\n","removal":"unsafe","type":"oem"},{"id":"com.samsung.android.app.spage","description":"Samsung Free (previously known as 'Bixby Home') is a vertically scrolling list of information that Bixby can interact with for example weather, fitness activity and buttons\nfor controlling their smart home gadgets.\nhttps://galaxystore.samsung.com/prepost/000005445489?appId=com.samsung.android.app.spage","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.storyalbumwidget","description":"The Story Album widget enables you to access the Story Album app and create digital picture albums that you can view and acess directly \nfrom the widget on a Home screen.\nOld feature (from Galaxy S4)\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.talkback","description":"Voice assistant. Accessibility feature\nScreen Reader to provide audible feedback to assist blind and low-vision users.\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.taskedge","description":"Handle task edge panel\nThrough Tasks edge, you can quickly perform frequently used tasks, such as composing messages and creating events.\nhttps://www.samsung.com/levant/support/mobile-devices/galaxy-s7-edge-how-do-i-add-tasks-edge/\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.telephonyui","label":"Samsung Call settings","description":"The phone call UI that offers the option to accept or reject calls, send a message, etc.","removal":"unsafe","warning":"Removing the package breaks phone app settings and SIM Manager.","type":"oem"},{"id":"com.samsung.android.app.telephonyui.esimclient","label":"EsimClient","description":"only has ESimClient Webview Activity","removal":"caution","type":"oem"},{"id":"com.samsung.android.app.tips","description":"Tips on how to use your phone\"\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.updatecenter","description":"App update\nInstalls app updates","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.vrsetupwizards","description":"Samsung Gear VR (Virtual Reality) setup wizard (https://en.wikipedia.org/wiki/Samsung_Gear_VR)\nhttps://360samsungvr.com/portal/content/about_samsung_vr\nStub = https://stackoverflow.com/questions/10648280/what-is-stub-and-aidl-for-in-java\nSetup wizard : The first time you turn your device on, a Welcome screen is displayed. It guides you through the basics of setting up your device.\nIt's the setup for Samsung VR services.\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.vrsetupwizardstub","description":"Samsung Gear VR (Virtual Reality) setup wizard (https://en.wikipedia.org/wiki/Samsung_Gear_VR)\nhttps://360samsungvr.com/portal/content/about_samsung_vr\nStub = https://stackoverflow.com/questions/10648280/what-is-stub-and-aidl-for-in-java\nSetup wizard : The first time you turn your device on, a Welcome screen is displayed. It guides you through the basics of setting up your device.\nIt's the setup for Samsung VR services.\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.watchmanager","description":"Samsung Galaxy Wearable (Samsung Gear) (https://play.google.com/store/apps/details?id=com.samsung.android.app.watchmanager)\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.watchmanagerstub","description":"Stub for the Galaxy Wearable app.\nStub = https://stackoverflow.com/questions/10648280/what-is-stub-and-aidl-for-in-java","removal":"delete","type":"oem"},{"id":"com.samsung.android.app.withtv","description":"WitTV (replaced by com.sec.android.app.withtv)\nUsed to stream content from your phone to a Samsung smart TV.\nhttps://www.samsung.com/us/apps/smart-view-2/\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.applock","description":"Samsung App Lock\nLets you lock your app (Settings > Advanced functions > App lock)\nYou should lock your apps storing private data (provides data at rest encryption when your phone is locked)\n","removal":"unsafe","type":"oem"},{"id":"com.samsung.android.appseparation","label":"Separated Apps","description":"Separated Apps isolates third-party apps in a sandboxed folder. The third-party apps cannot intercommunicate with work apps or access confidential work data. Keep in mind that Separated Apps does not provide the same privacy guarantees as the new work profile on company-owned devices. As such, it is not intended for personal apps and data.","web":["https://docs.samsungknox.com/admin/knox-platform-for-enterprise/separated-apps.htm","https://beta.pithus.org/report/cae5798a835dc434037400436fba27f5eed960c6f476a7b7d17d85a1425530c0"],"removal":"caution","type":"oem"},{"id":"com.samsung.android.ardrawing","label":"Samsung AR Doodle","description":"Accessible through AR Zone.\nLets you draw on your face using the front camera and uses AR Core for drawing on the environment with the rear camera.\nOnly Sasmung AR app (afaik) that requests location access, and it refuses to run without it.","removal":"delete","type":"oem"},{"id":"com.samsung.android.aremoji","description":"AR Emoji mode for Samsung camera \nhttps://www.samsung.com/global/galaxy/what-is/ar-emoji/\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.aremojieditor","description":"AR Emoji Editor\nEdits those AR people emoji things\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.arzone","description":"AR Zone\nhttps://www.samsung.com/levant/support/mobile-devices/which-features-are-available-in-the-ar-zone-in-the-galaxy-z-flip/\nLets you access other AR apps.\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.asksmanager","description":"Samsung device protection manager.\nIt's anti-theft feature. I couldn't find exactly what does the samsung layer to the already existing android device protection : \nhttps://www.greenbot.com/article/2904397/everything-you-need-to-know-about-device-protection-in-android-51.html\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.audiomirroring","description":"AudioMirroring\nAudio Mirroring to Cast other devices will not work after remove.\nAlso this app running in the background.","removal":"replace","type":"oem"},{"id":"com.samsung.android.authfw","description":"Used by Samsung Pass\nBiometric authentication service that can be used to sign in to websites and apps in your mobile.\nhttps://www.samsung.com/global/galaxy/apps/samsung-pass/\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.aware.service","label":"Samsung Quick Share Agent","description":"Formerly, Quick Share.\nUse Wifi direct to share files between 2 Samsung Galaxy phones (it's only for Samsung Galaxy users).","removal":"replace","suggestions":"sharing_apps","type":"oem"},{"id":"com.samsung.android.bbc.bbcagent","description":"BBCAgent (B. B. Container Agent?)\nCollects device information and manages installation/uninstallation of trusted apps in KNOX containers\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.bbc.fileprovider","description":"KNOX BBC Provider.\nProvider for KNOX BBC\nContent providers encapsulate data, providing centralized management of data shared between apps.","removal":"delete","type":"oem"},{"id":"com.samsung.android.beaconmanager","description":"Replaced by Samsung Smart Things (com.samsung.android.ststub)\nAllows users to control, automate, and monitor their home environment via mobile device. \nhttps://en.wikipedia.org/wiki/SmartThings\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.bio.face.service","label":"Face","description":"Handles Face recognition unlock.\nNOTE: Removing this package causes biometric prompts in apps to be delayed by 5-8 seconds before appearing. Selecting 'Biometrics and security' in the settings app also locks up the Settings app for about 5-8 seconds but eventually loads and functions as normal.","web":["https://kp-cdn.samsungknox.com/b60a7f0f59df8f466e8054f783fbbfe2.pdf"],"removal":"delete","type":"oem"},{"id":"com.samsung.android.biometrics","description":"Provide biometric support\n","removal":"replace","type":"oem"},{"id":"com.samsung.android.biometrics.app.setting","description":"Biometric settings\n","removal":"replace","type":"oem"},{"id":"com.samsung.android.bixby.agent","description":"Removing this will disable the bixby hardware key without breaking Bixby itself.\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.bixby.agent.dummy","description":"Bixby Voice Stub","removal":"delete","type":"oem"},{"id":"com.samsung.android.bixby.es.globalaction","description":"Bixby stuff [MORE INFO NEEDED]","removal":"delete","type":"oem"},{"id":"com.samsung.android.bixby.plmsync","description":"Bixby stuff [MORE INFO NEEDED]","removal":"delete","type":"oem"},{"id":"com.samsung.android.bixby.service","description":"Bixby Service","removal":"delete","type":"oem"},{"id":"com.samsung.android.bixby.voiceinput","description":"Bixby service needed for voice control","removal":"delete","type":"oem"},{"id":"com.samsung.android.bixby.wakeup","description":"Bixby voice wake-up","removal":"delete","type":"oem"},{"id":"com.samsung.android.bixbyhelper","description":"QQ music plugin\nRequire QQ Music and it's Chinese.","removal":"delete","type":"oem"},{"id":"com.samsung.android.bixbytouch","description":"Bixby Touch\nApp for smart things, still only for China.","removal":"delete","type":"oem"},{"id":"com.samsung.android.bixbyvision.framework","description":"BixbyVision Framework","removal":"delete","type":"oem"},{"id":"com.samsung.android.bluelightfilter","description":"Blue light filter\nUsing the sunrise/sunset option uses the ACCESS_FINE_LOCATION permission. It's better to program the activation of the filter according to the time.\nNote: reducing blue light can prevent eyestrain and other health issues. See https://www.webmd.com/eye-health/blue-light-reduce-effects","removal":"replace","type":"oem"},{"id":"com.samsung.android.brightnessbackupservice","label":"BrightnessBNR","description":"Backup brightness configuration\n May be used when reverting the brightness from a mode, routine, or powersaving mode.","removal":"caution","type":"oem"},{"id":"com.samsung.android.calendar","description":"Samsung Calendar App\n","removal":"replace","suggestions":"calendars","type":"oem"},{"id":"com.samsung.android.callassistant","description":"Appears to handle a whole bunch of AI services related to Gallery and Phone. Can break stuff in OneUI 6.1+","removal":"caution","type":"oem"},{"id":"com.samsung.android.callbgprovider","label":"CallBGProvider","description":"Call Background.\nIt is customizing background theme of calling.\nSCloud, backup things found in code.","removal":"caution","type":"oem"},{"id":"com.samsung.android.camerasdkservice","label":"SCameraService","description":"Camera SDK, probably used for the main camera app?\nThere are a lot of features that I do not know if it is not only available to developers.\nApp has many permissions like arcsoft, smartcamerascan, supernight, too many to write here.\nA strange app with many things sometimes mentioned in the code that it is used for calibration.","removal":"caution","type":"oem"},{"id":"com.samsung.android.cameraxservice","label":"SCameraXService","description":"It's hidden app for testing camera things also it's camera calibration","removal":"delete","type":"oem"},{"id":"com.samsung.android.carkey","description":"Digital Key Framework\nFrameworks to CarKey app, only China.","removal":"delete","type":"oem"},{"id":"com.samsung.android.carlink","description":"CarLife app only for China.","removal":"delete","type":"oem"},{"id":"com.samsung.android.chnfileshare.kit","description":"S Share\nChinese app to Secure transfer files using bluetooth.","removal":"delete","type":"oem"},{"id":"com.samsung.android.cidmanager","label":"Configuration update","description":"Sets the CSC for the phone depending on the SIM card. This is responsible for forcing you to restart when to activate the so-called features when inserting new SIM card. It may also revert band settings after OTA update in some cases.","removal":"caution","warning":"It may be required for receiving OTA updates, currently unknown.","type":"oem"},{"id":"com.samsung.android.clipboarduiservice","description":"User interface for clipboard\n","removal":"unsafe","type":"oem"},{"id":"com.samsung.android.cmfa.framework","label":"CMFA Framework","description":"Continous Multi-Factor Authentication Framework\nSeems useless if you aren't logged-in to a Samsung Account.","web":["https://docs.samsungknox.com/dev/knox-sdk/appendix/additional-advanced-access-control-enhancements/"],"removal":"replace","type":"oem"},{"id":"com.samsung.android.coldwalletservice","label":"Samsung Blockchain Keystore","description":"Secure and convenient place to manage your private key used for cryptocurrency transactions.","removal":"delete","type":"oem"},{"id":"com.samsung.android.communicationservice","description":"Message Service.\nNeeded for SMS/MMS communication\n","removal":"unsafe","type":"oem"},{"id":"com.samsung.android.contacts","description":"Samsung contacts app","removal":"replace","suggestions":"contacts","type":"oem"},{"id":"com.samsung.android.container","label":"ContainerService","description":"Creates container. Maybe not very useful. Someone can test?","removal":"caution","type":"oem"},{"id":"com.samsung.android.coreapps","description":"Samsung Enhanced features\nFiles and profiles sharing feature. It may be called \"Enhanced messaging\".\nUsing this service lets Samsung to automatically collect your phone number, contact list and messages\nhttps://forums.androidcentral.com/samsung-galaxy-s6-edge/523172-enhanced-features.html\nhttps://www.samsung.com/za/support/mobile-devices/what-is-enhanced-messaging/\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.da.daagent","description":"Samsung dual messenger (https://www.samsung.com/global/galaxy/what-is/dual-messenger/)\nAllows you to use two separate accounts for the same app.\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.dbsc","description":"Galaxy Service Setup\nDevice-Based Service Consent. Pairs with `com.sec.spp.push` to broadcast new service consent requests to your device.\nReally slimey move by Samsung to name it this in the wake of Google announcing their own DBSC (Device Bound Session Credentials) which will phase out third-party cookies and help secure browsers against cookie theft.\nhttps://blog.chromium.org/2024/04/fighting-cookie-theft-using-device.html\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.dck.timesync","description":"DckTimeSyncService\nOnly logs found","removal":"delete","type":"oem"},{"id":"com.samsung.android.deviceidservice","description":"DeviceIdService\nCollects device id but it is not known what it is used for.\nTake's OAID, VAID, AAID.","removal":"delete","type":"oem"},{"id":"com.samsung.android.dialer","label":"Samsung Phone","description":"Stock Samsung dialer","removal":"caution","type":"oem"},{"id":"com.samsung.android.digitalkey","label":"Digital Key","description":"I found on this app only: Using your phone as a key to open doors. Coming soon! Useless. Not available digital key for users.","removal":"delete","type":"oem"},{"id":"com.samsung.android.dkey","description":"Samsung Wallet Digital Key\nUsed for cars, only China.","removal":"delete","type":"oem"},{"id":"com.samsung.android.dlp.service","description":"SamsungDLPService (KNOX). Old feature. Was replaced by SDP (Sensitive Data Protection)\nData Loss Prevention (DLP) feature\nSDP is good because it allows to have encrypted data at rest (= decryption keys not in RAM) even when your phone is on.\nhttps://docs.samsungknox.com/admin/whitepaper/kpe/sensitive-data-protection.htm \nhttps://docs.samsungknox.com/knox-platform-for-enterprise/admin-guide/sensitive-data-protection.htm\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.dqagent","label":"DQA","description":"DQA = Samsung Device Quality Agent. Logging agent for diagnostic device information.","removal":"delete","type":"oem"},{"id":"com.samsung.android.drivelink.stub","description":"Stub for car mode \nREMINDER : Stub = https://stackoverflow.com/questions/10648280/what-is-stub-and-aidl-for-in-java\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.dsms","label":"Dsms","description":"Hidden diagmonagent logs components.","removal":"delete","type":"oem"},{"id":"com.samsung.android.dynamiclock","label":"Samsung Wallpaper services","description":"Formerly, Dynamic Lockscreen.\nAutomatically changes your Lock screen's wallpaper every time your Galaxy phone wakes up (wallpapers update every 2 weeks).","web":["https://www.samsung.com/us/support/answer/ANS10001593"],"required_by":["com.samsung.systemui.lockstar"],"removal":"caution","warning":"Removing the app breaks LockStar customizations.","type":"oem"},{"id":"com.samsung.android.easysetup","label":"Nearby device scanning","description":"Previously, Samsung Connect Easy Setup, Samsung SmartThings.\nRequires \"com.samsung.android.beaconmanager\" to be useful.","removal":"delete","type":"oem"},{"id":"com.samsung.android.email.composer","description":"another app related to Email app, bad privacy","removal":"delete","type":"oem"},{"id":"com.samsung.android.email.provider","label":"Samsung Email","description":"Samsung email app","web":["https://play.google.com/store/apps/details?id=com.samsung.android.email.provider"],"removal":"replace","suggestions":"email_clients","type":"oem"},{"id":"com.samsung.android.email.sync","description":"Sync to Email app, bad privacy","removal":"delete","type":"oem"},{"id":"com.samsung.android.email.ui","description":"Email app with bad privacy","removal":"delete","type":"oem"},{"id":"com.samsung.android.email.widget","description":"Email Widget on some old samsung phones like S6","removal":"delete","type":"oem"},{"id":"com.samsung.android.emergency","description":"Emergency SOS\nHas a lot of permissions, lets you send SOS messages and calls.","removal":"replace","type":"oem"},{"id":"com.samsung.android.emojiupdater","description":"AR Emoji updater\nThis package has no permission so I wonder how it can update anything.\nSee com.samsung.android.aremoji\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.fast","description":"Samsung Secure Wi-Fi\nSamsung VPN service powered by McAfee\nhttps://www.pcmag.com/news/mcafee-samsung-partner-on-built-in-security-vpn-for-galaxy-s9\nhttps://www.ctrl.blog/entry/what-is-samsung-secure-wi-fi.html\nNote: If you need to use a VPN use something more trustworthy*\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.fingerprint.service","description":"Fingerprint service\n","removal":"replace","type":"oem"},{"id":"com.samsung.android.fmm","description":"Find My Mobile\nTracks down your device when it gets lost. \nLets you remotely lock your device, block access to Samsung Pay and wipe data from the entire device.\nhttps://www.samsung.com/global/galaxy/what-is/find-my-mobile/\nhttps://findmymobile.samsung.com/\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.forest","description":"Digital Wellbeing (old version of com.samsung.android.wellbeing)\nThat's an app for device and app usage tracking and limiting.\nhttps://galaxystore.samsung.com/prepost/000004807357\nNOTE for Dex users: uninstalling this package makes the UI unstable, in particular the taskbar is not loaded and UI continues to crash.","removal":"delete","type":"oem"},{"id":"com.samsung.android.framework.res","description":"This package is part of Samsung’s custom system framework and manages various resources for Samsung-specific features and UI enhancements.","removal":"replace","type":"oem"},{"id":"com.samsung.android.galaxycontinuity","description":"Samsung Flow (https://play.google.com/store/apps/details?id=com.samsung.android.galaxycontinuity)\nIt's a service to 'seamlessly' connect your devices. You can authenticate your Tablet/PC with your smartphone, share content between devices, and sync notifications or view contents from your smartphone on your Tablet/PC.\nYou can turn on the smartphone's Mobile Hotspot to keep your Tablet/PC connected. You can also log in to your Tablet/PC with your biometric data (Iris, Fingerprints) if you register with Samsung Pass.\nhttps://www.samsung.com/levant/support/mobile-devices/what-is-the-samsung-flow-and-how-to-use-it/\n\nHas more than 81 permissions. Not the kind of app you want to keep installed if you don't use it. It increases the attack surface and can potentially compromise a lot of things (including your computer). FYI, 4 CVE has been discovered between 2021 and 2022: https://www.opencve.io/cve?vendor=samsung&product=samsung_flow\n\nPithus analysis: https://beta.pithus.org/report/77216cd6209c73d00332180319249ac6e4ef8479e68d2a21c97a52fdc5f3d62b","removal":"delete","type":"oem"},{"id":"com.samsung.android.gallery.plugins","description":"SamsungGallery Plugins\nChinese mapcamera plugin.","removal":"delete","type":"oem"},{"id":"com.samsung.android.gallery.watch","description":"Samsung Watch gallery app","removal":"replace","type":"oem"},{"id":"com.samsung.android.game.gamehome","description":"Samsung Game Launcher \nhttps://www.samsung.com/global/galaxy/apps/game-launcher/\nAll in one hub for mobiles games\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.game.gametools","label":"Samsung Game Booster","description":"Lets you record and share screenshots of your game-play.","web":["https://www.samsung.com/au/support/mobile-devices/how-to-use-game-tools/"],"removal":"delete","type":"oem"},{"id":"com.samsung.android.game.gos","label":"Samsung Game Optimizing Service","description":"It's supposed to \"improve\" game performance.\nHas a \"Game Intent Service\" that runs occasionally. If uninstalling fails, try disabling.","web":["https://pcgamer.com/samsungs-game-optimization-service-might-be-throttling-the-performance-of-over-10000-apps"],"removal":"delete","type":"oem"},{"id":"com.samsung.android.gametuner.thin","description":"Samsung Game Tuner (https://play.google.com/store/apps/details?id=com.samsung.android.gametuner.thin)\nGame Tuner is advanced setting app. It enables you to change the resolution and frames per second settings\nin mobile games that require tuning for different Android devices, and thereby control heat generation and battery drain.\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.gearoplugin","description":"Gear S Plugin (https://play.google.com/store/apps/details?id=com.samsung.android.gearoplugin)\nPlugin for com.samsung.android.app.watchmanager\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.globalpostprocmgr","description":"GlobalPostProcMgr\nUnknown app, has Draft Recovery.\nUninstalling breaks camera functionality.","removal":"caution","type":"oem"},{"id":"com.samsung.android.gru","description":"Galaxy Resource Updater\nNeeded for galaxy store probably and not sure but also for system updates?","removal":"caution","type":"oem"},{"id":"com.samsung.android.hdmapp","label":"HdmApp","description":"Hypervisor Device Manager App","web":["https://docs.samsungknox.com/devref/knox-sdk/reference/com/samsung/android/knox/hdm/HdmManager.html"],"removal":"delete","type":"oem"},{"id":"com.samsung.android.hmt.vrshell","description":"Gear VR Shell \nGear VR : https://360samsungvr.com/portal/content/about_samsung_vr\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.hmt.vrsvc","description":"Gear VR Service\nSee above.\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.homemode","label":"Daily Board","description":"Show a slideshow of your favourite pictures while your device is charging.","web":["https://play.google.com/store/apps/details?id=com.samsung.android.homemode"],"removal":"delete","type":"oem"},{"id":"com.samsung.android.honeyboard","description":"Samsung keyboard","web":["https://developer.android.com/training/articles/direct-boot"],"removal":"caution","warning":"Do NOT disable if you don't have another keyboard with direct boot mode support, or you'll be stuck at boot (no keyboard to unlock the phone).\nDo NOT remove this package with root if it wasn't first uninstalled with the non-root method.\nRemoving this package also breaks the Accessibility settings on Android 11.","suggestions":"keyboards","type":"oem"},{"id":"com.samsung.android.icecone","description":"Keyboard Content Center\nLets you choose media content (e.g. stickers and music) from the Galaxy Keyboard.\nThis app always runs in background.\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.imagepp","description":"Chat PP\nOnly for China.","removal":"delete","type":"oem"},{"id":"com.samsung.android.incall.contentprovider","label":"CallContentProvider","description":"","removal":"caution","warning":"After removal, some features will not be available: face AR, AR emoji, filter, sticker.","type":"oem"},{"id":"com.samsung.android.incallui","description":"UI when \"being called/in call\". It's basically the screen that shows you who is calling, lets you answer and hang up, switch to speaker, etc\n","removal":"unsafe","type":"oem"},{"id":"com.samsung.android.inputshare","description":"Samsung Multi-Control, allows certain apps to be continued on multiple Galaxy devices (like Internet or Notes)","removal":"delete","type":"oem"},{"id":"com.samsung.android.intelligenceservice2","label":"IntelligenceService2","description":"It seems that this package is a kind of spyware. Very difficult to find information about this.\nSome people say it's linked to Carrier IQ (which is a carrier rootkit for the NSA).\nThis package also have very stranges permissions: READ_PLACE / WRITE_PLACE. I couldn't find any explaination on the web.\nSo either it's a useless samsung package or a spyware. I deleted it and I didn't notice anything bad.","web":["https://en.wikipedia.org/wiki/Carrier_IQ","https://forum.xda-developers.com/showpost.php?s=c85df628dfc39c3a971e6f9cfa98cbb8&p=54071328&postcount=6"],"removal":"delete","type":"oem"},{"id":"com.samsung.android.ipsgeofence","description":"Samsung Visit In app\n\nIPSGeofence\nIPS = Indoor Positioning System.\nThe concept of Indoor Positioning System designates a network of connected devices within a building making it possible to trace the position of another device – and therefore potentially of a person – in environments where GPS systems are \nnot efficient .\nGeofencing is a technique which consists in activating preconfigured actions when a device enters a certain geographical area.\nFor example, a user can use it to automatically turn on Wi-Fi and home lights when their smartphone is detected nearby.\nIn short, if enabled, this app will track your location everywhere and all the time!\nhttps://www.comparitech.com/blog/vpn-privacy/what-is-geofencing-privacy/","removal":"delete","type":"oem"},{"id":"com.samsung.android.keycustomizationinfobackupservice","description":"If you don't use backup service, it's safe to remove.","removal":"delete","type":"oem"},{"id":"com.samsung.android.keyguardwallpaperupdator","description":"Lets you customize your Samsung device with different images (provided by Samsung) on the lock screen. \n","removal":"delete","type":"oem"},{"id":"com.samsung.android.kgclient","description":"Samsung Payment Services\nRemoving this package will LOCK YOU OUT of your device with a full-screen overlay message saying that Device Services was uninstalled in an unauthorised manner. This is persistent upon reboots until a factory data reset is initiated. Filesystem can still be accessed if ADB permissions were granted beforehand.\nUnless you know what you're doing, you shouldn't uninstall this package.","removal":"unsafe","type":"oem"},{"id":"com.samsung.android.kidsinstaller","description":"Samsung Kids Home (https://www.samsung.com/global/galaxy/apps/samsung-kids-home/)\nLets you shape a \"safe environment\" for your child.\nNOTE : You shouldn't give your phone to a child. That bad ! \nhttps://ifstudies.org/blog/a-smartphone-will-change-your-child-in-ways-you-might-not-expect-or-want\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.kmxservice","description":"KmxService\nKnox Matrix. Provides a feature to securely generate and store keys used for end-to-end encryption (E2EE).\nIt encrypts/decrypts data on the user’s device and transmits it to the server.\nThe user’s original data cannot be checked as decryption is impossible from the server, which only has encrypted data, and is safely protected from external attacks.\nhttps://www.apkmirror.com/apk/samsung-electronics-co-ltd/kmxservice/","removal":"replace","type":"oem"},{"id":"com.samsung.android.knox.analytics.uploader","label":"Knox Analytics Uploader","description":"Knox Analytics Uploader\nSends analytics to Samsung.","removal":"delete","type":"oem"},{"id":"com.samsung.android.knox.app.networkfilter","description":"knoxNwFilter\nIs responsible for Bluetooth to work.","removal":"caution","type":"oem"},{"id":"com.samsung.android.knox.attestation","description":"KNOX Attestation\nLets you check the integrity of a Samsung Android device by connecting to a Samsung Attestation server.\nhttps://docs.samsungknox.com/admin/whitepaper/kpe/attestation.htm\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.knox.containeragent","label":"Work profile","description":"Formerly, Workspace.\nProvides an isolated environment to store data (see Secure Folder)\n\nNote: With Knox 3.4, Knox containers are now deprecated and replaced by Android work profiles.\nComunicate with Samsung servers:\n- https://vas.samsungapps.com (App updates)\n- http://cn-ms.samsungapps.com (APK Server).","web":["https://support.samsungknox.com/hc/en-us/articles/115012547907-What-URLs-do-I-have-to-whitelist-to-make-Samsung-apps-work-with-an-authenticated-proxy-"],"removal":"delete","type":"oem"},{"id":"com.samsung.android.knox.containercore","label":"KnoxCore","description":"Providess an isolated environment to store data (see Secure Folder)\n\nNote: With Knox 3.4, Knox containers are now deprecated and replaced by Android work profiles.\nComunicate with Samsung servers:\n- https://vas.samsungapps.com (App updates)\n- http://cn-ms.samsungapps.com (APK Server).","web":["https://support.samsungknox.com/hc/en-us/articles/115012547907-What-URLs-do-I-have-to-whitelist-to-make-Samsung-apps-work-with-an-authenticated-proxy-"],"removal":"delete","type":"oem"},{"id":"com.samsung.android.knox.containerdesktop","description":"Knox Container Desktop\nProvides UI for the work(space) container? \nSee \"com.samsung.android.knox.containeragent\"\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.knox.kpecore","description":"Samsung KPECore\nKPE stands for 'Knox Platform for Enterprise'","removal":"delete","type":"oem"},{"id":"com.samsung.android.knox.mpos","description":"Knox mPOS Enabler offers a range of security/usability features, enabling secure and contactless payments on Samsung devices.\nhttps://play.google.com/store/apps/details?id=com.samsung.android.knox.mpos","removal":"replace","type":"oem"},{"id":"com.samsung.android.knox.pushmanager","description":"KnoxPushManager\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.knox.zt.framework","description":"KnoxZT Framework\nTo learn how you type, Touch Dynamics\nwill collect and analyze sensor data while typing.","removal":"delete","type":"oem"},{"id":"com.samsung.android.livestickers","description":"Deco Pic (accessible through AR Zone)\nCamera app with stickers and snapchat-like filters\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.localeoverlaymanager","label":"Locale Overlay Manager","description":"Has overlays about FOTA updates, Cleaning up files, checking setup complete","removal":"caution","type":"oem"},{"id":"com.samsung.android.location","label":"slocation","description":"Handles GPS queries for some Samsung (Galaxy) apps, such as Camera location tagging. Apps exclusively using raw GPS and/or Google Location API are unaffected.","web":["https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/issues/762#issuecomment-2567149732"],"removal":"replace","type":"oem"},{"id":"com.samsung.android.lool","label":"Device Care","description":"While this package was suspected previously to send lots of data to Chinese servers it has since been debunked - only connects to Chinese server to retrieve database, but still sends some very basic data about the phone like model number, storage capacity, and a randomly generated identifier.","web":["https://play.google.com/store/apps/details?id=com.samsung.android.lool","https://www.reddit.com/r/Android/comments/el99r0/samsung_members_koreas_official_reply_has_arrived/"],"removal":"caution","warning":"Disabling/Removing this package may remove the option to manage Power Saving, Fast Charging & Battery Protection on some devices.","type":"oem"},{"id":"com.samsung.android.mapsagent","label":"Application recommendations","description":"A lot of spying code. I guess it's an app from first-boot setup.","removal":"delete","type":"oem"},{"id":"com.samsung.android.mateagent","description":"Samsung Galaxy Friends is an accessory platform service that allows the user to enjoy a variety of content quickly \nand easily by simply connecting an accessory, without having to install additional applications.\nhttps://developer.samsung.com/codelab/SDC18-experiences/Galaxy-Friends\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.mcfds","label":"Continuity Service","description":"Chat with other peoples. This app is available on Galaxy Store.","web":["https://galaxystore.samsung.com/prepost/000006390343?appId=com.samsung.android.mcfds"],"removal":"delete","type":"oem"},{"id":"com.samsung.android.mcfserver","label":"Samsung Multi Connectivity","description":"See https://gitlab.com/W1nst0n/universal-android-debloater/-/issues/12","removal":"replace","type":"oem"},{"id":"com.samsung.android.mdagent","description":"MdAgent","removal":"replace","type":"oem"},{"id":"com.samsung.android.mdecservice","label":"Call & Message Continuity","description":"Not 100% sure, but by looking at the decompiled Java/Dalvik/A.R.T. code it seems the app provides a way to receive call and SMS on Samsung accessories.\nIn any case it is only useful for Samsung IoT stuff.\nEmbeded Google Firebase analytics.","removal":"delete","type":"oem"},{"id":"com.samsung.android.mdm","description":"MDMApp (Mobile Device Management app)\nUsed to monitor and manage remotely mobile devices.\nFor example locking split-screen, blocking safe mode boot, enabling branding logo in the lock screen, remotely configuring IMAP email...\nMost likely related to KNOX \nhttps://www.samsungknox.com/en/solutions/it-solutions/knox-manage\nhttps://developer.samsung.com/tech-insights/knox/mobile-device-management\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.mdx","description":"Link to Windows Service\nWorks in conjunction with the Microsoft Your Phone app and activates a connection to your PC on Windows\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.mdx.kit","label":"S Share Connectivity","description":"Previously, Mdx Kit Service, MDE Service Framework.\nMDE = Multi Devices Experience.\nFramework for IoT stuff.\nAsks for a LOT of dangerous permissions\nInteracts with \"com.samsung.android.mobileservice\" and \"com.osp.app.signin\"","web":["https://www.samsung.com/levant/multi-device-experience/"],"removal":"delete","type":"oem"},{"id":"com.samsung.android.mdx.quickboard","label":"Media and devices","description":"Kind of a hub for managing the media played on smart devices (e.g play music to 2 Bluetooth devices simultaneously with Dual audio).\nHas a lot of permissions and asks for ACCESS_COARSE_LOCATION, QUERY_ALL_PACKAGES.","web":["https://www.samsung.com/latin_en/support/mobile-devices/media-and-device-feature/"],"removal":"caution","warning":"Removing this package does not prevent you to connect your phones to smart devices, but oddly enough causes the brightness slider in the notification panel to not be displayed in landscape orientation (it's still shown in portrait) on some devices.","type":"oem"},{"id":"com.samsung.android.mediacontroller","description":"Ability to controls phone's audio from your watch.","removal":"replace","type":"oem"},{"id":"com.samsung.android.messaging","description":"Samsung Messaging app\n","removal":"replace","suggestions":"sms","type":"oem"},{"id":"com.samsung.android.messaging.extension.chn","description":"MessagingExtensionCh\nHas too much spying things.","removal":"delete","type":"oem"},{"id":"com.samsung.android.mfi","label":"Galaxy Widget","description":"Just shows you ads of some of Samsung's services or other apps/games.","web":["https://play.google.com/store/apps/details?id=com.samsung.android.mfi"],"removal":"delete","type":"oem"},{"id":"com.samsung.android.mobileservice","label":"Group Sharing","description":"Previously, Samsung Experience Service. The legacy version was similar to Samsung Core Services (citation needed).\nCan remove safely if you don't use any group sharing features (shared albums in gallery, etc.).\nHandles your Samsung account and is needed to use some Samsung apps features.\nIt allows you to use Samsung apps, such as Samsung Health, Samsung Pay, Galaxy Apps, Samsung Members, and SmartThings with your Samsung account credentials.","web":["https://techshift.net/what-is-samsung-group-sharing/"],"removal":"caution","type":"oem"},{"id":"com.samsung.android.mocca","description":"MoccaMobile\nRequire google play services.\nDiagnostics, logs, car crash, better remove this app.","removal":"delete","type":"oem"},{"id":"com.samsung.android.motionphoto.viewer","label":"Motion photo viewer","description":"Can only watch videos on it, also found diagmonagent logs.","removal":"caution","type":"oem"},{"id":"com.samsung.android.mtp","description":"MTP application\nIt doesn't cause a bootloop, but its highly not recommended to remove it.\nThis app is needed for transfer files to PC/Phone.","removal":"unsafe","type":"oem"},{"id":"com.samsung.android.net.wifi.wifiguider","label":"Wi-Fi Tips","description":"Shows a question-mark button next to Wi-Fi networks that failed to connect for any reason, providing info to fix the specific issue. It also has a dedicated Settings activity that lists all possible help-tips, accessible via the \"More tips\" button when reading a tip.\nUpon reinstalling a notification pops up saying \"Analyzing Wi-Fi\" for a few seconds, no idea what it's doing.","removal":"caution","warning":"Removing the app makes it impossible to connect to a Wi-Fi network without a detected internet connection","type":"oem"},{"id":"com.samsung.android.networkdiagnostic","description":"Network Diagnostic\nAutostart after boot. 9 permissions (including ACCESS_FINE_LOCATION : precise GPS location) + 1 unknown permission : SEC_FACTORY_PHONE\nSeems to be telemetry.\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.networkstack","label":"NetworkStackOverlay","description":"I guess it's needed for wifi and it can be important app","removal":"unsafe","type":"oem"},{"id":"com.samsung.android.networkstack.tethering.overlay","description":"Found only wifi configs","removal":"unsafe","type":"oem"},{"id":"com.samsung.android.oneconnect","description":"Samsung Smart Things (https://play.google.com/store/apps/details?id=com.samsung.android.oneconnect)\nLets you manage all your Samsung and SmartThings-compatible devices.\nhttps://www.samsung.com/global/galaxy/apps/smartthings/\n\nProbably needs com.samsung.android.beaconmanager\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.packageinstaller","description":"Removing it is Unsafe if there is no alternative.","removal":"caution","type":"oem"},{"id":"com.samsung.android.peripheral.framework","description":"Peripheral Framework\nIt's knox analytics.","removal":"delete","type":"oem"},{"id":"com.samsung.android.personalpage.service","description":"Private mode (was replaced by Secure Folder)\nhttps://www.samsung.com/uk/support/mobile-devices/what-is-private-mode-and-how-do-i-use-it/\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.photoremasterservice","description":"PhotoRemasterService\nIf removed, Photo Remaster in the gallery won't work.","removal":"replace","type":"oem"},{"id":"com.samsung.android.privacydashboard","description":"Privacy dashboard in settings:\nhttps://insights.samsung.com/2023/08/17/what-is-privacy-dashboard-and-how-does-it-protect-my-data-2/","removal":"replace","type":"oem"},{"id":"com.samsung.android.privateshare","description":"Blockchain-powered file sharing system for Samsung phones.\nWhy blockchain? Because it's a nice buzzword! The privacy policy of this Samsung service is really bad: https://libreddit.spike.codes/r/privacy/comments/rqbb9b/samsung_private_share_is_not_so_private/","removal":"delete","type":"oem"},{"id":"com.samsung.android.provider.filterprovider","label":"com.samsung.android.provider.filterprovider","description":"FilterProvider dependency to Samsung Camera\nProvides access to filters (when you swipe right from the camera app)","removal":"caution","warning":"Samsung camera will crash if this package is deleted.","type":"oem"},{"id":"com.samsung.android.provider.shootingmodeprovider","label":"ShootingModeProvider","description":"Provide camera modes (when you swipe left from the camera app)\nSafe to remove (but it is quite useful)","removal":"replace","suggestions":"cameras","type":"oem"},{"id":"com.samsung.android.provider.stickerprovider","description":"One more package related to camera stickers.\nDO NOT REMOVE THIS IF YOU USE STOCK CAMERA (Samsung camera-app closes after about 4s!) \nadb shell 'pm disable-user com.samsung.android.provider.stickerprovider' can be used as a workaround if you want to stop this running in the background.\n","removal":"replace","type":"oem"},{"id":"com.samsung.android.providers.carrier","label":"Configuration update","description":"App for installing updates in order to get latest features for your SIM card.\nIt is hard to say how useful this app is.","removal":"caution","type":"oem"},{"id":"com.samsung.android.providers.contacts","label":"Samsung Contacts Storage","description":"Likely same as com.android.providers.contacts, but for Samsung phones.","removal":"caution","warning":"Breaks messaging and calling services, as well as contact functionality.","type":"oem"},{"id":"com.samsung.android.providers.context","description":"Spyware\nhttps://www.eteknix.com/samsungs-context-service-may-take-data-collection-surveillance-worrying-levels/\nhttps://www.theinquirer.net/inquirer/news/2328363/samsung-context-service-will-collect-user-data-to-share-with-developers\nContent providers encapsulate data, providing centralized management of data shared between apps.\nhttps://developer.android.com/guide/topics/providers/content-providers.html","removal":"delete","type":"oem"},{"id":"com.samsung.android.providers.factory","label":"FactoryTestProvider","description":"It's used for diagnostics and factory hardware testing.","removal":"caution","warning":"Removal breaks hwmoduletest.","type":"oem"},{"id":"com.samsung.android.providers.media","label":"Samsung Media Storage","description":"Likely same as com.android.providers.media; scans the device for media files and allows permitted apps access to them.","removal":"caution","type":"oem"},{"id":"com.samsung.android.providers.trash","label":"SecTrashProvider","description":"Trash feature for Galaxy phones since One UI 6.0.\nAllows users to delete and restore files from My Files, Gallery, Voice Recordings, etc. directly from the Trash folder in My Files.","removal":"caution","warning":"Removal breaks apps that delete to Trash.","type":"oem"},{"id":"com.samsung.android.rajaampat","label":"Samsung Pay","web":["https://play.google.com/store/apps/details?id=com.samsung.android.rajaampat"],"description":"Samsung Pay Services for Indonesia.","removal":"delete","type":"oem"},{"id":"com.samsung.android.rampart","description":"Auto Blocker\nUses for blocking unknown apps install, commands.","removal":"delete","type":"oem"},{"id":"com.samsung.android.rlc","description":"Samsung Biz Service\nRemote Lock Control? Remote Mobile Manager? It has not needed notification components.","removal":"delete","type":"oem"},{"id":"com.samsung.android.rubin.app","label":"Customization Service","description":"Collects a massive amount of data: basically everything you do on your phone for \"a better user experience\".\nUsed to display customized advertisements about products and services that may be of interest to you.","web":["https://www.samsung.com/us/account/customization-service/"],"removal":"delete","type":"oem"},{"id":"com.samsung.android.samsungpass","description":"Samsung Pass app\nhttps://www.samsung.com/global/galaxy/apps/samsung-pass/\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.samsungpassautofill","description":"Auto Fill for Samsung Pass\nOnce your account information is registered, you can use iris, fingerprint, or face recognition to sign in.\nhttps://www.samsung.com/us/support/answer/ANS00082282/\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.samsungpositioning","description":"Run at startup and ask for an unknown permission SEC_FACTORY_PHONE\nThis package seems to be used for samsung apps needing location.\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.scloud","description":"Samsung Cloud (https://www.samsung.com/us/support/owners/app/samsung-cloud)","removal":"delete","type":"oem"},{"id":"com.samsung.android.scloud.auth","label":"Samsung Cloud Data Relay","description":"Handle authentication for Samsung cloud","removal":"delete","type":"oem"},{"id":"com.samsung.android.scloud.backup","description":"Samsung Backup only for Samsung Cloud","removal":"delete","type":"oem"},{"id":"com.samsung.android.scloud.sync","description":"Samsung cloud synchronisation service","removal":"delete","type":"oem"},{"id":"com.samsung.android.sconnect","label":"Quick connect","description":"(Discontinued) In theory, it lets you connect your phone to a variety of devices over Wifi that support multiple protocols — including Wifi Direct and Miracast — to display photos, video or audio.","web":["https://www.samsung.com/uk/support/tv-audio-video/what-is-screen-mirroring-and-how-do-i-use-it-with-my-samsung-tv-and-samsung-mobile-device/"],"removal":"delete","type":"oem"},{"id":"com.samsung.android.scpm","description":"Samsung Cloud Platform Manager\nRelated to Samsung Cloud. Safe to remove if you don't use it.","removal":"delete","type":"oem"},{"id":"com.samsung.android.scs","label":"Samsung Core Services","description":"For quick and easy provision of the main features used by Samsung applications through application updates rather than through the distribution of software updates.\nProvides improved search, extraction, and intelligence features based on text and images. Used in various Samsung applications such as Camera, Gallery, Messages, Contacts, Settings, and Finder.","web":["https://galaxystore.samsung.com/prepost/000005514204"],"removal":"caution","type":"oem"},{"id":"com.samsung.android.sdk.handwriting","description":"Handwriting Service\nOnly for Samsung Note? \nhttps://www.samsung.com/sg/support/mobile-devices/how-do-you-convert-handwriting-to-text-and-other-formats-using-s-pen-and-samsung-notes/\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.sdk.ocr","description":"It's for OCR (Optical Character Recognition), used among Samsung apps like the gallery, without this, you can't extract text from images.","removal":"replace","type":"oem"},{"id":"com.samsung.android.sdk.professionalaudio.app.audioconnectionservice","description":"AudioConnectionService\nI believe it allows to modulate an audio signal. I didn't find a lot of apps using this package.\nNothing really worrying but safe to remove if you want.\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.sdk.professionalaudio.utility.jammonitor","description":"Professional Audio\nAllows you to create virtual instrument applications with Android.\nhttps://developer.samsung.com/html/techdoc/ProgrammingGuide_ProfessionalAudio.pdf\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.sdk.spenv10","description":"Samsung Pen engine, Pen settings, SPen Engine Update.","removal":"delete","type":"oem"},{"id":"com.samsung.android.sdm.config","description":"Configuration Update for Samsung Deskphone Manager (SDM)\nSDM allows a user to synchronize your smartphone with a IP deskphone\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.secsoundpicker","label":"SecSoundPicker","description":"Ringtone picker","removal":"caution","type":"oem"},{"id":"com.samsung.android.securitylogagent","label":"SecurityLogAgent","description":"Runs in the background and monitors your device for any change to the system partition. It also listens for use of permissions by 3rd-party apps. This is a feature found in Security settings, named 'permission monitor'\nNOTE: When you root your phone, it will constantly tell you that your device is modified.","web":["https://www.androidexplained.com/galaxy-note-9-disable-security-log-agent/","https://samsung.com/levant/support/mobile-devices/what-is-app-permission-monitor-feature-and-how-to-turn-it-off","https://howtogeek.com/332816/how-to-stop-samsungs-app-permission-monitor-from-displaying-notifications"],"removal":"delete","type":"oem"},{"id":"com.samsung.android.server.iris","label":"Iris","description":"Provides iris recognition feature.\nNOTE: removing this package causes biometric prompts in apps to be delayed by 5-8 seconds before appearing. Selecting 'Biometrics and security' in the settings app also locks up the Settings app for about 5-8 seconds but eventually loads and functions as normal.","removal":"delete","type":"oem"},{"id":"com.samsung.android.server.wifi.mobilewips","label":"MobileWips","description":"Detects suspicious activities on Wi-Fi.","removal":"delete","type":"oem"},{"id":"com.samsung.android.server.wifi.mobilewips.client","description":"MobileWips\nDetects suspicious activity on Wi-Fi network.","removal":"replace","type":"oem"},{"id":"com.samsung.android.service.health","description":"Health Platform (https://play.google.com/store/apps/details?id=com.samsung.android.service.health)\n\nIt is a data aggregator. You can use it to link multiple health apps (like Strava, google fit etc) together. This app will unify their collected data and store them all together.\nConstantly phones to Samsung servers.\n\nPithus analysis: https://beta.pithus.org/report/968364daf4fbb1828dfe9d8dbcce6d5f7f9a79522a5267c4be5bba19e6cd88b0","removal":"replace","type":"oem"},{"id":"com.samsung.android.service.livedrawing","description":"Live Message enables you to draw your own animated GIFs or emojis.\nhttps://www.samsung.com/global/galaxy/what-is/live-message/\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.service.peoplestripe","label":"People Edge","description":"Gives you immediate access to your favorite contacts from the edge of your phone.","web":["https://www.samsung.com/global/galaxy/what-is/people-edge/","https://videotron.tmtx.ca/en/topic/samsung_galaxys9/using_people_edge.html"],"removal":"delete","type":"oem"},{"id":"com.samsung.android.service.stplatform","label":"SmartThings Framework","description":"I guess it's app for installing sponsored apps. Useless Framework.","removal":"delete","type":"oem"},{"id":"com.samsung.android.service.tagservice","label":"Tags","description":"Tags in Samsung Gallery app.","removal":"caution","type":"oem"},{"id":"com.samsung.android.service.travel","description":"Samsung Travel Wallpaper (discontinued)\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.setting.multisound","label":"Separate app sound","description":"Used to split audio different apps between phone speaker and bluetooth speaker. Pretty handy.","removal":"caution","type":"oem"},{"id":"com.samsung.android.setupindiaservicestnc","description":"Samsung Services\nResponsible for the persistent notification after every system update if you don't agree to data collection.\nThe only way to dismiss it without agreeing to anything is to click the small text and uncheck all the items in a list. Then the 'Agree' button becomes a 'Skip' button. Removing this package doesn't have any known side effects.","removal":"delete","type":"oem"},{"id":"com.samsung.android.shealthmonitor","description":"Samsung Health Monitor\n\nEnables you to record ECG and blood pressure.","removal":"replace","type":"oem"},{"id":"com.samsung.android.shortcutbackupservice","description":"ShortcutBNR \nRelated to smartSwitch Samsung Cloud features\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.singletake.service","label":"Single Take","description":"It's face unlock I guess.","removal":"caution","type":"oem"},{"id":"com.samsung.android.slinkcloud","description":"Samsung Cloud Gateway\nNEEDED FOR Scloud app\nA cloud storage gateway is designed to provide interoperability between different data protocols used \nin a client (Scloud app)/server cloud architecture. \nMORE INFO : https://searchstorage.techtarget.com/definition/cloud-storage-gateway\n#\nNeeds a lot of permission (including the dangerous one : READ_PHONE_STATE)\nIt means the app has the ability to read the device ID (e.g. IMEI or ESN) and phone number.\nhttps://developer.android.com/reference/android/Manifest.permission#READ_PHONE_STATE\n#\nHardcoded Alibaba (chinese) server IP (42.120.153.17) \nhttps://www.hybrid-analysis.com/sample/2ef5367f700d2644fc51d2cdd8dd0ce97e9a6594cb5b89052537037c5a7aac56?environmentId=200\nhttps://web.archive.org/web/20200604093347/https://www.hybrid-analysis.com/sample/2ef5367f700d2644fc51d2cdd8dd0ce97e9a6594cb5b89052537037c5a7aac56?environmentId=200\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.sm","label":"Samsung Smart Manager","description":"Provides pretty useless optimizing features using Chinese company Qihoo database.\nAutomatically scans and optimizes data usage to preserve battery levels, manage storage and RAM","web":["https://www.privateinternetaccess.com/blog/android-community-worried-about-presence-of-chinese-spyware-by-qihoo-360-in-samsung-smartphones-and-tablets/","https://forum.xda-developers.com/galaxy-note-9/help/samsung-services-dialling-home-to-china-t3894033"],"removal":"delete","type":"oem"},{"id":"com.samsung.android.sm.devicesecurity","description":"Samsung Device security for the Smart Manager app using McAfee antivirus engine.\nThis is the antivirus in Settings -> Device care -> Security\nPrivacy nightmare(LOTS of permissions!) for a bit of security.\nhttps://www.hybrid-analysis.com/sample/05dab93ee2102a2fb6edf16e85750eb1f0189d7b82703c6a00c92cd08d62bb28?environmentId=200\nARCHIVE: https://web.archive.org/web/20200607140002/https://www.hybrid-analysis.com/sample/05dab93ee2102a2fb6edf16e85750eb1f0189d7b82703c6a00c92cd08d62bb28?environmentId=200\n\nSome people reported that without this package they weren't able to install apps anymore BUT I personally removed this and\nI still can install apps.\nI think(not sure) that you can remove this safely if you also remove com.samsung.aasaservice and com.samsung.android.sm","removal":"replace","type":"oem"},{"id":"com.samsung.android.sm.devicesecurity.tcm","description":"Device security\nChinese scanning app for searching viruses.","removal":"delete","type":"oem"},{"id":"com.samsung.android.sm.policy","description":"SCPM (Samsung Cloud Platform Manager) client.\nAllows the app to read data from the Samsung Cloud Platform Manager in order to authenticate your phone and configure your apps and services. Surely linked to Smart Manager. On some devices, the app icon is cloud-shaped.","removal":"unsafe","type":"oem"},{"id":"com.samsung.android.sm.provider","description":"Smart Manager Provider\nSCPM Client? A lot debugging stuff","removal":"delete","type":"oem"},{"id":"com.samsung.android.sm_cn","description":"Device care but in Chinese Samsung phones\nIs different from global, could break some important functions if removed.","removal":"caution","type":"oem"},{"id":"com.samsung.android.smartcallprovider","label":"Samsung Smart Call","description":"Provides caller profile information to help consumers identify incoming calls and block unwanted ones.\nAlso related to the 'local places' feature in Samsung dialer.\nRelies on Hiya (see com.hiya.star)\nTL;DR : really bad for your privacy.","removal":"delete","type":"oem"},{"id":"com.samsung.android.smartface","label":"SmartFaceService","description":"Used to automatically detect faces when using the Samsung camera. It might be related to \"Keep screen on while viewing\" under Motions and gestures in Settings.\nNOTE: This package has nothing to do with face unlock (com.samsung.android.bio.face.service).","removal":"delete","type":"oem"},{"id":"com.samsung.android.smartface.overlay","description":"Not needed overlay to smartface","removal":"delete","type":"oem"},{"id":"com.samsung.android.smartfitting","description":"Smart Fitting Service\nAdapts the size/ratio of the screen. Automatic mode is default but there is a manual mode\nhttps://www.samsung.com/levant/support/mobile-devices/galaxy-s8-s8-plus-what-is-the-smart-fitting-display-for-vod-games/\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.smartmirroring","label":"Samsung Smart View","description":"Enables you to mirror screen your phone to a TV.","web":["https://www.samsung.com/us/apps/smart-view-2/"],"removal":"replace","required_by":["com.samsung.android.video"],"type":"oem"},{"id":"com.samsung.android.smartprovider","description":"Samsung File Provider\nProvides intelligent classification, search and image/video favorite when selecting images/videos to send to friends/Moments/Channels in WeChat app.","removal":"delete","type":"oem"},{"id":"com.samsung.android.smartsuggestions","label":"Smart suggestions","description":"","removal":"delete","type":"oem"},{"id":"com.samsung.android.smartswitchassistant","description":"Samsung SmartSwitch\nLets you transfer your data from your old (Samsung) phone to your new one.\nNeeded for com.sec.android.easyMover?\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.softsim","description":"Softsim Service\nInstalls softsim and it's full Chinese.","removal":"delete","type":"oem"},{"id":"com.samsung.android.spay","label":"Samsung Wallet","description":"Also known as, Samsung Pay.\nSamsung Pay is a mobile payment and digital wallet service by Samsung that lets users make payments using compatible phones and other Samsung-produced devices. Any personal information including transactions may be sold to third-party.\nNOTE: Samsung Wallet is KNOX dependant and will never work again if you root your phone.","web":["https://play.google.com/store/apps/details?id=com.samsung.android.spay","https://en.wikipedia.org/wiki/Samsung_Pay","https://www.sammobile.com/news/samsung-pay-new-privacy-policy-your-data-sold/"],"removal":"delete","dependencies":["com.sec.android.app.samsungapps","com.samsung.android.spayfw"],"type":"oem"},{"id":"com.samsung.android.spayfw","label":"Samsung Pay Framework","description":"Framework for Samsung Wallet.\nSamsung Pay is a mobile payment and digital wallet service by Samsung that lets users make payments using compatible phones and other Samsung-produced devices.","removal":"delete","required_by":["com.samsung.android.spay"],"type":"oem"},{"id":"com.samsung.android.spaymini","label":"Samsung Pay mini","description":"Same service as Samsung Wallet but for online payments only and is available on all compatible android devices (not just Samsung devices).","web":["https://www.samsung.com/in/samsung-pay/mini/"],"removal":"delete","type":"oem"},{"id":"com.samsung.android.spdclient","description":"Security policy updates (part of KNOX)\nUpdates the SELinux policies according to Security Enhancements for Android (SE for Android)\nThere are privacy implications to the data collected by Samsung\nhttps://security.stackexchange.com/questions/161190/does-samsungs-security-enhancements-for-android-offer-anything-for-consumers\nNot mandatory if you know what you are doing and if you don't install software from unknown sources.\nNeeds confirmation but removing this package could change SELinux mode (enforcing by default)\nhttps://source.android.com/security/selinux\n","removal":"replace","type":"oem"},{"id":"com.samsung.android.spdfnote","description":"Write on PDF (https://play.google.com/store/apps/details?id=com.samsung.android.spdfnote)\nPDF annotator\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.sskds","description":"SoterSskdsService\nThis app has something to 'com.tencent.soter.soterserver'.\nOnly china.","removal":"delete","type":"oem"},{"id":"com.samsung.android.stextclassifier","description":"From https://developer.android.com/reference/android/view/textclassifier/TextClassifier:\nInterface for providing text classification related features.\n\nThe TextClassifier may be used to understand the meaning of text, as well as generating predicted next actions based on the text.\n\nSo it got something to do with text/spelling correction? But a samsung implementation of it. It needs some further testing, so far it doesn't affect even the auto-correct.\nNote: this app has no permission and doesn't run in background when not in used","removal":"caution","type":"oem"},{"id":"com.samsung.android.stickercenter","description":"Sticker center. Used to retrieve stickers from the web in the camera app.\nhttps://developer.samsung.com/galaxy/stickers\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.stickerplugin","description":"StickerPlugin\nNot sure if this package also provides stickers for camera. I don't have it so I can't test\nhttps://developer.samsung.com/galaxy/stickers\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.storage.watchstoragemanager","description":"Storage manager. DO NOT REMOVE THIS","removal":"unsafe","type":"oem"},{"id":"com.samsung.android.ststub","label":"SmartThings","description":"Allows users to control, automate, and monitor their home environment via mobile device.","web":["https://en.wikipedia.org/wiki/SmartThings"],"removal":"delete","type":"oem"},{"id":"com.samsung.android.sume.nn.service","description":"Provides the photo remaster feature in the Gallery app. Has the camera permission and can access all your medias but performs its job locally on the device.\n\nhttps://www.samsung.com/au/support/mobile-devices/remastering-photos-on-samsung-phone/","removal":"caution","type":"oem"},{"id":"com.samsung.android.svcagent","label":"SVC Agent","description":"The APK has various icons in it + DiagMon library (telemetry). It has full access to internet and occasionally runs a MainService for many consecutive hours.","removal":"delete","type":"oem"},{"id":"com.samsung.android.svoice","description":"Samsung Voice (S Voice) was replaced by bixby on Samsung Galaxy S8(+) and newer phones.\nVirtual mobile personal assistant capable of running a basic tasks through voice command alone.\nhttps://www.samsung.com/global/galaxy/what-is/s-voice/\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.svoiceime","description":"Samsung voice input \nVoice input powered by Bixby. See above.\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.tadownloader","label":"TADownloader","description":"Seems to check if a trusted application needs an update and downloads it. \nThis package probably does more than that. There is a LOT of lines of code (obfuscated obviously)\nIt was used to push an update to fix a security issue with the fingerprint sensor in 2019.\nSeems to be only used for biometrics stuff\nThere is Samsung analytics inside. You may want to remove it if you don't use biometrics authentication.\n","web":["https://libredd.it/r/galaxys10/comments/bcy93f/adb_how_to_get_the_fingerprint_update_pushed_to/"],"removal":"replace","type":"oem"},{"id":"com.samsung.android.tapack.authfw","label":"AuthFw TaPack","description":"Authentication Framework for Trusted Application? (don't know what 'Pack' could mean)\nHard to know what this app really does. Seems to be an assets provider used by com.samsung.android.tadownloader.","removal":"caution","type":"oem"},{"id":"com.samsung.android.tencentwifisecurity","description":"Chinese Tencent flags risky networks and prevent to connect to them.","removal":"delete","type":"oem"},{"id":"com.samsung.android.themecenter","label":"Galaxy Themes Service","description":"Runs at startup and allows you to apply themes from \"com.samsung.android.themestore\". On some devices, it provides the built-in wallpaper images to the Theme Store.\nIt will enable itself if you disable it, so try uninstalling (may throw an error).\nHas of lot of permissions (including INTERNET and INSTALL_PACKAGES) and connects to Samsung domains for analytics.","web":["https://beta.pithus.org/report/973ba78ddd74a13dcf5268e980010a64ba42a3d2a1c4c62df277ead5a17cd10c"],"removal":"caution","type":"oem"},{"id":"com.samsung.android.themestore","label":"Galaxy Themes","description":"Forces you to login to a Samsung Account if you want to download ANY content (icons, wallpapers, themes, etc.). If you already downloaded something, you can apply it \"freely\".\nNot needed for Wallpaper API, so any app (Samsung or not) can set Home & Lock wallpapers. However, on some devices, this app is required to view/apply built-in wallpapers (in that case, it requires \"com.samsung.android.themecenter\" service).","removal":"caution","type":"oem"},{"id":"com.samsung.android.timezone.autoupdate_N","description":"This app handles automatic updates for time zone information on Samsung devices, ensuring accurate timekeeping across different regions.","removal":"replace","type":"oem"},{"id":"com.samsung.android.timezone.autoupdate_O","description":"Samsung Time Zone Updater\nUsed to automatically detect appropriate timezone\nREMOVING THIS WILL BOOTLOOP YOUR DEVICE\n","removal":"unsafe","type":"oem"},{"id":"com.samsung.android.timezone.data.P","description":"Samsung timezone data?","removal":"unsafe","type":"oem"},{"id":"com.samsung.android.timezone.data.updater","description":"Samsung Time Zone Updater\nUsed to automatically detect appropriate timezone.\nA similar Samsung package apparently bootloops the device if removed, so be careful.","removal":"unsafe","type":"oem"},{"id":"com.samsung.android.timezone.data_Q","description":"Stores timezone data?\n","removal":"unsafe","type":"oem"},{"id":"com.samsung.android.timezone.updater","description":"Time Zone Updater\nUsed to detect or set timezone.","removal":"caution","type":"oem"},{"id":"com.samsung.android.tncpage","description":"TNCPageCN\nIt's app for enhance lock screen.","removal":"delete","type":"oem"},{"id":"com.samsung.android.tripwidget","description":"Discontinued package (used in Galaxy S4) handling trip wallpaper widget.\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.tzdata.update_M","description":"Samsung Timezone Data Updater","removal":"replace","type":"oem"},{"id":"com.samsung.android.uds","description":"Ultra data saving\nHave boring notifications and theres no point of keeping this app.\nhttps://play.google.com/store/apps/details?id=com.samsung.android.uds","removal":"delete","type":"oem"},{"id":"com.samsung.android.unifiedprofile","label":"My Profile","description":"Related to Samsung Members?","removal":"delete","type":"oem"},{"id":"com.samsung.android.universalswitch","description":"Universal Switch lets you designate certain touches or gestures to control specific actions on your phone. \nhttps://www.samsung.com/uk/accessibility/mobile-universal-switch/\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.uwb","label":"UwbApplication","description":"It's used for SmartThings and new Smart Tags. This app is bloated.","removal":"delete","type":"oem"},{"id":"com.samsung.android.vdc","description":"Virtual Device Center\nIt's Chinese app for Video calls.","removal":"delete","type":"oem"},{"id":"com.samsung.android.vdcservice","description":"This app depends on Virtual Device Center app.\nHas network connection frameworks.","removal":"replace","type":"oem"},{"id":"com.samsung.android.video","label":"Samsung Video Player","dependencies":["com.samsung.android.smartmirroring"],"description":"Default video Player for Samsung devices.","web":["https://galaxystore.samsung.com/prepost/000003980724?appId=com.samsung.android.video"],"removal":"replace","suggestions":"video_players","type":"oem"},{"id":"com.samsung.android.visionarapps","description":"\"AR apps\"\nNot really sure what this is, but the icon is Bixby as an eye so I assume it's for accessing AR stuff through Bixby.\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.visioncloudagent","description":"VisionCloudAgent\nCloud Agent is a service which automatically upload on the cloud the photos you take on your phone. It connects to your \"Samsung account\".\nIt is related to Dropbox.\nGiven the Vision in the package name there is a link with Bixby.\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.visionintelligence","description":"Bixby Vision\nAugmented reality camera that can identify objects in real-time and potentially offer the user\nto purchase them online, translate text, read QR codes, and recognize landmarks. \nhttps://www.samsung.com/global/galaxy/apps/bixby/vision/\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.visualars","description":"Smart Touch Call.\nIt allows the user to \"Get voice and Screen interactive service when you call a customer service\".\nBloated with Google API stuff","removal":"delete","type":"oem"},{"id":"com.samsung.android.voc","label":"Samsung Members","description":"The other version is \"com.samsung.oh\".","web":["https://play.google.com/store/apps/details?id=com.samsung.android.voc"],"removal":"delete","type":"oem"},{"id":"com.samsung.android.voicewakeup","description":"Voice wake-up for using Bixby\nhttps://www.samsung.com/us/support/answer/ANS00080448/\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.vtcamerasettings","label":"Video call effects","description":"Keep the focus on you during video calls by blurring the background or covering it with an image or color.\nUnfortunately, only supports a few apps (Google Meet, WhatsApp, Zoom).","removal":"delete","type":"oem"},{"id":"com.samsung.android.wallpaper.res","description":"Pre-installed wallpapers\nRemoving this package causes the background to be transparent (black, but the system will see it as white unless you use a custom wallpaper).","removal":"replace","type":"oem"},{"id":"com.samsung.android.watch.alarm","description":"Samsung Alarm app.","removal":"delete","type":"oem"},{"id":"com.samsung.android.watch.cameracontroller","description":"Mirrors phone's camera to your watch. I can't find a use case for my usage. Safe to remove.","removal":"delete","type":"oem"},{"id":"com.samsung.android.watch.compass","description":"Samsung Compass app.","removal":"delete","type":"oem"},{"id":"com.samsung.android.watch.findmyphone","description":"The phone will start ringing, if connected to watch via BT or WiFi, when pressing 'start ringing' on the watch.","removal":"replace","type":"oem"},{"id":"com.samsung.android.watch.findmywatch","description":"The watch will start ringing, if connected to phone via BT or WiFi, when pressing 'start ringing' on the phone. Also fetches location and is able to lock or factory reset.","removal":"replace","type":"oem"},{"id":"com.samsung.android.watch.flashlight","description":"Samsung Flashlight","removal":"replace","type":"oem"},{"id":"com.samsung.android.watch.runestone.app","description":"Customization Service from Samsung. Provides customized content and recommendations. Collects a lot of personal information.\nSee: https://www.samsung.com/us/account/customization-service/\n\nPithus analysis: https://beta.pithus.org/report/0f26752e636a9689bf0603e6023939e23a8cbd7197dea7b44c7ac93e2a930c24","removal":"delete","type":"oem"},{"id":"com.samsung.android.watch.screencapture","description":"Provides the ability to take screenshots from the smart watch.","removal":"replace","type":"oem"},{"id":"com.samsung.android.watch.stopwatch","description":"Samsung Stopwatch","removal":"delete","type":"oem"},{"id":"com.samsung.android.watch.timer","description":"Timer app from Samsung.","removal":"delete","type":"oem"},{"id":"com.samsung.android.watch.watchface.analogmodular","description":"Preinstalled watchface.","removal":"replace","type":"oem"},{"id":"com.samsung.android.watch.watchface.analoguefont","description":"Preinstalled watchface.","removal":"replace","type":"oem"},{"id":"com.samsung.android.watch.watchface.animal","description":"Preinstalled watchface.","removal":"replace","type":"oem"},{"id":"com.samsung.android.watch.watchface.aremoji","description":"Preinstalled watchface.","removal":"replace","type":"oem"},{"id":"com.samsung.android.watch.watchface.basicclock","description":"Preinstalled watchface.","removal":"replace","type":"oem"},{"id":"com.samsung.android.watch.watchface.basicdashboard","description":"Preinstalled watchface.","removal":"replace","type":"oem"},{"id":"com.samsung.android.watch.watchface.bespoke","description":"Preinstalled watchface.","removal":"replace","type":"oem"},{"id":"com.samsung.android.watch.watchface.bitmoji","description":"Preinstalled watchface.","removal":"replace","type":"oem"},{"id":"com.samsung.android.watch.watchface.boldindex","description":"Preinstalled watchface.","removal":"replace","type":"oem"},{"id":"com.samsung.android.watch.watchface.companionhelper","description":"Watchfaces fail to load without this. Removing it also breaks editing and changing watchfaces.","removal":"caution","type":"oem"},{"id":"com.samsung.android.watch.watchface.digitalfont","description":"Preinstalled watchface.","removal":"replace","type":"oem"},{"id":"com.samsung.android.watch.watchface.digitalmodular","description":"Preinstalled watchface.","removal":"replace","type":"oem"},{"id":"com.samsung.android.watch.watchface.dualwatch","description":"Preinstalled watchface.","removal":"replace","type":"oem"},{"id":"com.samsung.android.watch.watchface.dynamicfont","description":"Preinstalled watchface.","removal":"replace","type":"oem"},{"id":"com.samsung.android.watch.watchface.emergency","description":"Watchface in the emergency launcher.","removal":"replace","type":"oem"},{"id":"com.samsung.android.watch.watchface.gradientfont","description":"Preinstalled watchface.","removal":"replace","type":"oem"},{"id":"com.samsung.android.watch.watchface.healthmodular","description":"Preinstalled watchface.","removal":"replace","type":"oem"},{"id":"com.samsung.android.watch.watchface.infomodular","description":"Preinstalled watchface.","removal":"replace","type":"oem"},{"id":"com.samsung.android.watch.watchface.large","description":"Preinstalled watchface.","removal":"replace","type":"oem"},{"id":"com.samsung.android.watch.watchface.livewallpaper","description":"Preinstalled watchface.","removal":"replace","type":"oem"},{"id":"com.samsung.android.watch.watchface.mypebble","description":"Preinstalled watchface.","removal":"replace","type":"oem"},{"id":"com.samsung.android.watch.watchface.myphoto","description":"Preinstalled watchface.","removal":"replace","type":"oem"},{"id":"com.samsung.android.watch.watchface.mystyle","description":"Preinstalled watchface.","removal":"replace","type":"oem"},{"id":"com.samsung.android.watch.watchface.premiumanalog","description":"Preinstalled watchface.","removal":"replace","type":"oem"},{"id":"com.samsung.android.watch.watchface.simpleanalogue","description":"Preinstalled watchface.","removal":"replace","type":"oem"},{"id":"com.samsung.android.watch.watchface.simpleclassic","description":"Preinstalled watchface.","removal":"replace","type":"oem"},{"id":"com.samsung.android.watch.watchface.simplecomplication","description":"Preinstalled watchface.","removal":"replace","type":"oem"},{"id":"com.samsung.android.watch.watchface.superfiction","description":"Preinstalled watchface.","removal":"replace","type":"oem"},{"id":"com.samsung.android.watch.watchface.tickingsound","description":"Ticking sound on watchfaces that support it.","removal":"replace","type":"oem"},{"id":"com.samsung.android.watch.watchface.together","description":"Preinstalled watchface.","removal":"replace","type":"oem"},{"id":"com.samsung.android.watch.watchface.typography","description":"Preinstalled watchface.","removal":"replace","type":"oem"},{"id":"com.samsung.android.watch.watchface.weather","description":"Preinstalled watchface.","removal":"replace","type":"oem"},{"id":"com.samsung.android.watch.weather","label":"Weather","description":"Weather application from Samsung.","removal":"replace","suggestions":"weather_apps","type":"oem"},{"id":"com.samsung.android.watch.worldclock","description":"Worldclock app. This also includes a widget, displaying time in different time zones.","removal":"delete","type":"oem"},{"id":"com.samsung.android.wcmurlsnetworkstack","description":"China URL connection. generate_204 (it's only config, not sure if it's needed for checking wifi connection)","removal":"caution","type":"oem"},{"id":"com.samsung.android.wcs.exstention","description":"Samsung Internet Extensions\nSamsung Internet for Android allows users to customize their browsing experience by installing extensions, which are additional software packages that add new features and functionality to the browser and help developers offer tailored services to users on mobile devices.\n\nNOTE: Disabling this broke the UI on my Watch5 for some reason so PROCEED WITH CAUTION.","removal":"replace","type":"oem"},{"id":"com.samsung.android.wear.blockednumber","description":"Blocked number storage. Doesn't affect the dialer or contacts.","removal":"caution","type":"oem"},{"id":"com.samsung.android.wear.contacts.sync","description":"Handles 'open on phone' events. Also, settings often crash when this is uninstalled.","removal":"caution","type":"oem"},{"id":"com.samsung.android.wear.musictransfer","description":"Used to sync music with the watch.","removal":"replace","type":"oem"},{"id":"com.samsung.android.wear.shealth","description":"Samsung Health app for WearOS.","removal":"replace","type":"oem"},{"id":"com.samsung.android.wearable.samsungaccount","description":"Samsung account settings. Breaks settings app if uninstalled.","removal":"caution","type":"oem"},{"id":"com.samsung.android.wearable.setupwizard","description":"Samsung Wearable Setup Wizard\nThe first time you turn your device on, a Welcome screen is displayed. It guides you through the basics of setting up your device.\nIt's the setup for Samsung services.\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.weather","label":"Samsung weather forecast","description":"Unified Daemon app. Named \"Weather\" on some Samsung Galaxy devices.\nProvides support for a number of different apps on your device. These include the Weather, Yahoo Finance and Yahoo News apps amongst others. The data is used by apps such as the Alarm, Calendar app and the Camera (including the \"Scan QR code\" Quick-Panel tile). If the display-name is \"Weather\", it doubles as the Samsung Weather app, which provides weather widgets.","removal":"replace","suggestions":"weather_apps","type":"oem"},{"id":"com.samsung.android.wellbeing","description":"Digital Wellbeing\nThat's an app for device and app usage tracking and limiting.\nhttps://galaxystore.samsung.com/prepost/000004807357","removal":"delete","type":"oem"},{"id":"com.samsung.android.widget.pictureframe","description":"Samsung gallery widget, shows your selected photos as widget","removal":"replace","type":"oem"},{"id":"com.samsung.android.widgetapp.yahooedge.finance","description":"Special edge panel widget for Yahoo Finance\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.widgetapp.yahooedge.sport","description":"Special edge panel widget for Yahoo Sport\n","removal":"delete","type":"oem"},{"id":"com.samsung.android.wifi.ai","description":"No activities, provides WiFi diagnostics, has several ai models that it is not clear how useful they are.","removal":"replace","type":"oem"},{"id":"com.samsung.android.wifi.decrease.scan.interval.resources","description":"Needed for WiFi.","removal":"unsafe","type":"oem"},{"id":"com.samsung.android.wifi.h2e.resources","description":"needed for wifi, has config values for that","removal":"unsafe","type":"oem"},{"id":"com.samsung.android.wifi.increase.scan.interval.resources","description":"Needed for WiFi.","removal":"unsafe","type":"oem"},{"id":"com.samsung.android.wifi.p2paware.resources","description":"Related to WiFi Direct","removal":"replace","type":"oem"},{"id":"com.samsung.android.wifi.resources","description":"Needed for WiFi.","removal":"unsafe","type":"oem"},{"id":"com.samsung.android.wifi.resources.qc","description":"Needed for WiFi.","removal":"unsafe","type":"oem"},{"id":"com.samsung.android.wifi.resources.wifilock","description":"Needed for WiFi.","removal":"unsafe","type":"oem"},{"id":"com.samsung.android.wifi.softap.resources","description":"Needed for Wi-Fi.","removal":"unsafe","type":"oem"},{"id":"com.samsung.android.wifi.softapwpathree.resources","description":"","removal":"caution","type":"oem"},{"id":"com.samsung.android.wifi.wapi.resources","description":"Needed for WiFi.","removal":"unsafe","type":"oem"},{"id":"com.samsung.android.wifinetworkdiagnostics","description":"WifiNetworkDiagnostics\nDiagnostics network connection.","removal":"delete","type":"oem"},{"id":"com.samsung.app.highlightplayer","description":"Samsung Story Video Editor\nLets you edit your videos stories \n","removal":"delete","type":"oem"},{"id":"com.samsung.app.jansky","description":"Multi-lines settings\nLets you have multiple virtual phone numbers.\nThis feature is only available on some US carrier-locked devices\nhttps://www.reddit.com/r/GalaxyS8/comments/6esiub/tmobile_s8s8_multiline_setting_is_awesome/did2pur/\n","removal":"delete","type":"oem"},{"id":"com.samsung.app.newtrim","label":"Editor Lite","description":"Formerly, Samsung Video Trimmer.\nLets you quickly trim video files (from the gallery “Edit -> Studio -> Video Trimmer”). This trimmer is both imprecise and inaccurate, ffmpeg is much better.","removal":"replace","type":"oem"},{"id":"com.samsung.app.slowmotion","description":"Slowmotion mode in camera app\n","removal":"replace","type":"oem"},{"id":"com.samsung.attribution","description":"Related to data collection (?):\nhttps://developer.samsung.com/galaxy-store/galaxy-store-statistics/user-attribution.html","removal":"delete","type":"oem"},{"id":"com.samsung.carrier.logcollector","description":"it's the same like diagmonagent, collects dumplogs","removal":"delete","type":"oem"},{"id":"com.samsung.chn.apps.devicemanagement","description":"AutoLoginService\nRegister device with China Telecom. Only for China.","removal":"delete","type":"oem"},{"id":"com.samsung.clipboardsaveservice","description":"Clipboard Save service saves all the content you saved in the clipboard (clipboard history)\nIf you remove this you will still be able to copy/cust/past but a new content in clipboard will replace the current content.\nIn short : there will no longer be a history.\n","removal":"replace","type":"oem"},{"id":"com.samsung.cmfa.AuthTouch","description":"Continuous Multi-Factor Authentication AuthTouchService\nRelated to `com.samsung.android.cmfa.framework`.","removal":"replace","type":"oem"},{"id":"com.samsung.cmh","description":"CMH Provider is a dependency for the the samsung gallery app. This package asks for a lot of permissions. \nIt seems to be be used for cloud and story stuff in the gallery and also seems needed for content recognition.\nHas the same shared user id as com.samsung.faceservice, com.samsung.mlp, com.samsung.mpl\n \nNOTE : On some phone models, deleting this package can also prevent to preview photos from the camera app.\nSeems to trigger com.samsung.faceservice, com.samsung.mlp, com.samsung.mpl when needed.\n","removal":"replace","type":"oem"},{"id":"com.samsung.commonimsservice","description":"Stat notify volte","removal":"replace","type":"oem"},{"id":"com.samsung.crane","description":"Call+ (https://support.vodafone.co.uk/Vodafone-apps/Call-and-Message/60900956/What-is-Call.htm)\nCall+ features on Samsung dialer\nNOTE: I have the feeling that these features are carrier/country dependant because I don't have them. But I have this package anyway.\n","removal":"delete","type":"oem"},{"id":"com.samsung.daydream.customization","description":"Samsung customization for Google Daydream VR headset (https://arvr.google.com/daydream/)\nNOTE : Google discontinued Daydream in 2019 and it no longer works on Android 10 Samsung devices\n","removal":"delete","type":"oem"},{"id":"com.samsung.dcmservice","description":"Hard to find what it really does but I do know what DCM is in telecommunication. It means Dual Carrier Modulation.\nTo stay simple, we use signal modulation to transfer information. DCM can be seen as an enhancement to conventional QPSK modulation\nthat expand the coverage and robustness of an outdoor hotspot.\nhttps://www.ekahau.com/wp-content/uploads/2017/03/Webinar-slides-802.11ax-Sneak-Peek-%E2%80%93-The-Next-Generation-Wi-Fi.pdf\nNot a good idea to remove this unless it only impacts samsung apps. Need testing.\n","removal":"replace","type":"oem"},{"id":"com.samsung.desktopsystemui","label":"Samsung DeX System UI","description":"Extends your smartphone into a \"desktop computing experience\".","web":["https://developer.samsung.com/samsung-dex/how-it-works"],"removal":"delete","type":"oem"},{"id":"com.samsung.discover","description":"It shows up as a pane on OneUI home and prompts you to install all sorts of random apps. OneUI works perfectly fine if you uninstall these and simply removes the discover pane.","removal":"delete","required_by":["com.samsung.discover.sep"],"type":"oem"},{"id":"com.samsung.discover.sep","description":"Related to `com.samsung.discover` but doesnt seem to do much on its own. Safe to remove without any effect.","removal":"delete","dependencies":["com.samsung.discover"],"type":"oem"},{"id":"com.samsung.ecomm","label":"Shop Samsung","description":"App where you can buy all (and only) Samsung products.","web":["https://play.google.com/store/apps/details?id=com.samsung.ecomm","https://www.samsung.com/us/explore/shop-samsung-app/"],"removal":"delete","type":"oem"},{"id":"com.samsung.ecomm.global.in","description":"Samsung Shop (https://play.google.com/store/apps/details?id=com.samsung.ecomm)\nApp where you can buy all (and only) Samsung products.\nhttps://www.samsung.com/us/explore/shop-samsung-app/\n","removal":"delete","type":"oem"},{"id":"com.samsung.emergencyreporthelper","description":"Only for KOREA emergency report.\nAlso no activities, only warn.","removal":"delete","type":"oem"},{"id":"com.samsung.enhanceservice","description":"Enhanced service is the process for Samsung cloud messaging (equivalent to iMessage on iOS).\nMessages on Samsung phones can be transmitted through either the network carrier or the non-archived Samsung service \n(which is transmitted over wireless data).\nThis features is available in stock samsung SMS app settings.\n","removal":"delete","type":"oem"},{"id":"com.samsung.euicc","label":"SamsungEuiccService","description":"eUICC refers to the architectural standards published by GSMA or implementations of those standard for eSIM, a device used to securely store one or more SIM card profiles, which are the unique identifiers and cryptographic keys used by service providers to uniquely identify and securely connect to mobile network devices.","web":["https://en.wikipedia.org/wiki/EUICC"],"removal":"caution","type":"oem"},{"id":"com.samsung.faceservice","description":"Face service detection\nAnalyzes all the photos in the Samsung Gallery to detect human faces using Samsung’s built-in face detection technology. Once FaceService identifies that the photo contains a face, it shows a button that allows users to add name tags to the photo and create a People Album of similar photos by selecting the name tag.\n\nSame shared user id as com.samsung.ipservice, com.samsung.mlp, com.samsung.cmh\nNeeded for face recognition in the Gallery\nNOTE : Removing this package does not break face unlock\n","removal":"delete","type":"oem"},{"id":"com.samsung.felicalock","description":"FeliCa Password NFC things, not needed","removal":"delete","type":"oem"},{"id":"com.samsung.fresco.logging","description":"Fresco Logging Service\nFresco is an android library for managing images and the memory they use (https://github.com/facebook/fresco)\n","removal":"delete","type":"oem"},{"id":"com.samsung.gamedriver.ex2100","label":"Samsung Exynos2100 GameDriver","description":"","removal":"unsafe","type":"oem"},{"id":"com.samsung.gamedriver.sm8250","label":"Samsung SM8250 GameDriver","description":"","removal":"unsafe","type":"oem"},{"id":"com.samsung.gamedriver.sm8550","label":"Samsung SM8550 GameDriver.","description":"","removal":"unsafe","type":"oem"},{"id":"com.samsung.gpuwatchapp","label":"GPUWatch","description":"In this app, I only found GPU dumping mode, dev mode, game debugging. Useless.","removal":"delete","type":"oem"},{"id":"com.samsung.groupcast","description":"Samsung Group Play (discontinued)\nAllows you to share pictures , documents and music files with many people at same time if everyone is connected to a Wi-Fi network. \nhttps://www.samsung.com/in/support/mobile-devices/what-is-group-play-in-samsung-smartphones/\n","removal":"delete","type":"oem"},{"id":"com.samsung.helphub","description":"Not sure if this package still exist.\nProvide help \n","removal":"delete","type":"oem"},{"id":"com.samsung.hidden.china","description":"China Hidden Menu\nLast call, secret code: 319712358.","removal":"delete","type":"oem"},{"id":"com.samsung.hiddennetworksetting","description":"Set of hidden network settings (inlcuding frequency band choice).\nHow to see these settings: https://forum.xda-developers.com/galaxy-note-8/help/q-hidden-network-settings-pie-t3914421/page4","removal":"replace","type":"oem"},{"id":"com.samsung.hongbaoassistant","description":"Hongbao Assistant\nChinese app.","removal":"delete","type":"oem"},{"id":"com.samsung.hs20provider","description":"only found useless hotspot logs","removal":"delete","type":"oem"},{"id":"com.samsung.huxplatform","description":"","removal":"caution","type":"oem"},{"id":"com.samsung.ims.smk","label":"SimMobilityKit","description":"VoLTE calling, IMS.","removal":"caution","type":"oem"},{"id":"com.samsung.inputshare","description":"MultiControl\nIts use is to connect 2 devices and control one with the other.","removal":"delete","type":"oem"},{"id":"com.samsung.internal.systemui.navbar.gestural_no_hint","description":"Gestural Navigation Bar","removal":"caution","type":"oem"},{"id":"com.samsung.internal.systemui.navbar.gestural_no_hint_extra_wide_back","description":"","removal":"caution","type":"oem"},{"id":"com.samsung.internal.systemui.navbar.gestural_no_hint_narrow_back","description":"","removal":"caution","type":"oem"},{"id":"com.samsung.internal.systemui.navbar.gestural_no_hint_wide_back","description":"","removal":"caution","type":"oem"},{"id":"com.samsung.internal.systemui.navbar.sec_gestural","description":"Gestural Navigation Bar","removal":"caution","type":"oem"},{"id":"com.samsung.internal.systemui.navbar.sec_gestural_no_hint","description":"Gestural Navigation Bar","removal":"caution","type":"oem"},{"id":"com.samsung.ipservice","description":"Set of hidden network settings (inlcuding frequency bands choice)\nHow to see these settings : https://forum.xda-developers.com/galaxy-note-8/help/q-hidden-network-settings-pie-t3914421/page4\n\nSame shared user id as com.samsung.faceservice, com.samsung.mlp, com.samsung.cmh\nUsed by Galaxy Finder & Galaxy Vision to access web data\nDo removing this package break face/content recognition? \n#\nName and permissions of this package suggest that it is used by Galaxy Finder to seek stuff on the web.\nSame shared user id as com.samsung.faceservice, com.samsung.mlp, com.samsung.cmh\n","removal":"delete","type":"oem"},{"id":"com.samsung.japan.initonce","description":"InitOnce\nI found in code only name app","removal":"delete","type":"oem"},{"id":"com.samsung.klmsagent","label":"KLMS Agent","description":"Checks the validity of your KLM/KPE (Knox Licence Manager) license.\nThis package is needed for Samsung Health (\"com.sec.android.app.shealth\") and probably all Knox-related apps (like Secure Folder, Samsung Pay...)\n\nNote: KLM licences are deprecated. Samsung now only supports KPE (Knox Platform for Enterprise) keys.\nKPE keys are provided by Samsung and enable app's developers to access knox features.","removal":"delete","type":"oem"},{"id":"com.samsung.knox.appsupdateagent","description":"This app is part of Samsung Knox, managing updates for apps within the Knox security framework.","removal":"replace","type":"oem"},{"id":"com.samsung.knox.keychain","description":"Knox Key Chain\nAllows apps to sign data using system-wide private key/certificate pairs. \nSo, even though the Android Keystore provides per-app access to credentials, the Android KeyChain runs as a system user, \nand hence, credentials stored through the Android KeyChain are associated with the system ID instead of a user ID.\nhttps://docs.samsungknox.com/dev/knox-sdk/about-keystores.htm\nThis is only useful for apps using the TIMA Keystore. The big question I'm trying to answer is:\nWhich are using this except Samsung apps? Can an android dev help on this?\n","removal":"unsafe","type":"oem"},{"id":"com.samsung.knox.knoxtrustagent","description":"Knox Quick Access allows users to access the Knox Workspace container using wearables such as the Galaxy Gear S2.\n","removal":"delete","type":"oem"},{"id":"com.samsung.knox.kss","description":"Knox Keyguard. Not much more information\n","removal":"delete","type":"oem"},{"id":"com.samsung.knox.rcp.components","description":"Knox Content Mgr\nSecurity-related. Knox Content Mgr","removal":"delete","type":"oem"},{"id":"com.samsung.knox.securefolder","label":"Secure Folder","description":"Create a secure space on your device to encrypt and store your private data and apps.\nNOTE: The key used to encrypt the files is not derived from the password you use to unlock the secure folder but rather from a key stored in the hardware that is set in the factory.","web":["https://www.samsungknox.com/en/solutions/personal-apps/secure-folder"],"removal":"delete","type":"oem"},{"id":"com.samsung.knox.securefolder.setuppage","description":"Provides the setup process when opening secure folder (com.samsung.knox.securefolder) for the first time\n","removal":"delete","type":"oem"},{"id":"com.samsung.locationhistory","description":"It's only location logs","removal":"delete","type":"oem"},{"id":"com.samsung.logwriter","description":"LogWriter\nWrites data in a logs SQL database.\nRuns at boot and is triggered when an download from an Iron Source (Iron Source is an Israeli advertising company)\napp is completed (probably \"com.aura.oobe.samsung\")\n","removal":"delete","type":"oem"},{"id":"com.samsung.mdl.radio","description":"Samsung Milk Music (discontinued in 2016)\nIt was a freemium online music streaming service, with music streams and a recommendation engine powered by Slacker Radio.\nhttps://en.wikipedia.org/wiki/Milk_Music_(streaming_service)\n","removal":"delete","type":"oem"},{"id":"com.samsung.mdl.radio.radiostub","description":"Milk Music (shut down by Samsung)\nIt was a music streaming app\nhttps://en.wikipedia.org/wiki/Milk_Music_(streaming_service)\n","removal":"delete","type":"oem"},{"id":"com.samsung.memorysaver","description":"Helps you to free up space by letting you delete duplicates and move contents and apps on an SD card.","removal":"replace","type":"oem"},{"id":"com.samsung.mlp","description":"Samsung content recognition.\nmpl= Media Learning Platform. Has permissions linked to com.samsung.cmh and com.samsung.android.visionintelligence\n","removal":"delete","type":"oem"},{"id":"com.samsung.mobiletv","description":"Hidden testing mobiletv\nhttps://apkcombo.com/pt/mobiletv/com.samsung.mobiletv/","removal":"delete","type":"oem"},{"id":"com.samsung.networkui","description":"User interface of the Mobile Network settings\n","removal":"unsafe","type":"oem"},{"id":"com.samsung.oda.service","description":"SamsungOdaService\nRequires google play services.\nAnother spying app collects sim info and other things.","removal":"delete","type":"oem"},{"id":"com.samsung.oh","description":"Samsung Members (https://play.google.com/store/apps/details?id=com.samsung.oh)\nSamsung community. It's a kind of social media app for Samsung users.\nhttps://www.samsung.com/global/galaxy/apps/samsung-members/\nOOOPS ! https://bgr.com/2019/10/31/samsung-members-dong-pic-oops/\nThe other version is \"com.samsung.android.voc\".\n","removal":"delete","type":"oem"},{"id":"com.samsung.packageinstalleroverlay","description":"Most likely the overlay that appears when you installed an application.","removal":"caution","type":"oem"},{"id":"com.samsung.phone.overlay.common","description":"Possibly the incoming-call screen?","removal":"caution","type":"oem"},{"id":"com.samsung.pregpudriver.ex2100","label":"Galaxy Pre GPUDriver","description":"","removal":"caution","type":"oem"},{"id":"com.samsung.preloadapp","description":"Install app\nInstalls recommended apps.","removal":"delete","type":"oem"},{"id":"com.samsung.qosindicator","label":"QoSIndicator","description":"QOS - quality of service, but it's only for diagnostics.","removal":"delete","type":"oem"},{"id":"com.samsung.rcs","description":"RCS (Rich Communication Services)\nHas permissions linked to com.samsung.cmh, and com.samsung.android.visionintelligence (and I don't understand why).\nRCS is a communication protocol between mobile telephone carriers and between phone and carrier, aiming at replacing SMS.\nhttps://en.wikipedia.org/wiki/Rich_Communication_Services\nUses IP protocol, so it needs an internet connection.\nIt's a hot mess right now. It aims at being universal but only exists in Samsung Messages and Google Messages, because Google hasn't released a public API yet, so 3rd-party apps can't support it.\nIn a lot of countries messages go through Google's Jibe servers.\nhttps://jibe.google.com/policies/terms/\nhttps://pocketnow.com/why-you-should-probably-avoid-googles-rcs-text-messaging-chat-feature","removal":"delete","type":"oem"},{"id":"com.samsung.safetyinformation","label":"Safety Information","description":"Safety information telling you to be careful with the usage of your phone.","removal":"delete","type":"oem"},{"id":"com.samsung.sait.sohservice","label":"SoHService","description":"FactoryApp\nThis weird app collects battery data and probably others, who knows? Also to test battery things such as set voltage, Soc, temperature, status, timestamp.","removal":"delete","type":"oem"},{"id":"com.samsung.samsungpssdplus","description":"Samsung Magician\nhttps://play.google.com/store/apps/details?id=com.samsung.samsungpssdplus\nAllows users to conveniently manage their Samsung Portable SSD settings.","removal":"delete","type":"oem"},{"id":"com.samsung.sdm","description":"Handles OTA system Updates.\n","removal":"replace","type":"oem"},{"id":"com.samsung.sdm.sdmviewer","description":"Lets you view installed updates?\n","removal":"replace","type":"oem"},{"id":"com.samsung.sec.android.application.csc","description":"Do something related to Country Specific Code (CSC). Maybe it only let you change your CSC\nEvery Android device from Samsung has a folder called CSC.\nThis folder contains some XML files that keep the configuration codes for the country and carrier-based customization options.\nMaybe it's safe to remove if you'll never change your CSC but it needs testing and I lack time for this.\n(I already have plenty of other packages uninstallation to test)\n","removal":"replace","type":"oem"},{"id":"com.samsung.sec.android.teegris.tui_service","label":"TEEgrisTuiService","description":"Security-related, but not necessary.","removal":"delete","type":"oem"},{"id":"com.samsung.sec.mtv","description":"Hidden testing mobiletv","removal":"delete","type":"oem"},{"id":"com.samsung.slsi.audiologging","label":"AudioLogging","description":"It's hidden Audio logging.","removal":"delete","type":"oem"},{"id":"com.samsung.slsi.telephony.silentlogging","description":"Hidden network logging. Safe to remove","removal":"delete","type":"oem"},{"id":"com.samsung.ssu","description":"Network Unlock\nNetwork unlock? It's needed for SIM probably.","removal":"caution","type":"oem"},{"id":"com.samsung.storyservice","description":"Samsung StoryService\nCreate stories in the Gallery from your pictures and videos.\nhttps://www.samsung.com/uk/support/mobile-devices/what-is-video-collage-and-how-do-i-use-it/\nUse of content recognition (so may be related)\n","removal":"delete","type":"oem"},{"id":"com.samsung.svoice.sync","description":"Samsung Voice service\n","removal":"delete","type":"oem"},{"id":"com.samsung.systemui.bixby","description":"System UI for Bixby/Bixby2","removal":"delete","type":"oem"},{"id":"com.samsung.systemui.bixby2","description":"System UI for Bixby/Bixby2\n","removal":"delete","type":"oem"},{"id":"com.samsung.tmovvm","description":"Samsung Visual Voicemail (for T-mobile only)\nAllows you to review and manage your voicemail directly from your smartphone, eliminating the need to dial into your mailbox.\nhttps://mobile.spectrum.com/support/article/360001296667/samsung-visual-voicemail\n","removal":"delete","type":"oem"},{"id":"com.samsung.tmowfc.wfcpref","description":"Wifi Calling for T-mobile clients only! (tmowfc = t-mobile only wifi calling)\nLets you call or text on Wi-Fi networks with your T-Mobile phone number\nhttps://www.t-mobile.com/support/coverage/wi-fi-calling-from-t-mobile\nVoLTE/IMS is needed for this to work (see com.sec.imsservice)\n","removal":"replace","type":"oem"},{"id":"com.samsung.ucs.agent.boot","label":"bootagent","description":"UCS is a company which has partnered with Samsung to provide licenses for Samsung Knox.\nI don't have precise information about the package itself but there are chances that it verifies some files on boot. If these files are not verified then it may prevent the phone from booting.","web":["https://www.ucssolutions.com/blog/samsung-knox/"],"removal":"unsafe","type":"oem"},{"id":"com.samsung.ucs.agent.ese","description":"eSE UCS Plugin is another package from UCS. It makes possible for apps to access eSE of Samsung mobile devices by using the UCM \n(Universal Credential Management) APIs and framework.\nhttps://docs.samsungknox.com/dev/knox-sdk/faqs/general/what-is-universal-credential-management_-ucm.htm\nhttps://www.samsung.com/semiconductor/security/ese/\nSee above\n","removal":"delete","type":"oem"},{"id":"com.samsung.ucs.ucspinpad","description":"UcsPinpad\nIn this app u can setup pin,puk\nUCS is a company which has partnered with Samsung to provide licenses for Samsung Knox","removal":"delete","type":"oem"},{"id":"com.samsung.unifiedtp","description":"Unified tethering provisioner. Tethering still works for me without it.","removal":"replace","type":"oem"},{"id":"com.samsung.upsmtheme","description":"Handle the theme of UPSM (Ultra Power Saving Mode)\nSafe to remove\n","removal":"replace","type":"oem"},{"id":"com.samsung.visionprovider","label":"VisionProvider","description":"Provider for Bixby Vision (com.samsung.android.visionintelligence)\nManages access to data stored by itself, stored by other apps, and provide a way to share these data with other apps.","removal":"delete","type":"oem"},{"id":"com.samsung.vklayer.sm8250","label":"Samsung SM8250 VKLayer","description":"Vulkan GPU driver for SM8250","removal":"unsafe","type":"oem"},{"id":"com.samsung.vklayer.sm8350","label":"Samsung SM8350 VKLayer","description":"Vulkan GPU Driver for SM8350","removal":"unsafe","type":"oem"},{"id":"com.samsung.voiceserviceplatform","description":"Samsung Voice (for Galaxy S7)\nVirtual mobile personal assistant capable of running basic tasks through voice\nhttps://www.samsung.com/global/galaxy/what-is/s-voice/\n","removal":"delete","type":"oem"},{"id":"com.samsung.vvm","description":"Samsung Verizon Voicemail\nAllows you to review and manage your voicemail directly from your smartphone, eliminating the need to dial into your mailbox.\nYou can scroll through your messages, pick the ones you want to listen to, and erase them right from your device's screen.\nhttps://mobile.spectrum.com/support/article/360001296667/samsung-visual-voicemail","removal":"delete","type":"oem"},{"id":"com.samsung.vvm.se","description":"Samsung Verizon Voicemail \nAllows you to review and manage your voicemail directly from your smartphone, eliminating the need to dial into your mailbox.\nYou can scroll through your messages, pick the ones you want to listen to, and erase them right from your device's screen.\nhttps://mobile.spectrum.com/support/article/360001296667/samsung-visual-voicemail\n","removal":"delete","type":"oem"},{"id":"com.satispay.promotion","label":"Satispay Promotion","description":"Cashbacks and promotional related app","web":["https://beta.pithus.org/report/d2aced319e53ccf9de03b47be1bdb11bfb6b1ccd0cc15e5279e34697e23db338"],"removal":"delete","type":"oem"},{"id":"com.scanning.agold.agoldscanning","description":"\"Scan\" Settings > intelligent assistant: Scan. QR code & Bar code scanner.\n","removal":"replace","type":"oem"},{"id":"com.scee.psxandroid","description":"PlayStation app\nhttps://play.google.com/store/apps/details?id=com.scee.psxandroid","removal":"delete","type":"oem"},{"id":"com.scorpio.securitycom","description":"SecurityPlugin\nIt cant be uninstalled.","removal":"unsafe","type":"oem"},{"id":"com.sec.allsharecastplayer","description":"Screen Mirroring (only in Galaxy S6)\nCast your mobile screen to a TV.\nhttps://www.samsung.com/us/2012-allshare-play/\n","removal":"delete","type":"oem"},{"id":"com.sec.android.AutoPreconfig","description":"Auto Preconfig\nTells you to format the device when sim from other country is used basically (won't let you use another one)\n","removal":"delete","type":"oem"},{"id":"com.sec.android.CcInfo","description":"CcInfo\nSamsung logs/analytics.","removal":"delete","type":"oem"},{"id":"com.sec.android.Cdfs","label":"CDFS MODE Launcher","description":"It's CdfsService Probably needed for usb mtp","removal":"caution","type":"oem"},{"id":"com.sec.android.GeoLookout","description":"Geo News\nfor korea or japanese","removal":"delete","type":"oem"},{"id":"com.sec.android.Preconfig","description":"On some phones founded testing things menu and on some not found any hidden menu\nbut it's app for secret codes to testing hardware things","removal":"delete","type":"oem"},{"id":"com.sec.android.RilServiceModeApp","description":"Service mode RIL hidden app. Used for debug and diagnostics\ndial *#0011# for modem connectivity info, *#9090# for diagnostics control\n#\nRIL means Radio Interface Layer. It's the bridge between Android phone framework services and the hardware.\nhttps://wladimir-tm4pda.github.io/porting/telephony.html\nhttps://stackoverflow.com/questions/11111067/how-does-modem-code-talk-to-android-code\nSamsung RIL is a add on from Samsung : Modem <=> Linux kernel <=> libsamsung-ipc <=> Samsung-RIL <=> Android framework <=> Android applications\n","removal":"delete","type":"oem"},{"id":"com.sec.android.app.DataCreate","description":"Automation Test\nAnother hidden test app. A lot of mention of samsung note (memo). Has access to basically everything on the phone\nRelated to these hidden menus (accessible by typing these codes in the samsung dialer) :\n- *#3282*727336*# (Status of data usage) \n- *#273283*255*3282*# (Data create menu) \n- *#*#273283*255*663282*#*#* (Backup all media files)\n","removal":"delete","type":"oem"},{"id":"com.sec.android.app.SecSetupWizard","label":"Samsung SetupWizard","description":"The first time you turn your device on, a welcome screen is displayed. It guides you through the basics of setting up your device. It's the setup for Samsung services.","removal":"caution","warning":"Both \"com.sec.phone\" & \"com.sec.android.app.SecSetupWizard\" must be enabled to be able to add new eSIMs to device. Otherwise, the menu will spin indefinitely.","type":"oem"},{"id":"com.sec.android.app.aftersalecamera","description":"Calibration Camera app.","removal":"delete","type":"oem"},{"id":"com.sec.android.app.apex","label":"Samsung ApexService","description":"Enables taking motion photos and provides a motion photos player/viewer, also does face recognition in the gallery app.","web":["https://www.samsung.com/global/galaxy/what-is/motion-photo/"],"removal":"caution","type":"oem"},{"id":"com.sec.android.app.applinker","description":"Related to FeliCa Networks (https://en.wikipedia.org/wiki/FeliCa / https://www.felicanetworks.co.jp/en/).\nFeliCa is contactless RFID smart card system mainly used for wallet function on mobile devices\n#\nHas the permission INSTALL_PACKAGES\n","removal":"delete","type":"oem"},{"id":"com.sec.android.app.bcocr","description":"Business card recognition\nSomething about business cards\nnot very useful","removal":"delete","type":"oem"},{"id":"com.sec.android.app.billing","description":"Samsung billing/Checkout\nUsed to purchase apps through Samsung Store application that is delivered with Samsung phones. \nActs as bridge between Samsung Store and payment servers.\n","removal":"delete","type":"oem"},{"id":"com.sec.android.app.bluetoothagent","description":"Bluetooth Agent\nBluetooth test service.","removal":"delete","type":"oem"},{"id":"com.sec.android.app.bluetoothtest","label":"BluetoothTest","description":"Hidden feature accessible by entering *#*#232331#*#* in the Samsung dialer.\nIt was found to be calling home back in 2015.","web":["https://forum.xda-developers.com/galaxy-s5/help/bluetoothtest-apk-calling-home-t3035182"],"removal":"delete","type":"oem"},{"id":"com.sec.android.app.camera","description":"Samsung camera app\nSafe to remove (but not recommended)\n","removal":"replace","type":"oem"},{"id":"com.sec.android.app.camerasaver","description":"Weird app that has BootCameraService. Probably you can't uninstall it.\nIn the code found things: createCameraPreviewSession, generateTextureIds, openCamera, setUpCameraOutputs.","removal":"caution","type":"oem"},{"id":"com.sec.android.app.chromecustomizations","description":"Samsung stuff on the homepage of Google Chrome\n","removal":"delete","type":"oem"},{"id":"com.sec.android.app.clockpackage","description":"Samsung clock\n","removal":"replace","type":"oem"},{"id":"com.sec.android.app.desktoplauncher","label":"Samsung DeX Home","description":"DeX Enables users to extend their device into a desktop-like experience by connecting a keyboard, mouse, and monitor.\n\"DeX\" is a contraction of \"Desktop eXperience\".","web":["https://en.wikipedia.org/wiki/Samsung_DeX"],"removal":"delete","type":"oem"},{"id":"com.sec.android.app.dexonpc","description":"Samsung DeX\nExtends your smartphone into a \"desktop computing experience\".\nConcretely this lets you access all your mobile apps and content from a computer.\nOnly works on Windows/MacOS. You will need to install the Samsung DeX app on your computer.\nhttps://en.wikipedia.org/wiki/Samsung_DeX\nhttps://www.samsung.com/global/galaxy/apps/samsung-dex/\n","removal":"delete","type":"oem"},{"id":"com.sec.android.app.dictionary","description":"Samsung Dictionary is is an app that enables you to manage all the dictionaries stored on your Samsung device.\n","removal":"replace","suggestions":"dictionaries","type":"oem"},{"id":"com.sec.android.app.easysetup","description":"Core of Samsung SmartThings (formerly Samsung Easy Setup)\nSee com.samsung.android.easysetup\n","removal":"delete","type":"oem"},{"id":"com.sec.android.app.factorykeystring","description":"DeviceKeyString : Dialable hidden diagnostic/debug app\nDial *#0283# to open audio LoopbackTest control, dial *#2663# for TSP firmware update\n","removal":"delete","type":"oem"},{"id":"com.sec.android.app.felicatest","description":"FeliCa Test, hidden debugs,logs HAL_Nfc","removal":"delete","type":"oem"},{"id":"com.sec.android.app.firewall","description":"Blocked calls'msgs\nChinese blocking calls and spam.","removal":"delete","type":"oem"},{"id":"com.sec.android.app.fm","description":"Samsung Radio\nListen to FM radio stations\n","removal":"replace","suggestions":"radios","type":"oem"},{"id":"com.sec.android.app.gamehub","description":"Samsung Game Hub\nWas replaced by \"com.samsung.android.game.gamehome\"\nhttps://www.techradar.com/news/phone-and-communications/mobile-phones/the-samsung-game-hub-explained-1143450\n","removal":"delete","type":"oem"},{"id":"com.sec.android.app.hwmoduletest","label":"HwModuleTest","description":"A hardware hidden test app (dial *#0*# to open it).\nHas stuff to quickly check the phone's hardware. Removing it does not break anything, but no point in removing it. Note: opening this menu is required to change CSC with PC programs.","removal":"delete","type":"oem"},{"id":"com.sec.android.app.kidshome","description":"This app provides a child-friendly interface and controls on Samsung devices, designed to create a safe environment for children by restricting access to apps and content.","removal":"delete","type":"oem"},{"id":"com.sec.android.app.launcher","label":"Samsung One UI Home","description":"Samsung One UI Home launcher (homescreen) which is also Samsung's TouchWiz default launcher.","web":["https://play.google.com/store/apps/details?id=com.sec.android.app.launcher"],"removal":"caution","warning":"Disabling this package breaks the multitasking navigation button on all newer versions of One UI, nothing would happen when pressing it.","suggestions":"launchers","type":"oem"},{"id":"com.sec.android.app.magnifier","description":"Lets you use your device as a magnifying glass making it easier to read any small font or expand the details of any object, for example.\n","removal":"delete","type":"oem"},{"id":"com.sec.android.app.minimode.res","description":"Minimode will be not available when this app be removed","removal":"delete","type":"oem"},{"id":"com.sec.android.app.mt","description":"Mobile tracker security feature. If someone inserts a new SIM card in your device the device will automatically \nsends the SIM contact number to specified recipients to help you locate and recover you device.\nhttps://www.samsung.com/nz/support/mobile-devices/what-is-mobile-tracker/\n","removal":"delete","type":"oem"},{"id":"com.sec.android.app.myfiles","label":"Samsung My Files","description":"Samsung file manager app\n","web":["https://play.google.com/store/apps/details?id=com.sec.android.app.myfiles"],"removal":"replace","warning":"If you remove this package on Android 10+, you will no longer be able to manage storage the same way as before. For example you will lose the ability to unmount or format the SD card from within the Settings app.\n","suggestions":"file_managers","type":"oem"},{"id":"com.sec.android.app.ocr","description":"Optical Read (feature replaced by Bixby Vision : com.samsung.android.visionintelligence)\nLets you scan or extract text or data from images, documents, business cards, or QR codes.\n","removal":"delete","type":"oem"},{"id":"com.sec.android.app.parser","label":"DRParser Mode","description":"Secret code parser\nSupport for hidden samsung apps launched via secret codes.","removal":"delete","type":"oem"},{"id":"com.sec.android.app.personalization","description":"Has something to do with personalization?\nHas 2 permissions: `READ_PHONE_STATE` and `CHANGE_PHONE_STATE`","removal":"replace","type":"oem"},{"id":"com.sec.android.app.popupcalculator","label":"Samsung Calculator","description":"Samsung calculator app\n","web":["https://play.google.com/store/apps/details?id=com.sec.android.app.popupcalculator"],"removal":"replace","suggestions":"calculators","type":"oem"},{"id":"com.sec.android.app.popupuireceiver","description":"Not needed popupui with battery animation charging or other system notifications.","removal":"delete","type":"oem"},{"id":"com.sec.android.app.qsfastpairoverlay","description":"About half street overlay, has to gms.","removal":"replace","type":"oem"},{"id":"com.sec.android.app.quicktool","description":"The Quick Tools panel includes a ruler, a compass and a torch. To add this to the Edge Panel (com.samsung.android.app.clipboardedge)\n","removal":"delete","type":"oem"},{"id":"com.sec.android.app.ringtoneBR","description":"Samsung ringtone backup/restore feature\nWhere is this feature? (available from Samsung Galaxy S9)\n","removal":"delete","type":"oem"},{"id":"com.sec.android.app.safetyassurance","description":"Safety assurance is related to emergency features. It is especially used for SOS messages.\nhttps://www.samsung.com/nz/support/mobile-devices/samsung-sos-smart-phone-emergency-message-guide/\nHas obviously a huge amount of permissions.\n\nPithus analysis: https://beta.pithus.org/report/a06501fce61a39cb2b38df088eba4d0ce7ca3ed8fce3e8b672d8eb807538fb1f","removal":"replace","type":"oem"},{"id":"com.sec.android.app.samsungapps","label":"Galaxy Store","required_by":["com.samsung.android.spay"],"description":"The Samsung app store.","web":["https://en.wikipedia.org/wiki/Samsung_Galaxy_Store","https://www.computerworld.com/article/3514999/samsung-selling-data.html","https://www.reddit.com/r/Android/comments/xtq9pq/samsungs_privacy_policy_for_oct_1st_is_crazy/"],"removal":"replace","warning":"Removing the app may break Samsung Pay features.","suggestions":"app_stores","type":"oem"},{"id":"com.sec.android.app.saspmodemailerprovider","description":"docomo mail\nno activities, looks like more useless frameworks also it's only for japanese","removal":"delete","type":"oem"},{"id":"com.sec.android.app.sbrowser","label":"Samsung Internet","description":"From their privacy policy: \"The information we obtain [..] include, identifiers associated with your devices, types of devices, web browser characteristics, device and operating system type and characteristics, language preferences, clickstream data, your interactions with Samsung Internet (such as the web pages you visit, links you click and features you use), dates and times of your use of Samsung Internet, and other information about your use of Samsung Internet.\"","web":["https://play.google.com/store/apps/details?id=com.sec.android.app.sbrowser","https://developer.samsung.com/internet/privacy-policy-us.html","https://privacytests.org/android.html"],"removal":"replace","suggestions":"browsers","type":"oem"},{"id":"com.sec.android.app.sbrowser.lite","label":"Samsung Internet Lite/Go","description":"Lite version of the Samsung browser (hah! Because the base one was too bloated?)\nFrom their privacy policy: \"The information we obtain [..] include, identifiers associated with your devices, types of devices, web browser characteristics, device and operating system type and characteristics, language preferences, clickstream data, your interactions with Samsung Internet (such as the web pages you visit, links you click and features you use), dates and times of your use of Samsung Internet, and other information about your use of Samsung Internet.\"","web":["https://developer.samsung.com/internet/privacy-policy-us.html","https://privacytests.org/android.html"],"removal":"replace","suggestions":"browsers","type":"oem"},{"id":"com.sec.android.app.scloud","description":"I guess it's the core of Samsung Cloud.","removal":"delete","type":"oem"},{"id":"com.sec.android.app.servicemodeapp","description":"SysDump hidden app\nLow-level debugging and diagnostics tools (dial *#9900# to open it)\n","removal":"delete","type":"oem"},{"id":"com.sec.android.app.setupwizard","label":"Setup Wizard","description":"The Welcome screen which guides you through the basics of setting up your device when you boot it for the first time (or after a factory reset). On some Verizon-carried devices, it can show an annoying \"SIM card not from Verizon Wireless\" notification, for each boot, and it can't be blocked unless you disable the app.","removal":"delete","type":"oem"},{"id":"com.sec.android.app.setupwizardlegalprovider","description":"SetupWizardLegalProvider\nAll the legal terms you need to accept when you boot your phone for the first time. \nThe Welcome screen which guides you through the basics of setting up your device is the android setup wizard.\n","removal":"delete","type":"oem"},{"id":"com.sec.android.app.shealth","label":"Samsung Health","description":"Serves to track various aspects of daily life contributing to well being such as physical activity, diet, and sleep.\nS Health data is stored in a Knox container (with HIPAA compliance).","web":["https://play.google.com/store/apps/details?id=com.sec.android.app.shealth","https://en.wikipedia.org/wiki/Samsung_Health"],"removal":"delete","type":"oem"},{"id":"com.sec.android.app.simsettingmgr","description":"SIM card manager.\nContains configuration and settings for handling dual SIM (give a SIM an icon, a name, and so on)\n","removal":"unsafe","type":"oem"},{"id":"com.sec.android.app.sns3","description":"Samsung Galaxy (Only installed on older phone before Galaxy S7)\nDon't really know what this app does but majority of people deleted this.\n","removal":"delete","type":"oem"},{"id":"com.sec.android.app.snsimagecache","description":"SnsImageCache\nA lot logs, probably it's for image cache. A lot samsung phones dont have it so it's safe to remove","removal":"delete","type":"oem"},{"id":"com.sec.android.app.soundalive","label":"Samsung SoundAlive","description":"Responsible for Dolby Atmos and other equalizer stuff (accessible from the Settings app).\nNeeded by Adapt Sound (com.sec.hearingadjust) which is a pretty useful feature.","removal":"delete","type":"oem"},{"id":"com.sec.android.app.suwscriptplayer","description":"SuwScriptPlayer\nSeems to be another test app which test some \"scripts\"\n","removal":"delete","type":"oem"},{"id":"com.sec.android.app.sysscope","label":"SysScope","description":"Checks after every boot if the ROM and kernel have been modified. This package is usually present on Verizon-locked phones.\nVerizon has the ability to check if your device has root access (content://com.verizon.security/ROOT_STATUS).","removal":"delete","type":"oem"},{"id":"com.sec.android.app.taskmanager","description":"Hidden app that shows uninstall unused apps(+boring notifications that may be disabled)\nand button to (other, in the same app)task manager(shows ram and buttons kill user apps).","removal":"delete","type":"oem"},{"id":"com.sec.android.app.tourviewer","description":"3d tour image, Virtual tour, Viewer used to view photos taken with the Galaxy S5 camera's Virtual Tour Shot mode.","removal":"delete","type":"oem"},{"id":"com.sec.android.app.translator","description":"Samsung Translater (S Translater)\nhttps://www.samsung.com/africa_en/support/mobile-devices/what-is-s-translator-and-how-does-it-work/\n","removal":"replace","suggestions":"translators","type":"oem"},{"id":"com.sec.android.app.uwbtest","label":"UwbTest","description":"For testing UWB in the factory.","removal":"delete","type":"oem"},{"id":"com.sec.android.app.ve.vebgm","description":"Samsung Editing Assets (\"Video Editor BackGround Music\").\nThis app lets you choose background music from the Video Editor app.","removal":"delete","type":"oem"},{"id":"com.sec.android.app.vepreload","description":"Samsung video editor\nLets you add add transitions, music, stickers and text to your videos. You can also change the speed of the action, \nor even add filters to switch up the mood of your videos.\n","removal":"replace","type":"oem"},{"id":"com.sec.android.app.voicenote","label":"Samsung Voice Recorder","description":"Samsung Voice recorder\n","web":["https://play.google.com/store/apps/details?id=com.sec.android.app.voicenote"],"removal":"replace","suggestions":"audio_recorders","type":"oem"},{"id":"com.sec.android.app.volumemonitorprovider","label":"VolumeMonitorProvider","description":"Used in Digital Wellbeing to average the dB heard through earphones and display it.","removal":"delete","type":"oem"},{"id":"com.sec.android.app.wallpaperchooser","description":"This app provides the functionality to select and manage wallpapers on Samsung devices, including Samsung-specific wallpaper options.","removal":"replace","type":"oem"},{"id":"com.sec.android.app.wfdbroker","description":"I found it's for connecting to tv also settings\nbut app size is too small for doing these things.","removal":"delete","type":"oem"},{"id":"com.sec.android.app.withtv","description":"Samsung Smart View\nAllows you to cast your phone screen to the TV.\nhttps://www.samsung.com/us/apps/smart-view-2/\n","removal":"delete","type":"oem"},{"id":"com.sec.android.app.wlantest","description":"wlan test\nHidden test app responding to the #232339*# and *#232338*# secret codes\nFYI : wlan = wireless LAN (https://en.wikipedia.org/wiki/Wireless_LAN)\nNOTE: Disabling this test will rise the exclamation mark on the WI-Fi icon and will show the message \"Unable to connect to the host\" in Settings -> Connections -> More connections settings - Private DNS - provider hostname.\nThe connection seems to work despite those esthetic errors.","removal":"delete","type":"oem"},{"id":"com.sec.android.autodoodle.service","label":"AutoDoodle","description":"Samsung Auto Doodle for Photo Editor","web":["https://galaxystore.samsung.com/prepost/000005425352"],"removal":"delete","type":"oem"},{"id":"com.sec.android.automotive.drivelink","description":"Car mode\nit's hidden app and has a lot duplicate settings. Not needed","removal":"delete","type":"oem"},{"id":"com.sec.android.automotive.drivelinkremote","description":"DriveLinkRemote\nI found only Hello world!","removal":"delete","type":"oem"},{"id":"com.sec.android.casual2","description":"Casual theme","removal":"replace","type":"oem"},{"id":"com.sec.android.classic2","description":"Classic theme","removal":"replace","type":"oem"},{"id":"com.sec.android.cover.ledcover","description":"Samsung LED cover service\nDisplay stuff on the LED case.\nhttps://www.samsung.com/us/support/troubleshooting/TSG01001489/\nHOW IT WORKS : https://forum.xda-developers.com/galaxy-note-8/accessories/how-led-cover-t3686694\n","removal":"delete","type":"oem"},{"id":"com.sec.android.daemonapp","description":"Unified Daemon app \nprovides support for a number of different apps on your device. These include the Weather, Yahoo Finance and Yahoo News apps amongst others. \nThe data is used by apps such as the Alarm, Calendar app and the camera.\n","removal":"delete","type":"oem"},{"id":"com.sec.android.desktopcommunity","description":"Samsung DeX panel\n","removal":"delete","type":"oem"},{"id":"com.sec.android.desktopmode.uiservice","description":"Samsung DeX\nExtends your smartphone into a \"desktop computing experience\".\nConcretely this lets you access all your mobile apps and content from a computer.\nOnly works on Windows/MacOS. You will need to install the Samsung DeX app on your computer.\nhttps://en.wikipedia.org/wiki/Samsung_DeX\nhttps://www.samsung.com/global/galaxy/apps/samsung-dex/\n","removal":"delete","type":"oem"},{"id":"com.sec.android.dexsystemui","description":"Samsung DeX System UI\nSafe to remove if not use connect phone for example to TV.","removal":"replace","type":"oem"},{"id":"com.sec.android.diagmonagent","label":"DiagMonAgent","description":"Diagnostic Monitoring Agent\nUsed to send diagnostic data to Samsung\nData collection from Settings > Biometrics and security > Send diagnostic data\n","removal":"caution","warning":"Disabling this causes scheduled services to aggressively launch it every second, spamming the android crash log and overheating the device, thus draining battery. More info is needed on the dependencies that trigger this behavior. Version tested: 9.0.11.","type":"oem"},{"id":"com.sec.android.easyMover","description":"Samsung Smart Switch Mobile (https://play.google.com/store/apps/details?id=com.sec.android.easyMover)\nAllows you to easily transfer content (contacts, photos, music, notes, etc.) to a new Samsung Galaxy device. \nhttps://www.samsung.com/global/galaxy/apps/smart-switch/\nhttps://fr.wikipedia.org/wiki/Smart_Switch\n","removal":"delete","type":"oem"},{"id":"com.sec.android.easyMover.Agent","label":"Smart Switch Agent","description":"Needed to use Smart Switch. See \"com.sec.android.easyMover\".","removal":"delete","type":"oem"},{"id":"com.sec.android.easyonehand","description":"Samsung Easy One Hand mode\nAllows you to temporarily scale down the display size of your screen for easier control of your phone with just one hand.\nhttps://www.samsung.com/au/support/mobile-devices/using-one-handed-mode/\n","removal":"delete","type":"oem"},{"id":"com.sec.android.emergencylauncher","description":"Samsung Launcher when in emergency(low battery) mode.","removal":"replace","type":"oem"},{"id":"com.sec.android.emergencymode.service","description":"Emergency mode enables you to extend your device’s standby time when in an emergency situation and want your device to conserve power for as long as possible. When activated, screen brightness will decrease, some of functionality will be limited and the home screen is changed to a black theme, all to reduce battery drain (OLED screens draw less power when showing black, cuz black = pixel off).\nNOT related to SOS messages/911.\nhttps://www.samsung.com/uk/support/mobile-devices/what-is-emergency-mode/","removal":"replace","type":"oem"},{"id":"com.sec.android.fido.uaf.asm","label":"Fido ASM Data","description":"Fido is a set of open technical specifications for mechanisms of authenticating users to online services that do not depend on passwords.\nThe UAF protocol is designed to enable online services to offer passwordless and multi-factor security by allowing users to register their device to the online service and using a local authentication mechanism such as iris or fingerprint recognition.\nThe UAF Authenticator-Specific Module (ASM) is a software interface on top of UAF authenticators which gives a standardized way for FIDO UAF clients to detect and access the functionality of UAF authenticators and hides internal communication complexity from FIDO UAF Client.\nSafe to remove if you don't use password-less authentication to access online services.","web":["https://fidoalliance.org/specs/u2f-specs-1.0-bt-nfc-id-amendment/fido-glossary.html","https://fidoalliance.org/specs/fido-v2.0-rd-20170927/fido-overview-v2.0-rd-20170927.html","https://developers.google.com/identity/fido/android/native-apps","https://fidoalliance.org/specs/fido-uaf-v1.0-ps-20141208/fido-uaf-asm-api-v1.0-ps-20141208.html"],"removal":"delete","type":"oem"},{"id":"com.sec.android.fido.uaf.client","label":"FIDO UAF Client","description":"It's a layer that connects authenticator and RP (the application owner) and ensures validity of the connection. It can be browser, desktop application, mobile application, platform(i.e. android/ios).\nSafe to remove if you don't use password-less authentication to access online services.","removal":"delete","type":"oem"},{"id":"com.sec.android.gallery3d","description":"Samsung Gallery app (https://play.google.com/store/apps/details?id=com.sec.android.gallery3d)\nNote: Samsung Gallery is a dependency for the camera so it's not a good idea to delete it.\nNote : Good to know. When the original version of the image is deleted, the copy of it within the com.sec.android.gallery3d folder is not removed.\nhttps://athenaforensics.co.uk/com-sec-android-gallery3d-mobile-phone-forensics/\nNOTE : Deleting this package will also prevent to preview photos from the camera app.\n","removal":"replace","type":"oem"},{"id":"com.sec.android.gallery3d.panorama360view","description":"Let you see panoramic photos in the samsung Gallery.\n","removal":"replace","type":"oem"},{"id":"com.sec.android.game.gamehome","description":"Samsung Game launcher\nCentralizes all your android games. This app can track all your games, how many hours you've spent playing each one, and which genres you play the most.\nRecommends games based on your profile.\nhttps://galaxystore.samsung.com/prepost/000004906980?appId=com.samsung.android.game.gamehome \n","removal":"delete","type":"oem"},{"id":"com.sec.android.iaft","description":"iaft\nHas permission dump, trace errors probably.","removal":"delete","type":"oem"},{"id":"com.sec.android.inputmethod","description":"Samsung Keyboard\nSuperseded by `com.samsung.android.honeyboard`.","removal":"caution","type":"oem"},{"id":"com.sec.android.inputmethod.beta","description":"Samsung Keyboard Neural Beta\nSmarter Samsung Keyboard with better predictition/suggestion (using deep learning)\nhttps://galaxystore.samsung.com/prepost/000004170688?appId=com.sec.android.inputmethod.beta\n","removal":"replace","type":"oem"},{"id":"com.sec.android.inputmethod.iwnnime.japan","description":"Samsung Japanese keyboard","removal":"replace","type":"oem"},{"id":"com.sec.android.kies","description":"No activities, only useless frameworks in classes.dex","removal":"delete","type":"oem"},{"id":"com.sec.android.llmpolicy","description":"LLMPolicyService\nLow latency network to game? Looks unused.","removal":"delete","type":"oem"},{"id":"com.sec.android.mimage.avatarstickers","description":"Samsung My Emoji Stickers\nLet you turn yourself into an emoji. Woah ! What an incredible feature...\nhttps://www.samsung.com/us/support/answer/ANS00078920/\n","removal":"delete","type":"oem"},{"id":"com.sec.android.mimage.gear360editor","description":"360 Photo Editor\nLets you edit the 360-degree photos you took.\n","removal":"delete","type":"oem"},{"id":"com.sec.android.mimage.photoretouching","description":"Samsung Photo Editor\nDisabling this will disable the inbuilt photo editor accessed via the stock gallery.\nSafe to remove if you don't use Samsung gallery.\n","removal":"replace","type":"oem"},{"id":"com.sec.android.ofviewer","description":"Samsung selective focus camera mode.\nSafe to remove (but it's pretty useful)\n","removal":"replace","type":"oem"},{"id":"com.sec.android.omc","description":"OM Customize\nI found only things for Vodafone. It's not needed.","removal":"delete","type":"oem"},{"id":"com.sec.android.pagebuddynotisvc","description":"PageBuddyNotiSvc\nI only found it's for notifications to car, earphones, pen. Not needed.","removal":"delete","type":"oem"},{"id":"com.sec.android.preloadinstaller","description":"Very shady apk. According to if you're chinese or not, Samsung mount an hidden partition during the first boot and install some apps.","web":["https://web.archive.org/web/20200107110205/https://nitter.net/fs0c131y/status/1046689524691218432"],"removal":"delete","type":"oem"},{"id":"com.sec.android.provider.badge","label":"BadgeProvider","description":"Provider for the notification badges (which are not very useful IMHO)\nProvides a way for apps to use notifications badges.","web":["https://www.samsung.com/au/support/mobile-devices/what-is-app-icon-badge/"],"removal":"caution","warning":"Emergency Power saving launcher does not start after removing.","type":"oem"},{"id":"com.sec.android.provider.emergencymode","description":"Provider for emergency mode (com.sec.android.emergencylauncher)\nContent providers encapsulate data, providing centralized management of data shared between apps.\nFor example: the Settings provider. It stores all the settings from your Settings app in a database, which apps can query for info on whether you for example have Dark Mode turned on or off.\nhttps://developer.android.com/guide/topics/providers/content-providers.html","removal":"replace","type":"oem"},{"id":"com.sec.android.provider.logsprovider","description":"LogsProvider\nIt's app used for logs.","removal":"delete","type":"oem"},{"id":"com.sec.android.provider.snote","description":"Content provider for S Note (https://www.samsung.com/global/galaxy/apps/samsung-notes/).\nContent providers encapsulate data, providing centralized management of data shared between apps.\nhttps://developer.android.com/guide/topics/providers/content-providers.html","removal":"delete","type":"oem"},{"id":"com.sec.android.providers.mapcon","description":"Mapcon Provider\nOnly name app founded.","removal":"delete","type":"oem"},{"id":"com.sec.android.providers.security","description":"Provider of password security policies?\nContent providers encapsulate data, providing centralized management of data shared between apps.\nhttps://developer.android.com/guide/topics/providers/content-providers.html\nSeems to provide access to a password database but I don't know under what circumstances this database is used.\nThis provider is only usable by Samsung apps.\nI see a com.android.security.PASSWORD_EXPIRED intent filter in the AndroidManifest so my guess is it handles password policies.\nFor example: A policy could force a user to change their password after a certain amount of time. That's a common policy in enterprise work.","removal":"delete","type":"oem"},{"id":"com.sec.android.providers.tasks","description":"Tasks provider\nThis app have nothing in code, only name and useless permissions","removal":"delete","type":"oem"},{"id":"com.sec.android.romantic2","description":"Sweet theme","removal":"replace","type":"oem"},{"id":"com.sec.android.sdhms","description":"Samsung Device Health Manager Service\nBattery estimation service for Samsung Care/Device maintenance (com.samsung.android.lool)\nThere is some weird stuff in the java code. I don't understand why there is a need to parse torrent files for instance\nor why there is a string \"googleapis.com/drive\"\nhttps://developers.google.com/drive/api/v3/reference\n","removal":"replace","type":"oem"},{"id":"com.sec.android.service.health","description":"Samsung Health Service\nNeeded for Samsung Health (com.sec.android.app.shealth)\n","removal":"delete","type":"oem"},{"id":"com.sec.android.settingsmaps","description":"My places\nI found Search location also theres other stuff about location but it's not needed I guess","removal":"delete","type":"oem"},{"id":"com.sec.android.sidesync30","description":"SideSync (discontinued)\nLets you share the screen and data between your PC and mobile device. \nReceive alarms of your phone through PC and use various features of your phone on the computer.\nhttps://www.samsung.com/levant/support/side-sync/\n","removal":"delete","type":"oem"},{"id":"com.sec.android.smartfpsadjuster","label":"SmartFPSAdjuster","description":"Adjusts FPS automatically in Samsung phones? Safe to delete.","web":["https://docs.samsungknox.com/CCMode/G985F_Q.pdf"],"removal":"delete","type":"oem"},{"id":"com.sec.android.soagent","description":"System application that is responsible for checking and installing software updates.","removal":"caution","type":"oem"},{"id":"com.sec.android.splitsound","description":"SplitSoundService\nProvides ability to play music on the smartphone and an external speaker at the same time\nhttps://www.samsung.com/nz/support/mobile-devices/samsung-separate-app-sound/\n","removal":"replace","type":"oem"},{"id":"com.sec.android.stub.paywithpaypal","description":"Pay with PayPal\nHidden app that wanna to install PayPal to your phone.","removal":"delete","type":"oem"},{"id":"com.sec.android.systemupdate","label":"SystemUpdate","description":"System updater for Samsung phones.","web":["https://docs.samsungknox.com/CCMode/G985F_Q.pdf"],"removal":"caution","warning":"Updates will stop working.","type":"oem"},{"id":"com.sec.android.theme.natural","description":"Natural theme","removal":"replace","type":"oem"},{"id":"com.sec.android.uibcvirtualsoftkey","description":"UIBC (User input back channel) \nAllows users to experience the dual monitor function, with the keyboard and mouse having the ability to control your smartphone device.\nEither discontinued (for the benefit of Smart View : com.samsung.android.smartmirroring) or related to Smart View. \n","removal":"delete","type":"oem"},{"id":"com.sec.android.wallpapercropper2","description":"Samsung Wallpaper. Needed to set a wallpaper on the launcher.\nNote: it is possible to change the wallpaper and then disable this package.\nUsed wallpapers are stored in /data/data/com.sec.android.wallpapercropper2/","removal":"replace","type":"oem"},{"id":"com.sec.android.widgetapp.SPlannerAppWidget","description":"Calendar Widget","removal":"delete","type":"oem"},{"id":"com.sec.android.widgetapp.ap.hero.accuweather","description":"Weather app","removal":"delete","type":"oem"},{"id":"com.sec.android.widgetapp.ap.hero.weathernewsjp","description":"japanese samsung weather app","removal":"delete","type":"oem"},{"id":"com.sec.android.widgetapp.digitalclock","description":"Clock (digital) widget","removal":"delete","type":"oem"},{"id":"com.sec.android.widgetapp.digitalclockeasy","description":"Clock (digital easy) widget","removal":"delete","type":"oem"},{"id":"com.sec.android.widgetapp.diotek.smemo","description":"Samsung Memo widget (was replaced by Samsung Note : com.samsung.android.app.notes)\nPartnership with 3-party DIOTEK : https://www.diotek.co.kr/\n","removal":"delete","type":"oem"},{"id":"com.sec.android.widgetapp.dualclockdigital","description":"Dual Clock Widget","removal":"delete","type":"oem"},{"id":"com.sec.android.widgetapp.easymodecontactswidget","description":"Favourite Contacts widget\nLets you add favorite contacts to home screen\nhttps://www.samsung.com/au/getstarted/advanced/create-favourite-contacts-on-your-home-screen/\nIs it only usable when enabling the \"simple use\" senior mode?\n","removal":"delete","type":"oem"},{"id":"com.sec.android.widgetapp.samsungapps","description":"Galaxy Essential widget\nGalaxy Essentials is a collection of specially chosen applications available through Samsung Apps. \nFrom the Galaxy Essentials widget you can access and download a collection of premium content, free of charge.\nhttps://www.samsung.com/my/support/mobile-devices/what-is-galaxy-essentials-and-how-can-i-add-or-remove-it-from-my-smartphone-home-screen/\n","removal":"delete","type":"oem"},{"id":"com.sec.android.widgetapp.webmanual","description":"User Manual\nhttps://www.samsung.com/us/support/answer/ANS00077583/\n","removal":"delete","type":"oem"},{"id":"com.sec.android.yellowpage","description":"Yellow Page\nContain phone numbers of companies and services.","removal":"delete","type":"oem"},{"id":"com.sec.app.RilErrorNotifier","description":"RilNotifier\nDebug app for the RIL\nSee \"com.sec.android.RilServiceModeApp\"\n","removal":"delete","type":"oem"},{"id":"com.sec.app.TransmitPowerService","description":"This app manages settings related to the transmit power of wireless communications on Samsung devices, such as Wi-Fi and Bluetooth.","removal":"caution","type":"oem"},{"id":"com.sec.automation","description":"Tethering Automation enables sharing phone internet to the PC with a usb cable.\nSafe to remove (but it's a useful feature)\n","removal":"replace","type":"oem"},{"id":"com.sec.bcservice","description":"Broadcast Service\nDiagnostic/debug hidden app. TCP dump.\n","removal":"delete","type":"oem"},{"id":"com.sec.clocationservice","description":"CLocationService\nNetwork location provider only to Chinese.\nBad privacy.","removal":"delete","type":"oem"},{"id":"com.sec.downloadablekeystore","description":"Keystore is a secure place provided by Android to store cryptographic keys and make it more difficult to extract from the device.\nThis package is used by enterprise to update certificates on the device.\nNOTE : It allows IT admins to install certificates while the device is still locked. \nThis means certificates can be silently installed into a keystore without any interaction from the device-user.\nIt uses the KNOX TIMA (Named Trust-zone-based Integrity Measurement Architecture) that allows storage of keys in the container for certificate signing using the TrustZone hardware platform.[16] \nhttps://docs.samsungknox.com/dev/knox-sdk/about-keystores.htm\nhttps://docs.samsungknox.com/dev/knox-sdk/faqs/general/what-is-the-knox-tima-ccm.htm\nhttps://docs.samsungknox.com/admin/whitepaper/kpe/client-certificate-manager.htm\n","removal":"delete","type":"oem"},{"id":"com.sec.enterprise.knox.attestation","description":"KNOX Attestation\nLets you check the integrity of a Samsung Android device by connecting to a Samsung Attestation server.\nhttps://docs.samsungknox.com/admin/whitepaper/kpe/attestation.htm\n","removal":"delete","type":"oem"},{"id":"com.sec.enterprise.knox.cloudmdm.smdms","description":"Knox Enrollment Service\nmdm = mobile device management = software used by an IT department to monitor employees' mobile devices.\nUsed to enroll/register a large number of phones to the KNOX MDM service\nhttps://docs.samsungknox.com/admin/knox-mobile-enrollment/enroll-your-devices.htm\nFYI : https://blog.quarkslab.com/abusing-samsung-knox-to-remotely-install-a-malicious-application-story-of-a-half-patched-vulnerability.html\n","removal":"delete","type":"oem"},{"id":"com.sec.enterprise.knox.shareddevice.keyguard","description":"KNOX shared device keyguard.\nKnox Configure Shared Device feature enables multiple users to access the same device without sharing data across multiple devices.\nhttps://docs.samsungknox.com/KC-Getting-Started/Content/about-shared-device.htm\n","removal":"delete","type":"oem"},{"id":"com.sec.enterprise.mdm.services.simpin","label":"Enterprise Sim Pin Service","description":"I couldn't find information about this package. No permissions asked. It's quite strange.\nMobile device management (MDM) is a type of security software used by an IT department to monitor employees' mobile devices.\nKNOX-dependent.","web":["https://developer.samsung.com/tech-insights/knox/mobile-device-management"],"removal":"delete","type":"oem"},{"id":"com.sec.enterprise.mdm.vpn","label":"Enterprise VPN Services","description":"I couldn't find information about this package. No permissions asked too.\nSee above for MDM signification\n","removal":"delete","type":"oem"},{"id":"com.sec.epdg","description":"Safe to remove if you're not using VoWifi.\nYou need to know that:\n3GPP is a standards organization for mobile telephony (2G/3G/4G/5G).\nNon-3GPP RAT refers to wireless connection methods not specified by 3GPP, including Wi-Fi.\nePDG (evolved Packet Data Gateway) secures connections over untrusted non-3GPP access, commonly used for VoWiFi (Voice over Wi-Fi).","web":["https://www.3gpp.org/technologies/keywords-acronyms/100-the-evolved-packet-core","https://www.aptilo.com/solutions/mobile-data-offloading/3gpp-wifi-access/","https://en.wikipedia.org/wiki/System_Architecture_Evolution#Evolved_Packet_Core_(EPC)"],"removal":"caution","type":"oem"},{"id":"com.sec.epdgtestapp","description":"Test app for ePDG (see com.sec.epdg)\n","removal":"delete","type":"oem"},{"id":"com.sec.esdk.elm","description":"ELM Agent\nHidden ELM for Developers Logs","removal":"delete","type":"oem"},{"id":"com.sec.everglades","description":"Samsung Hub (discontinued)\nIt was a cloud-based music service launched by Samsung. It allowed users to listen to music from a variety of Samsung devices\nhttps://en.wikipedia.org/wiki/Samsung_Music_Hub\n","removal":"delete","type":"oem"},{"id":"com.sec.everglades.update","description":"SamsungHub Updater (discontinued - See above)\n","removal":"delete","type":"oem"},{"id":"com.sec.facatfunction","description":"FacAtFunction\nUI Display, Sensors Tests","removal":"delete","type":"oem"},{"id":"com.sec.factory","description":"Device Test app\nDiagnostic hidden app.\n","removal":"delete","type":"oem"},{"id":"com.sec.factory.camera","description":"Camera Test (dial *#34971539# to open CameraFirmware Standard)\n","removal":"delete","type":"oem"},{"id":"com.sec.factory.cameralyzer","label":"Cameralyzer","description":"A factory testing app that allows manufacturers to check for defects in the camera, had a security issue in the past","web":["https://techforesta.com/cameralyzer/","https://docs.samsungknox.com/CCMode/G985F_Q.pdf"],"removal":"delete","type":"oem"},{"id":"com.sec.factory.iris.usercamera","description":"Camera Iris User Test (by dialing *#0*#)\n","removal":"delete","type":"oem"},{"id":"com.sec.hearingadjust","description":"Samsung Adapt Sound\nConfigures a sound profile according to your ears.\nImprove audio experience in the end (even with headphones)\nhttps://www.howtogeek.com/316375/how-to-use-adapt-sound-on-the-galaxy-s7-and-s8-for-better-sound-quality/\n#\nSettings > Sound and vibration > Sound Quality and effects > Adapt Sound\nNOTE : com.sec.android.app.soundalive is needed\n","removal":"delete","type":"oem"},{"id":"com.sec.hiddenmenu","description":"IOTHiddenMenu\nHidden menu used to access other hidden debug apps (those accessible with a secret code)\n","removal":"delete","type":"oem"},{"id":"com.sec.ims","description":"IMS(Ip Multimedia Subsystem) is an open industry standard for voice and multimedia communications over packet-based IP networks (VoLTE/VoIP/Wifi calling).\nDon't know how this is different from com.sec.imsservice. Could they interact?\nMay be unsafe to disable. Needs more testing.","removal":"caution","type":"oem"},{"id":"com.sec.ims.android","description":"IMS Framework\nNeeded for Wifi calling","removal":"replace","type":"oem"},{"id":"com.sec.imslogger","label":"ImsLogger","description":"IMS Logger provides logging opt-ins. Has known security flaws.","web":["https://web.archive.org/web/20211209093408/https://twitter.com/fs0c131y/status/1115889065285562368"],"removal":"delete","type":"oem"},{"id":"com.sec.imsservice","description":"IMS(Ip Multimedia Subsystem) is an open industry standard for voice and multimedia communications over packet-based IP networks (VoLTE/VoIP/Wifi calling).\nVideo calling is also affected.\nNote: Samsung Dialer will crash if you disable this package and have wifi-calling activated in the Dialer's settings.\nMay be unsafe to disable. Needs more testing.","removal":"caution","type":"oem"},{"id":"com.sec.internal.vsim.VSimServiceApp","description":"Non Sim Device Solution (NSDS) needed for VoLTE and VoWifi (Wifi Calling) if you have a virtual SIM. Enabled in devices without eSIMs for some reason. Not sure if there are non-esim virtual sims?\nSee com.sec.vsimservice. Uses IMS service.","removal":"caution","type":"oem"},{"id":"com.sec.kidsplat.installer","description":"Kids Mode (replaced by Kids Home : com.samsung.android.kidsinstaller)\nSamsung Kids Home (https://www.samsung.com/global/galaxy/apps/kids-mode/)\nLets you shape a safe environment for your child to happily explore and connect with the world.\nNOTE : You shouldn't give your phone to a child. That's bad ! \nhttps://ifstudies.org/blog/a-smartphone-will-change-your-child-in-ways-you-might-not-expect-or-want\n","removal":"delete","type":"oem"},{"id":"com.sec.knox.bluetooth","description":"KNOX bluetooth\nhttps://docs.samsungknox.com/knox-platform-for-enterprise/admin-guide/bluetooth.htm\nNOTE : This does not affect regular bluetooth.\n","removal":"delete","type":"oem"},{"id":"com.sec.knox.bridge","description":"Debug Bridge ? \n","removal":"delete","type":"oem"},{"id":"com.sec.knox.containeragent2","description":"Samsung Knox Container (v2 ?)\nhttps://docs.samsungknox.com/whitepapers/knox-platform/app-container.htm\n","removal":"delete","type":"oem"},{"id":"com.sec.knox.foldercontainer","description":"Needed by KNOX Secure folder (com.samsung.knox.securefolder)\n","removal":"delete","type":"oem"},{"id":"com.sec.knox.knoxsetupwizardclient","description":"KNOX SetupWizardClient\nThe first time you turn your device on, a Welcome screen is displayed. It guides you through the basics of setting up your device.\nIt's the setup for Samsung KNOX services.\n","removal":"delete","type":"oem"},{"id":"com.sec.knox.packageverifier","description":"KNOX Verifier\nUsed to scan installed packages\n","removal":"delete","type":"oem"},{"id":"com.sec.knox.shortcutsms","description":"Knox shortcut to switch to workspace \nhttps://docs.samsungknox.com/knox-platform-for-enterprise/admin-guide/workspace-shortcuts.htm\n","removal":"delete","type":"oem"},{"id":"com.sec.knox.switcher","description":"Knox Secure Folder","removal":"replace","type":"oem"},{"id":"com.sec.knox.switchknox","description":"Handles switches between KNOW/Work container and personal profile. \nIt also manages data sharing between them.\nhttps://docs.samsungknox.com/dev/knox-sdk/container-data-sharing-policies.htm\n","removal":"delete","type":"oem"},{"id":"com.sec.knox.switchknoxI","description":"Handles switches between KNOW/Work container and personal profile. \nIt also manages data sharing between them.\nhttps://docs.samsungknox.com/dev/knox-sdk/container-data-sharing-policies.htm\n","removal":"delete","type":"oem"},{"id":"com.sec.knox.switchknoxII","description":"Handles switches between KNOW/Work container and personal profile. \nIt also manages data sharing between them.\nhttps://docs.samsungknox.com/dev/knox-sdk/container-data-sharing-policies.htm\n","removal":"delete","type":"oem"},{"id":"com.sec.location.nfwlocationprivacy","label":"Service provider location","description":"Not needed for location, I found something like that:\n(This app enables your carrier to access your location for network improvement or other reasons)\nThis app is hidden so idk how useful it is.","removal":"delete","type":"oem"},{"id":"com.sec.location.nsflp2","label":"Samsung Location SDK","description":"It seems to only be used by Samsung (Galaxy) apps","removal":"delete","type":"oem"},{"id":"com.sec.mhs.smarttethering","label":"Auto Hotspot","description":"Formerly, SmartTethering.\nProbably needed for hotspots.","removal":"caution","type":"oem"},{"id":"com.sec.mldapchecker","description":"MLDAP log\nLDAP (Lightweight Directory Access Protocol; I don't know what the M means. Mobile?) is an open, vendor-neutral, industry standard application protocol for accessing and maintaining distributed directory information services over an IP network.\nDirectory service refers to the collection of software, hardware, and processes that store and organize everyday items and network resources(folders, files, printers, users, groups, devices, telephone numbers...)\nIt looks like a database but it's different.\nDirectory services excel at fast lookups for rarely changing data (email, username etc...)\nDifferences between database and Directory Service : https://www.c-sharpcorner.com/article/directory-services-vs-rdbms/\nLDAP uses a relatively simple, string-based query to extract information from Active Directory. LDAP can store and extract objects such as usernames and passwords in Active Directory, and share that object data throughout a network. \nExample of LDAP usage : https://stackoverflow.com/questions/239385/what-is-ldap-used-for/592339\n\nI don't know why and how Samsung uses LDAP. This package, according to its name only does logging.","removal":"delete","type":"oem"},{"id":"com.sec.modem.settings","description":"Name : SilentLogging\nThis package runs at startup and logs things (related to the modem ?). Seems Pretty shady to me (I don't like its orwellian name).\n","removal":"delete","type":"oem"},{"id":"com.sec.omadmspr","description":"OMADM\nhas activation things and firmware updates","removal":"caution","type":"oem"},{"id":"com.sec.phone","description":"Another test/debug app used to test the proper functioning of phone calls.","removal":"caution","warning":"Both \"com.sec.phone\" & \"com.sec.android.app.SecSetupWizard\" must be enabled to be able to add new eSIMs to device. Otherwise, the menu will spin indefinitely.","type":"oem"},{"id":"com.sec.providers.assisteddialing","description":"Assisted Dialing\nI found mcc countrycode OTA lookup, not needed","removal":"delete","type":"oem"},{"id":"com.sec.readershub","description":"Samsung Books (discontinued)\nAll-in-one e-Reading solution that offers instant access to thousands of e-reading contents.\n","removal":"delete","type":"oem"},{"id":"com.sec.smartcard.manager","description":"Smart Card Manager\nSmart Card enables communication with Secure Elements (SIM card, embedded Secure Elements, Mobile Security Card...)\nThese packages seem to be Samsung implementation.\n","removal":"delete","type":"oem"},{"id":"com.sec.spp.push","label":"Samsung Push Service","description":"Provides updates and notifications for services exclusive to Samsung (more like Samsung ads).","web":["https://play.google.com/store/apps/details?id=com.sec.spp.push","https://www.samsunggeeks.com/2015/10/25/what-is-the-samsung-push-service/"],"removal":"delete","type":"oem"},{"id":"com.sec.sve","label":"SecVideoEngineService","description":"Arguably a Samsung video engine service (handling enconding/decoding?) for displaying video through Samsung apps.\n3 permissions: RECORD_AUDIO, CAMERA, INTERACT_ACROSS_USERS_FULL","removal":"caution","warning":"Removing it will break WiFi Calling.","type":"oem"},{"id":"com.sec.svoice.lang.en_GB","description":"Language Pack for S-voice, the Samsung assistant (com.samsung.android.svoice)\n","removal":"delete","type":"oem"},{"id":"com.sec.tcpdumpservice","description":"only found app name tcpdumpservice\ntheres no code","removal":"delete","type":"oem"},{"id":"com.sec.unifiedwfc","label":"Samsung Wi-Fi Calling","description":"Wi-Fi calling app for Samsung.","removal":"caution","warning":"Wi-Fi calling may not work without the app","type":"oem"},{"id":"com.sec.usbsettings","label":"USBSettings","description":"Hidden settings. Lets you choose from ADB, MTP, RNDIS, ACM, DM (dial *#0808# to open)\nRuns at startup.","removal":"caution","type":"oem"},{"id":"com.sec.vsim.ericssonnsds.webapp","description":"NSDSWebApp.\nVirtual SIM is an application-enabled service that requires you to install an app to use a number. \nWith this technology, it is possible for one to have numbers of different countries. \nNon Sim Device Solution (NSDS) is needed for VoLTE and VoWifi (Wifi Calling) if you have a virtual SIM. \nNSDS allows connecting non sim devices to IMS core: https://uk.linkedin.com/in/hemant-kumar-dewnarain-2b779679\n","removal":"delete","type":"oem"},{"id":"com.sec.vsimservice","description":"VSim Service \nLets you use a virtual sim\nhttps://www.quora.com/What-is-VSIM-virtual-SIM-technology\nHas a LOT of permissions (and involving IMS service)\nRun at startup.\nFYI : https://security.stackexchange.com/questions/223290/esim-vs-sim-card-what-is-more-secure\n","removal":"replace","type":"oem"},{"id":"com.sec.yosemite.phone","description":"Samsung WatchON (discontinued)\nIt was a service allowing you to view programming information on the TV and choose programs directly from the phone.\nhttps://en.wikipedia.org/wiki/Samsung_WatchON\n","removal":"delete","type":"oem"},{"id":"com.ses.entitlement.o2","description":"O2 carrier app.\nUnknown.","removal":"caution","type":"oem"},{"id":"com.sgrl.fmradio","description":"FM Radio\nSafe to remove if unused.","removal":"replace","type":"oem"},{"id":"com.sgrl.pjj.factorymode","description":"FactoryMode, Calibration, Testing hardware things.","removal":"delete","type":"oem"},{"id":"com.sh.smart.caller","description":"Bloated Phone caller app with a lot of features. These apps have telemetry on them and are completely replaceable.","removal":"delete","type":"oem"},{"id":"com.shannon.qualifiednetworksservice","description":"Needed for RCS, IMS.","removal":"replace","type":"oem"},{"id":"com.silead.fingerprint","description":"Fingerprint silead manager\nHidden app for testing Fingerprint things. Not needed for normal users.","removal":"delete","type":"oem"},{"id":"com.sina.weibo","description":"Chinese Weibo app.","removal":"delete","type":"oem"},{"id":"com.skms.android.agent","description":"Samsung KMS agent service a client application for Android devices to support eSE-based (embedded secure element) mobile-NFC Services.\nhttps://developer.samsung.com/ese/overview.html\nKMS = Key Management System\nKNOX feature (https://en.wikipedia.org/wiki/Samsung_Knox)\n","removal":"delete","type":"oem"},{"id":"com.smile.gifmaker","description":"Chinese partner app 'Kuaishou'.","removal":"delete","type":"oem"},{"id":"com.snap.camerakit.plugin.v1","description":"Camera Kit\nit's used for effects.\nhttps://play.google.com/store/apps/details?id=com.snap.camerakit.plugin.v1","removal":"delete","type":"oem"},{"id":"com.softwinner.explore","description":"File Manager of SoftWinner\nCan be disabled via system settings.","removal":"delete","type":"oem"},{"id":"com.softwinner.service","description":"Possibly related to the firmware updater, since the updater has a similar name","removal":"replace","type":"oem"},{"id":"com.sohu.inputmethod.sogou.meizu","description":"Chinese keyboard closed-source","removal":"replace","type":"oem"},{"id":"com.sohu.inputmethod.sogou.nubia","description":"Chinese keyboard Closed-source (install other keyboard before removing this)\nBetter alternative: https://f-droid.org/en/packages/dev.patrickgold.florisboard/","removal":"delete","type":"oem"},{"id":"com.sohu.inputmethod.sogou.xiaomi","description":"Sogou keyboard for chinese only.\n","removal":"delete","type":"oem"},{"id":"com.sohu.inputmethod.sogouoem","description":"Default keyboard\n","removal":"caution","type":"oem"},{"id":"com.sohu.sohuvideo.emplayer","description":"HiMoviePlayerPlus\nWeird app without any code and there's nothing in Main Activity.","removal":"delete","type":"oem"},{"id":"com.sony.dtv.calibrationmonitor","description":"Screen calibration app\nhttps://play.google.com/store/apps/details/Calman%20for%20BRAVIA?id=com.sony.dtv.calibrationmonitor&hl=en_US\nUseful app, but can be removed if you don't need it.","removal":"replace","type":"oem"},{"id":"com.sony.dtv.ecodashboard","description":"Used to configure eco friendly settings. Breaks some options in the settings app if removed.","removal":"replace","type":"oem"},{"id":"com.sony.dtv.livingfit","description":"Turns your tv into an accessory by showing pictures. This is different to the screensaver and won't affect it.\nhttps://play.google.com/store/apps/details?id=com.sony.dtv.livingfit&hl=en_US","removal":"delete","type":"oem"},{"id":"com.sony.dtv.notificationcenter","description":"Notification app which reminds you to use apps from Sony's ecosystem. Can be removed safely without breaking other notifications","removal":"delete","type":"oem"},{"id":"com.sony.dtv.osat.music","description":"Music app which streams from your files.\nhttps://play.google.com/store/apps/details?id=com.sonyericsson.music&hl=en_US\nCan be removed if you don't need it.","removal":"delete","type":"oem"},{"id":"com.sony.dtv.promos","description":"Sony's promotion app with offers. You need an account to use it","removal":"delete","type":"oem"},{"id":"com.sony.dtv.recapp","description":"Allows you to view your reminders and timers but not add them? Can be removed safely","removal":"delete","type":"oem"},{"id":"com.sony.dtv.seeds.iot","description":"Allows you to control your TV with smart speaker e.g Alexa:\nhttps://play.google.com/store/apps/details?id=com.sony.dtv.seeds.iot&hl=en_US","removal":"delete","type":"oem"},{"id":"com.sony.dtv.smarthelp","description":"App with documentation of how to use a smart TV. Removing it has no effect","removal":"delete","type":"oem"},{"id":"com.sony.dtv.sonyselect","description":"Shows you bunch of popular subscription services and apps and redirects you to the play store. Better to use play store itself","removal":"delete","type":"oem"},{"id":"com.sony.dtv.timers","description":"Sony's clock app.\n Can be removed safely but on some Android Tv launchers it makes the Timer button not function. Not a deal breaker as the feature is pretty useless.","removal":"delete","type":"oem"},{"id":"com.sony.dtv.tvx","description":"Handles input settings. HDMI stops working if this is removed.","removal":"caution","type":"oem"},{"id":"com.sony.tvsideview.phone","label":"Video & TV SideView : Remote","description":"Sony's TV remote control app","web":["https://play.google.com/store/apps/details?id=com.sony.tvsideview.phone"],"removal":"delete","type":"oem"},{"id":"com.sony.tvsideview.videoph","label":"Video","description":"Sony's Video & TV SideView (replaced by \"com.sony.tvsideview.phone\")\nLets you use your smartphone or tablet as a TV remote control for the home.","removal":"delete","type":"oem"},{"id":"com.sonyericsson.album","description":"Sony gallery app (https://play.google.com/store/apps/details?id=com.sonyericsson.album)\n","removal":"replace","type":"oem"},{"id":"com.sonyericsson.android.addoncamera.artfilter","description":"Sony Creative effect\nGives options for various photographic toning effects in the Sony camera app.\nI'm not 100% sure for this one. Can someone confirm ? \n","removal":"replace","type":"oem"},{"id":"com.sonyericsson.android.camera3d","description":"Sony camera app (on older phones)\n","removal":"replace","type":"oem"},{"id":"com.sonyericsson.android.conversations","label":"Messaging","description":"Sony's default messages (SMS) app","removal":"replace","suggestions":"sms","type":"oem"},{"id":"com.sonyericsson.android.drm.drmlicenseservice","description":"Theres only Drm License Activity, only downloads license, also japanese","removal":"delete","type":"oem"},{"id":"com.sonyericsson.android.omacp","description":"omacp = OMA Client Provisioning. It is a protocol specified by the Open Mobile Alliance (OMA).\nIt is used by carrier to send \"configuration SMS\" which can setup network settings (such as APN).\nIn my case, it was automatic and I never needed configuration messages. I'm pretty sure that in France this package is useless.\nMaybe it's useful if carriers change their APN... but you still can change it manually, it's not difficult.\nThese special \"configuration SMS\" can be abused : \nhttps://www.zdnet.fr/actualites/les-smartphones-samsung-huawei-lg-et-sony-vulnerables-a-des-attaques-par-provisioning-39890045.htm\nhttps://www.csoonline.com/article/3435729/sms-based-provisioning-messages-enable-advanced-phishing-on-android-phones.html\n","removal":"replace","type":"oem"},{"id":"com.sonyericsson.conversations.res.overlay","description":"Used to display a overlay notification (= on top of others app) when you receive a SMS with Sony SMS app ?\n","removal":"replace","type":"oem"},{"id":"com.sonyericsson.conversations.res.overlay_305","description":"Used to display a overlay notification (= on top of others app) when you receive a SMS with Sony SMS app ?","removal":"delete","type":"oem"},{"id":"com.sonyericsson.idd.agent","label":"Anonymous Usage Stats","description":"Used to send \"anonymous\" information about how you use your Sony Smartphone to Sony servers.\nIt remains unclear exactly how this information is anonymized.","removal":"delete","type":"oem"},{"id":"com.sonyericsson.mtp","description":"MTP extension service\nNeeded to transfer data from phone to PC through MTP? (Media Transfer Protocol)","removal":"caution","type":"oem"},{"id":"com.sonyericsson.mtp.extension.backuprestore","description":"Backup/Restore Sony feature.\nEnables you to backup contacts, call logs, text messages, calendar, settings, bookmarks & media files.\nNOTE: I don't think this feature can backup your messages or calendars for instance if you don't use the Sony stock app.\nhttps://support.sonymobile.com/global-en/xperiaz2/userguide/backing-up-and-restoring-content-on-a-device/","removal":"replace","type":"oem"},{"id":"com.sonyericsson.mtp.extension.update","description":"Update service for MTP Extension.\nUpdates something for the MTP extension?","removal":"caution","type":"oem"},{"id":"com.sonyericsson.music","description":"Sony music player (https://play.google.com/store/apps/details?id=com.sonyericsson.music)\n","removal":"replace","type":"oem"},{"id":"com.sonyericsson.settings.res.overlay_305","description":"Some overlay for settings? Overlays are usually themes.","removal":"caution","type":"oem"},{"id":"com.sonyericsson.startupflagservice","description":"Startup Flag Service\nUsed during the production of the phone to verify that the touch input works. \nIt can be triggered when a specific TA-parameter is not set. This should never be triggered and if it does well it doesn't have any use for you.\n\nTA means Timing Advance and its value correspond to the length of time a signal takes to reach the base station from a mobile phone.\nhttps://www.telecomhall.net/t/parameter-timing-advance-ta/6390\n","removal":"delete","type":"oem"},{"id":"com.sonyericsson.textinput.chinese","description":"Sony chinese keyboard\n","removal":"delete","type":"oem"},{"id":"com.sonyericsson.trackid.res.overlay","description":"Some overlay for TrackID. Overlays are usually themes.\nTrackID was(now discontinued) a music and audio search engine (like Shazam).","removal":"caution","type":"oem"},{"id":"com.sonyericsson.trackid.res.overlay_305","description":"Overlay for TrackID. Overlays are usually themes.\nTrackID was(now discontinued) a music and audio search engine (like Shazam).","removal":"caution","type":"oem"},{"id":"com.sonyericsson.unsupportedheadsetnotifier","label":"Unsupported Headset Notifier","description":"Given its name, I think it displays a notification when you insert a headset not compatible with your phone.","removal":"replace","type":"oem"},{"id":"com.sonyericsson.wappush","description":"WAP Push\nUsed to display annoying WAP push.\nWAP push is a type of text message that contains a direct link to a particular Web page. \nWhen a user is sent a WAP-push message, he receives an alert, once clicked, directs him to the Web page via his browser.\nPersonally, I don't like this. URLs are now recognized by the SMS instant messaging apps and you just have to click on it.\n","removal":"delete","type":"oem"},{"id":"com.sonyericsson.warrantytime","description":"Lets you see some info about your warranty and how long it will last.","removal":"delete","type":"oem"},{"id":"com.sonyericsson.xhs","description":"Sony Xperia Lounge (discontinued by Sony on August 2019)\nThe Xperia Lounge app was meant to provide loyal fans with various rewards for their Xperia smartphones, \nsuch as exclusive Xperia Themes and wallpapers, as well as competitions.\n","removal":"delete","type":"oem"},{"id":"com.sonymobile.advancedlogging","description":"Advanced Logging\nSends logs to Sony Mobile. These logs contain a wide range of personal information such as unique device IDs, your location, \ndetails regarding running applications, and events/input leading up to a crash.\nLogging is only active for a short time and automatically disabled once logging has been completed. \nLogs are uploaded when connected to Wi-Fi and automatically deleted when the upload is complete.\n","removal":"delete","type":"oem"},{"id":"com.sonymobile.advancedwidget.topcontacts","description":"Top Contacts widget\nIt will show pictures of your most frequently used contacts right on your home screen.\nREMINDER : Widgets are small applications that you can use directly on the window screen. They also function as shortcuts\n","removal":"delete","type":"oem"},{"id":"com.sonymobile.android.addoncamera.soundphoto","description":"Sony Sound Photo\nLets you record a background sound and take a photo at the same time with the Sound Photo app.\n","removal":"delete","type":"oem"},{"id":"com.sonymobile.android.contacts","description":"Sony contacts\n","removal":"replace","suggestions":"contacts","type":"oem"},{"id":"com.sonymobile.android.contacts.res.overlay_305","description":"Overlay for Sony contacts. Overlays are usually themes.","removal":"delete","type":"oem"},{"id":"com.sonymobile.android.externalkeyboard","description":"International keyboard layouts\nUseless if you use a latin keyboard\n","removal":"replace","suggestions":"keyboards","type":"oem"},{"id":"com.sonymobile.android.externalkeyboardjp","description":"Japanese layout for Sony keyboard.\n","removal":"replace","suggestions":"keyboards","type":"oem"},{"id":"com.sonymobile.androidapp.cameraaddon.areffect","description":"Old package for AR Effect (https://play.google.com/store/apps/details?id=com.sonymobile.androidapp.cameraaddon.areffect)\nLets you add AR (Augmented Reality) effects to your pictures and videos.\n","removal":"delete","type":"oem"},{"id":"com.sonymobile.anondata","description":"Anonymous Usage Stats (yes just as com.sonyericsson.idd.agent but it's for other phones)\nUsed to send \"anonymous\" information about how you use your Sony Smartphone to Sony servers.\nIt remains unclear exactly how this information is anonymized.","removal":"delete","type":"oem"},{"id":"com.sonymobile.apnupdater","description":"Automatically updates APN settings if your carrier changes them? I thought that was the role of com.android.carrierconfig\nAPN: https://tamingthedroid.com/what-apn-settings-mean","removal":"replace","type":"oem"},{"id":"com.sonymobile.apnupdater.res.overlay_305","description":"Overlay for APN Updater. Overlays are usually themes.","removal":"caution","type":"oem"},{"id":"com.sonymobile.aptx.notifier","description":"Aptx Notifier\naptX (formerly apt-X) is a family of proprietary audio codec compression algorithms owned by Qualcomm.\nIf you don't mind closed source codec, aptX has lower latency and is less of a drain on your battery than default codec (AAC)\nThis package is used to display a notification when a device using aptX (bluetooth headphone typically) is connected.\nIts only use is to tell you that you use aptX bluetooth with the connected device.\n","removal":"replace","type":"oem"},{"id":"com.sonymobile.assist","description":"Xperia Assist (https://play.google.com/store/apps/details?id=com.sonymobile.assist)\nLearns how you use your phone.\n","removal":"delete","type":"oem"},{"id":"com.sonymobile.assist.persistent","description":"Related to Xperia Assist (see just above) but I don't know its purpose.\n","removal":"delete","type":"oem"},{"id":"com.sonymobile.camera","description":"Sony camera app\n","removal":"replace","suggestions":"cameras","type":"oem"},{"id":"com.sonymobile.cameracommon.wearablebridge","description":"Camera Wearable bridge\nLets you take pictures with your phone by using Sony SmartWatch.\n","removal":"delete","type":"oem"},{"id":"com.sonymobile.cellbroadcast.notification","description":"Cell information\nCell broadcast is designed to deliver messages to multiple users in an area.\nThis is notably used by ISP to send Emergency/Government alerts.\nhttps://en.wikipedia.org/wiki/Cell_Broadcast\nhttps://www.androidcentral.com/amber-alerts-and-android-what-you-need-know\nI think this package only handles notifications for broadcasts, not the implementation.\nIt seems like broadcast SMS use normal notifications so there is a chance this package provides special notification for Sony SMS app?","removal":"replace","type":"oem"},{"id":"com.sonymobile.coverapp2","description":"Style Cover\nThemes for lockscreen.\n","removal":"delete","type":"oem"},{"id":"com.sonymobile.demoappchecker","description":"Demo app checker\nLets you enter/exit (in) the demonstration mode.\nhttps://en.wikipedia.org/wiki/Demo_mode\n","removal":"delete","type":"oem"},{"id":"com.sonymobile.deviceconfigtool","description":"Configuration agent\nSeems to do things cloud related but it's unclear.\nhttps://knowledge.protektoid.com/apps/com.sonymobile.deviceconfigtool/91e44f1e19b364411776d758ff3b27f703bd4b60c9399c43c124f37d0c30df27\n","removal":"delete","type":"oem"},{"id":"com.sonymobile.devicesecurity.service","label":"DeviceSecurityService","description":"Not needed. Collects sim and network data.","removal":"delete","type":"oem"},{"id":"com.sonymobile.dualshockmanager","description":"DUALSHOCK\nProvide PlayStation DualShock controller support for Android (Settings > Device connection > Dualshock)\n","removal":"delete","type":"oem"},{"id":"com.sonymobile.email","label":"Sony Email","description":"Sony Email app","removal":"replace","suggestions":"email_clients","type":"oem"},{"id":"com.sonymobile.entrance","description":"What's New (discontinued in 2014)\nProvided news from Sony products through extremely annoying automated notifications.\n","removal":"delete","type":"oem"},{"id":"com.sonymobile.getmore.client","description":"Xperia Tips (https://play.google.com/store/apps/details?id=com.sonymobile.getmore.client)\nGives you tips for your Xperia device.\n","removal":"delete","type":"oem"},{"id":"com.sonymobile.getset","description":"Xperia Actions (discontinued)\nLets you automate some actions (only a few) \nhttps://support.sonymobile.com/global-en/xperiaxz/userguide/xperia-actions/\n","removal":"delete","type":"oem"},{"id":"com.sonymobile.getset.priv","description":"Xperia Actions System\nSame thing as com.sonymobile.getset.\n","removal":"delete","type":"oem"},{"id":"com.sonymobile.gettoknowit","description":"Introduction to Xperia (discontinued)\nIntroduces you the features of your phone.\n","removal":"delete","type":"oem"},{"id":"com.sonymobile.glovemode","description":"Sony Glove mode\nLets you use your smart phone and touch the screen while wearing regular gloves.\n","removal":"delete","type":"oem"},{"id":"com.sonymobile.googleanalyticsproxy","description":"Google Analytics Proxy\nAllows you to publicly share your Google Analytics reporting data\nhttps://developers.google.com/analytics/solutions/google-analytics-super-proxy\n","removal":"delete","type":"oem"},{"id":"com.sonymobile.home.product.res.overlay","description":"","removal":"caution","type":"oem"},{"id":"com.sonymobile.indeviceintelligence","description":"Xperia Intelligence Engine\nThis app is supposed to understand how you use the phone, the apps you prefer, and will suggest tips \nand options based on app usage, how often you use an app, what time of day...\nFor me this just looks like a AI bullshit app who has a huge list of permissions and launch in background at boot\nThis app performs geofencing (check if your are located in a certain perimeter, near your home for instance) \nand this doesn't looks great privacy-wise (https://en.wikipedia.org/wiki/Geo-fence)\n","removal":"delete","type":"oem"},{"id":"com.sonymobile.intelligent.backlight","description":"Smart backlight control\nKeeps the screen on as long as the device is held in your hand. Once you put down the device, the screen turns off according to your sleep setting.\n","removal":"delete","type":"oem"},{"id":"com.sonymobile.intelligent.gesture","description":"Smart call handling\nLets you handle incoming calls without touching the screen.\nhttps://support.sonymobile.com/global-en/xperiaxz/userguide/smart-call-handling/\n","removal":"delete","type":"oem"},{"id":"com.sonymobile.intelligent.iengine","description":"According to a Sony user it is part of Smart Screen rotation (auto screen rotation based on the gyroscope). Doesn't seem reliable.\nDoes it break the screen-rotation if removed?\nOn Xperia 10VI: doesn't break rotation.","removal":"delete","type":"oem"},{"id":"com.sonymobile.intelligent.observer","description":"IntelligentObserver\n???? (but intelligent stuff are safe to remove)\n","removal":"delete","type":"oem"},{"id":"com.sonymobile.lifelog","description":"Lifelog (https://play.google.com/store/apps/details?id=com.sonymobile.lifelog)\nAnother activity tracker app.\n","removal":"delete","type":"oem"},{"id":"com.sonymobile.moviecreator.rmm","description":"Movie Creator (https://play.google.com/store/apps/details?id=com.sonymobile.moviecreator.rmm)\nAutomatically creates short movies using your photos and videos.\n","removal":"delete","type":"oem"},{"id":"com.sonymobile.mtp.extension.fotaupdate","description":"fota update service\nFOTA = Firmware Over-The-Air\nFOTA allows manufacturers to remotely install new software updates, features and services.\nGiven there is \"mtp.extension\" in the package name, I think it lets you update your phone via your PC.\nWhat's weird is that it should be called SEUS then (https://www.mobilefun.co.uk/blog/2008/06/software-updates-sony-ericsson/)","removal":"replace","type":"oem"},{"id":"com.sonymobile.music.googlelyricsplugin","label":"Google lyrics extension","description":"Provides lyrics from Google in the Sony music app.","removal":"delete","type":"oem"},{"id":"com.sonymobile.music.wikipediaplugin","description":"Wikipedia plugin for sony music app\n","removal":"delete","type":"oem"},{"id":"com.sonymobile.music.youtubekaraokeplugin","description":"YouTube karaoke plugin for sony music app\n","removal":"delete","type":"oem"},{"id":"com.sonymobile.music.youtubeplugin","description":"YouTube plugin for sony music app\n","removal":"delete","type":"oem"},{"id":"com.sonymobile.pip","label":"pip","description":"Sony's PiP (Picture in Picture)\nAllows videos to shrink down to a small resizable window.\nOnly useful bere Android Oreo which provide native support for PiP?","web":["https://developer.android.com/guide/topics/ui/picture-in-picture","https://support.sonymobile.com/global-en/xperiaxz1compact/faq/apps-&-settings/8019307455ff6184015e92f63324005926/"],"removal":"delete","type":"oem"},{"id":"com.sonymobile.pobox","label":"Xperia™ Japanese Keyboard","description":"Package is named after POBox (Predictive Operation Based On eXample), a Japanese text entry technology and ambiguous retrieval, proposed in 1998 by Sony CSL fellow Toshiyuki Masuda.","web":["https://www.sonycsl.co.jp/project/402/"],"removal":"delete","type":"oem"},{"id":"com.sonymobile.prediction","description":"Sony text prediction (for Sony keyboard) \nIt's only a supposition. Can someone confirm ?\n","removal":"delete","type":"oem"},{"id":"com.sonymobile.retaildemo","description":"Retail Demo\nRetail demonstration mode.\nhttps://en.wikipedia.org/wiki/Demo_mode\n","removal":"delete","type":"oem"},{"id":"com.sonymobile.scan3d","description":"Sony 3D Creator (https://play.google.com/store/apps/details?id=com.sonymobile.scan3d)\nLets you capture your stuff in 3D, from your smartphone, and turn people and objects into high-resolution 3D avatars.\nhttps://www.sonymobile.com/global-en/apps-services/3d-creator/\n","removal":"delete","type":"oem"},{"id":"com.sonymobile.simlockunlockapp","description":"Sim Lock\nProvide menu (type *#*#7378423#*#* in dialer) to see if your device is locked to a network carrier\nIt need confirmation because it also could be related to SIM network unlock code.\n","removal":"delete","type":"oem"},{"id":"com.sonymobile.smartcharger","label":"Battery Care","description":"Detects your charging patterns and estimates the start and end time of your regular charging period. \nThe rate of charging is controlled so that your battery reaches 100% just before you disconnect the charger.\nhttps://support.sonymobile.com/gb/xperiaxz/userguide/battery-and-power-management/\n","removal":"replace","type":"oem"},{"id":"com.sonymobile.support","description":"Sony Support (https://play.google.com/store/apps/details?id=com.sonymobile.support)\nUseless support app. \n","removal":"delete","type":"oem"},{"id":"com.sonymobile.swiqisystemservice","description":"","removal":"caution","type":"oem"},{"id":"com.sonymobile.synchub","description":"Sony Backup & restore feature to/from Google drive ?\nCan someone confirm ? Does it impact com.sonyericsson.mtp.extension.backuprestore ?\nhttps://support.sonymobile.com/global-en/xperia10/faq/apps-&-settings/801930747866b72a016b307df3b6007faf/\n","removal":"delete","type":"oem"},{"id":"com.sonymobile.telephony.extension","description":"I have uninstalled it for the user 0 on my phone, and since then I haven't noticed any functionality drop.\nCell signal is similar as before, also calls, SMSs and mobile data. I think it is not worth having this package installed because it uses a lot of wakelock time, but I cannot see any cell signal drop since I uninstalled it.\nIn the code found some things: omadm, wifi calling, IMS. Its app for dual SIM reachability? Not sure if its app for debugging.","removal":"replace","type":"oem"},{"id":"com.sonymobile.themes.sou.cid18.black","description":"Sony themes","removal":"caution","type":"oem"},{"id":"com.sonymobile.themes.sou.cid19.silver","description":"Sony themes","removal":"caution","type":"oem"},{"id":"com.sonymobile.themes.sou.cid20.blue","description":"Sony themes","removal":"caution","type":"oem"},{"id":"com.sonymobile.themes.sou.cid21.pink","description":"Sony themes","removal":"caution","type":"oem"},{"id":"com.sonymobile.themes.xperialoops2","description":"Sony themes","removal":"caution","type":"oem"},{"id":"com.sonymobile.xperialounge.services","description":"Xperia™ Lounge Pass service (discontinued)\nThe Xperia Lounge app was meant to provide loyal fans with various rewards for their Xperia smartphones, \nsuch as exclusive Xperia Themes and wallpapers, as well as competitions.\nhttps://www.phonearena.com/news/Sony-Xperia-Lounge-shutting-down_id118252\n","removal":"delete","type":"oem"},{"id":"com.sonymobile.xperiaservices","description":"Xperia services\nI guess it provides things for Sony apps but I don't know what.\nSafe to remove but it would good be to know what Sony apps work without it.\n","removal":"delete","type":"oem"},{"id":"com.sonymobile.xperiatransfermobile","description":"Xperia Transfer Mobile (https://play.google.com/store/apps/details?id=com.sonymobile.xperiatransfermobile)\nHelps you move your contacts, messages, photos, and much more from your old Android, iOS or Windows Phone device to your new Xperia from Sony.\n","removal":"delete","type":"oem"},{"id":"com.sonymobile.xperiaweather","label":"Weather","description":"Sony weather app\nNote : Not all location are supported.","web":["https://play.google.com/store/apps/details?id=com.sonymobile.xperiaweather"],"removal":"replace","suggestions":"weather_apps","type":"oem"},{"id":"com.sonymobile.xperiaxlivewallpaper","description":"Xperia Loops\nUseless and ugly live wallaper from Sony.","removal":"delete","type":"oem"},{"id":"com.sonymobile.xperiaxlivewallpaper.product.res.overlay","description":"Some overlay for a live wallpaper from Sony? Overlays are usually themes, but not sure about this one as theming seems weird for live wallpapers. Could be that Sony automatically generates theme packages for all or most system apps, which might generate some unnecessary packages.","removal":"delete","type":"oem"},{"id":"com.sprd.ImsConnectionManager","description":"Needed for IMS, WiFi Calling.","removal":"caution","type":"oem"},{"id":"com.sprd.autoslt","description":"AutoSLT\nUseless camera tests.","removal":"delete","type":"oem"},{"id":"com.sprd.cameracalibration","description":"Camera calibration, tests.","removal":"delete","type":"oem"},{"id":"com.sprd.cameraipcontrol","description":"Secret code: 83785. CameraIPControl, has some camera features for testing.","removal":"delete","type":"oem"},{"id":"com.sprd.camta","description":"Hidden camera dump logs.","removal":"delete","type":"oem"},{"id":"com.sprd.engineermode","description":"Testing Hardware Components","removal":"delete","type":"oem"},{"id":"com.sprd.firewall","description":"Blacklist Calls.","removal":"replace","type":"oem"},{"id":"com.sprd.flashcontrol","description":"Flashlight Control, not sure how useful it is.","removal":"replace","type":"oem"},{"id":"com.sprd.linkturbo","description":"For WiFi speed? Has useless logs. I ran a speed test with it enabled and disabled, I don't see anything different if not my internet is slightly better now along with other apps being disabled,","removal":"delete","type":"oem"},{"id":"com.sprd.logmanager","description":"YLog\nHidden logs","removal":"delete","type":"oem"},{"id":"com.sprd.omacp","description":"OTA Config and Settings.","removal":"caution","type":"oem"},{"id":"com.sprd.overlay.sprdnote","description":"Overlay to 'com.sprd.sprdnote' png icons.","removal":"replace","type":"oem"},{"id":"com.sprd.providers.photos","description":"Photos Types Storage\nBokeh, portrait, FDR, AI photo.","removal":"replace","type":"oem"},{"id":"com.sprd.quickcamera","description":"camera agent\nAnother app for testing camera things.","removal":"delete","type":"oem"},{"id":"com.sprd.srmi","description":"Hidden logs in code only.","removal":"delete","type":"oem"},{"id":"com.sprd.uasetting","description":"UserAgent Setting\nUserAgent Setting? Useless frameworks.","removal":"delete","type":"oem"},{"id":"com.sprd.uplmnsettings","description":"Set UPLMN? Useless frameworks.","removal":"delete","type":"oem"},{"id":"com.sprd.validationtools","description":"Hidden testing hardware and it has a lot of secret codes.","removal":"delete","type":"oem"},{"id":"com.spreadtrum.ims","description":"Needed for WiFi calling, VoLTE, VoWifi.","removal":"replace","type":"oem"},{"id":"com.spreadtrum.proxy.nfwlocation","description":"Carrier location\nCarrier location? Sends location to carrier probably. Useless Map Collections, also probably it's for testing location.","removal":"delete","type":"oem"},{"id":"com.spreadtrum.sgps","description":"Useless GPS Test.","removal":"delete","type":"oem"},{"id":"com.spreadtrum.vce","description":"Useless logs (dialer google, volte)","removal":"delete","type":"oem"},{"id":"com.spreadtrum.vowifi","description":"WiFi Calling Test\nHidden tests WiFi calling.","removal":"delete","type":"oem"},{"id":"com.spreadtrum.vowifi.conf","description":"VoWifi config Secret Code: 869434234","removal":"replace","type":"oem"},{"id":"com.srin.indramayu","label":"Samsung Gift Indonesia","description":"Special application from Samsung that provides special offers and privileges for Indonesian users","removal":"delete","type":"oem"},{"id":"com.ss.android.article.news","description":"Chinese app NEWS Toutiao.","removal":"delete","type":"oem"},{"id":"com.ss.android.ugc.aweme","description":"Chinese TikTok.","removal":"delete","type":"oem"},{"id":"com.st.nfc.dta.mobile","description":"STNFCDta\nUnknown checking app (NFC UID gen mode, NFC CDL value, CR version, Extended RF frame size, T4AT priority over P2P).","removal":"caution","type":"oem"},{"id":"com.stability.camerastability","description":"CameraStability\nHidden camera test","removal":"delete","type":"oem"},{"id":"com.stevesoltys.seedvault","description":"Seedvault\nRunning in the background.\nit's app for Backup, Restore your data.","removal":"delete","type":"oem"},{"id":"com.summit.motorola.rcs","description":"Summit IMS Service\nA lot of logs and it's only for RCS messages. Anyway, it's a bad idea to use this app.","removal":"delete","type":"oem"},{"id":"com.suntek.mway.rcs.app.service","description":"RCS service, contains a lot of Chinese.","removal":"delete","type":"oem"},{"id":"com.svox.pico","description":"Pico TTS\nHidden sample voice data. Useless.","removal":"delete","type":"oem"},{"id":"com.swfp.factory","description":"Fingerprint test\nHidden app that tests your fingerprint not available for users.","removal":"delete","type":"oem"},{"id":"com.t2m.euiccoverlay","description":"Possibly needed for eSIM (eUICC)","removal":"caution","type":"oem"},{"id":"com.taboola.scoop","description":"Random wallpaper on lock screen (as i saw it personalizes it too). You need to open this app to make it work and when you swipe from right to left, it opens a menu on the right where random news appears.","removal":"delete","type":"oem"},{"id":"com.talpa.hiservice","description":"HiLanguageService\nUseless frameworks.","removal":"delete","type":"oem"},{"id":"com.tblenovo.center","description":"Useless dashboard related to the User Experience Program (com.lenovo.ue.device). Has 25 permissions (including ones you probably don't want to give to this kind of sketchy app\n\nPithus analysis: https://beta.pithus.org/report/dcb4acac003896077eaaeb8c7dc770d3171891784d98f7127f8495a3dec9954d","removal":"delete","type":"oem"},{"id":"com.tblenovo.lenovotips","description":"Useless Lenovo Tips app used by Lenovo to display un-dismissable and un-mutable ads in notifications.\n","web":["https://news.ycombinator.com/item?id=28382081"],"removal":"delete","type":"oem"},{"id":"com.tblenovo.lewea","label":"Lenovo Weather","description":"Lenovo weather application","web":["https://beta.pithus.org/report/96601b7ec8ced18bf3896946ab43edde94b14e09b95e7787ea941b25ca02164b"],"removal":"replace","suggestions":"weather_apps","type":"oem"},{"id":"com.tblenovo.setup","label":"SetupWizardExt","description":"Exactly not sure what it does.\n","removal":"caution","type":"oem"},{"id":"com.tblenovo.soundrecorder","label":"Sound Recorder","description":"Sound recorder for lenovo devices.\n","web":["https://beta.pithus.org/report/94123a466486800a367ecbedd5bbded54886834e001c20871d95c2820a9ea172"],"removal":"delete","type":"oem"},{"id":"com.tblenovo.whatsnewclient","label":"Lenovo Feature Update","description":"Exactly not sure what it does.\n","web":["https://beta.pithus.org/report/c1bf1b6a7b0bc987a654d8832274658251ba448858b34ce3b0a013309f9f0ade"],"removal":"caution","type":"oem"},{"id":"com.tblenovo.whatsnewhost","label":"Lenovo Feature Update Host","description":"Exactly not sure what it does.\n","web":["https://beta.pithus.org/report/e08b8712d07899653631f3a9ac12c0ed48ff7bdb699651724eba26871ac0ca2b"],"removal":"caution","type":"oem"},{"id":"com.tcl.android.launcher","label":"Launcher","description":"Stock Launcher app.","removal":"caution","type":"oem"},{"id":"com.tcl.android.launcher.a_overlay","description":"Another useless Chinese bookmarks icon overlay.","removal":"delete","type":"oem"},{"id":"com.tcl.android.launchertheme.res","label":"Launcher theme resources","description":"It's needed for themes, probably. Not sure if it's an important app.","removal":"caution","type":"oem"},{"id":"com.tcl.android.launchertheme.res.overlay","description":"","removal":"caution","type":"oem"},{"id":"com.tcl.android.wallpaper.livepicker","description":"","removal":"caution","type":"oem"},{"id":"com.tcl.aota.a_overlay","description":"","removal":"caution","type":"oem"},{"id":"com.tcl.camera","description":"","removal":"caution","type":"oem"},{"id":"com.tcl.camera.a_overlay","description":"","removal":"caution","type":"oem"},{"id":"com.tcl.compass","description":"","removal":"caution","type":"oem"},{"id":"com.tcl.compass.a_overlay","description":"","removal":"caution","type":"oem"},{"id":"com.tcl.demopage","description":"","removal":"caution","type":"oem"},{"id":"com.tcl.entitlement","description":"","removal":"caution","type":"oem"},{"id":"com.tcl.faceunlock","description":"Standard FaceUnlock functionality?\nUnlock your device by simply looking at the display.\nFace unlock is bad for security and privacy:\nhttps://www.ubergizmo.com/2017/03/galaxy-s8-facial-unlock-photograph/\nhttps://www.kaspersky.com/blog/face-unlock-insecurity/21618/\nhttps://www.freecodecamp.org/news/why-you-should-never-unlock-your-phone-with-your-face-79c07772a28/","removal":"caution","type":"oem"},{"id":"com.tcl.fmradio","label":"Radio","description":"Stock Radio app","removal":"replace","suggestions":"radios","type":"oem"},{"id":"com.tcl.fmradio.a_overlay","description":"Another useless icon overlay.","removal":"delete","type":"oem"},{"id":"com.tcl.fota.system","label":"System Update","description":"Provides System Updates.","removal":"caution","type":"oem"},{"id":"com.tcl.fota.system.a_overlay","description":"","removal":"caution","type":"oem"},{"id":"com.tcl.healthy","description":"","removal":"caution","type":"oem"},{"id":"com.tcl.keyguardcharginganimation","description":"Useless frameworks.","removal":"delete","type":"oem"},{"id":"com.tcl.keyguardshortcut","description":"Useless frameworks.","removal":"delete","type":"oem"},{"id":"com.tcl.kidsmode","description":"Kids Mode\nIt's an app for limiting spending time on apps for kids probably.","removal":"delete","type":"oem"},{"id":"com.tcl.logger.a_overlay","description":"Another useless icon overlay.","removal":"delete","type":"oem"},{"id":"com.tcl.mibc.tclplus","description":"","removal":"caution","type":"oem"},{"id":"com.tcl.mibc.tclplus.a_overlay","description":"","removal":"caution","type":"oem"},{"id":"com.tcl.nfc.gsma.usermenu","description":"","removal":"caution","type":"oem"},{"id":"com.tcl.screenrecorder","description":"","removal":"caution","type":"oem"},{"id":"com.tcl.screenrecorder.a_overlay","description":"","removal":"caution","type":"oem"},{"id":"com.tcl.screenshotex","description":"","removal":"caution","type":"oem"},{"id":"com.tcl.sos","description":"","removal":"caution","type":"oem"},{"id":"com.tcl.tclswitch.a_overlay","description":"","removal":"caution","type":"oem"},{"id":"com.tcl.tct.filemanager","description":"File Manager\nStock File Manager app","removal":"replace","type":"oem"},{"id":"com.tcl.tct.weather","description":"Weather\nStock Weather app for TCL Phones.","removal":"replace","type":"oem"},{"id":"com.tcl.token","label":"Token","description":"It's for ECID number but it's safe to remove. Secret code 7383243.","removal":"delete","type":"oem"},{"id":"com.tcl.usercare","description":"Support Centre, alcatel support. Safe to remove.","removal":"delete","type":"oem"},{"id":"com.tcl.usercare.a_overlay","description":"Another useless icon overlay.","removal":"delete","type":"oem"},{"id":"com.tclhz.gallery","label":"Gallery","description":"Stock Gallery app","removal":"replace","suggestions":"gallery","type":"oem"},{"id":"com.tclhz.gallery.a_overlay","description":"","removal":"caution","type":"oem"},{"id":"com.tct","description":"","removal":"caution","type":"oem"},{"id":"com.tct.aio","label":"TCT All in One Configuration","description":"It probably collects some data and CheckForUpdateTask OTA.","removal":"caution","type":"oem"},{"id":"com.tct.android.SaleCalib","description":"Hidden camera after sale calibration.","removal":"delete","type":"oem"},{"id":"com.tct.android.browser","description":"Browser\nBetter use other browser.","removal":"delete","type":"oem"},{"id":"com.tct.android.secureinput","label":"SecureInput","description":"Useless secure input for keycode.","removal":"delete","type":"oem"},{"id":"com.tct.android.video","description":"Video Player\nStock Video Player app for TCL phones.","removal":"replace","type":"oem"},{"id":"com.tct.applock","label":"App lock","description":"It's just app lock with password or pin.","removal":"delete","type":"oem"},{"id":"com.tct.batterywarning","label":"Battery warning","description":"Displays only messages about low battery or too high temperature.","removal":"delete","type":"oem"},{"id":"com.tct.calculator","label":"Calculator","description":"Stock Calculator app","removal":"replace","suggestions":"calculators","type":"oem"},{"id":"com.tct.calculator.a_overlay","description":"","removal":"caution","type":"oem"},{"id":"com.tct.camera","description":"Camera\nStock Camera app","removal":"replace","type":"oem"},{"id":"com.tct.camera.a_overlay","description":"Useless overlay. It has only an icon and no code.","removal":"delete","type":"oem"},{"id":"com.tct.camera.verifytool","description":"Another hidden camera calibration and DualCamVerifyTool.","removal":"delete","type":"oem"},{"id":"com.tct.cellular.arda","description":"","removal":"caution","type":"oem"},{"id":"com.tct.compass","description":"Compass\nStock Compass app","removal":"delete","type":"oem"},{"id":"com.tct.contacts.transfer","label":"Transfer To Phone Contacts","description":"It's additional thing.","removal":"delete","type":"oem"},{"id":"com.tct.diagprotector","description":"","removal":"caution","type":"oem"},{"id":"com.tct.dialer","label":"Phone","description":"Stock Phone app","removal":"replace","suggestions":"dialers","type":"oem"},{"id":"com.tct.dialer.a_overlay","description":"","removal":"caution","type":"oem"},{"id":"com.tct.endusertest","description":"Unused device issue feedback app.","removal":"delete","type":"oem"},{"id":"com.tct.entitlement","description":"Requires Google Play services and is probably for Wi-Fi calling for O2 Carrier.","removal":"caution","type":"oem"},{"id":"com.tct.faceunlock","description":"Standard FaceUnlock functionality?\nUnlock your device by simply looking at the display.\nFace unlock is bad for security and privacy:\nhttps://www.ubergizmo.com/2017/03/galaxy-s8-facial-unlock-photograph/\nhttps://www.kaspersky.com/blog/face-unlock-insecurity/21618/\nhttps://www.freecodecamp.org/news/why-you-should-never-unlock-your-phone-with-your-face-79c07772a28/","removal":"caution","type":"oem"},{"id":"com.tct.gamemode","label":"Game box","description":"Game Turbo. Not sure if it's worth keeping or not.","removal":"caution","type":"oem"},{"id":"com.tct.gdpr","description":"","removal":"caution","type":"oem"},{"id":"com.tct.gpdr","description":"GPDR\nUser Experience Improvement Program.","removal":"delete","type":"oem"},{"id":"com.tct.iris","description":"NXTVISION\nIt's for reading mode and Display Enhancement.","removal":"delete","type":"oem"},{"id":"com.tct.logger","description":"User Support\nit's for logs.","removal":"delete","type":"oem"},{"id":"com.tct.multipleuser","description":"Multiple User\nI guess it's for multi-accounts.","removal":"delete","type":"oem"},{"id":"com.tct.music","label":"Music","description":"Stock Music app","removal":"replace","suggestions":"music_apps","type":"oem"},{"id":"com.tct.onetouchbooster","label":"Alcatel Smart Manager","description":"For power saving.","removal":"caution","type":"oem"},{"id":"com.tct.onetouchbooster.a_overlay","description":"","removal":"caution","type":"oem"},{"id":"com.tct.phone","description":"TCT Phone Services\nSim card settings.","removal":"caution","type":"oem"},{"id":"com.tct.privacymode","description":"","removal":"caution","type":"oem"},{"id":"com.tct.privacyprotect","description":"Privacy Protection\nAnother Security thing.","removal":"delete","type":"oem"},{"id":"com.tct.privatespace","label":"Private Space","description":"Only password things.","removal":"delete","type":"oem"},{"id":"com.tct.reducesar","description":"Hidden Modify the SAR mode.","removal":"delete","type":"oem"},{"id":"com.tct.retaildemo","description":"Demo Mode is intended for use on shop demo devices only and it should never be activated on a normal user's device. Explained by TCL.","removal":"delete","type":"oem"},{"id":"com.tct.retaildemo.a_overlay","description":"","removal":"caution","type":"oem"},{"id":"com.tct.screenrecorder","description":"Screen Recorder","removal":"replace","type":"oem"},{"id":"com.tct.screenshotex","description":"Screenshot\nit's used for screenshots and editing them.","removal":"caution","type":"oem"},{"id":"com.tct.secretCode","description":"This app has only logs.","removal":"delete","type":"oem"},{"id":"com.tct.setupwizard","label":"Setup Wizard","description":"It's needed only for first-boot setup.","removal":"delete","type":"oem"},{"id":"com.tct.sidebar","description":"Edge Bar\nSidebar","removal":"delete","type":"oem"},{"id":"com.tct.simplelauncher","label":"Simple Launcher","description":"It's not needed for the Stock launcher and has SOS and other unnecessary stuff.","removal":"delete","type":"oem"},{"id":"com.tct.simplelauncher.a_overlay","description":"","removal":"caution","type":"oem"},{"id":"com.tct.simsettings","description":"","removal":"caution","type":"oem"},{"id":"com.tct.smart.account","description":"","removal":"caution","type":"oem"},{"id":"com.tct.smart.aikey","description":"","removal":"caution","type":"oem"},{"id":"com.tct.smart.aota","description":"OTA Updates or OTA Updates test?","removal":"caution","type":"oem"},{"id":"com.tct.smart.cloud","description":"","removal":"caution","type":"oem"},{"id":"com.tct.smart.drivemode","description":"","removal":"caution","type":"oem"},{"id":"com.tct.smart.ircontrol","description":"IR Remote\nI think it's useless.","removal":"delete","type":"oem"},{"id":"com.tct.smart.lostmode","description":"Remote Lock\nIt's for lost mode.","removal":"delete","type":"oem"},{"id":"com.tct.smart.notes","label":"Smart Notes","description":"Stock Notes app","removal":"replace","suggestions":"note_taking_apps","type":"oem"},{"id":"com.tct.smart.push","description":"Smart Push\nUseless frameworks.","removal":"delete","type":"oem"},{"id":"com.tct.smart.switchphone","label":"Switch Phone","description":"It's app for Move apps to new or old phone.","removal":"delete","type":"oem"},{"id":"com.tct.smart.switchphone.service","description":"","removal":"caution","type":"oem"},{"id":"com.tct.smart.tlink","description":"Easy Link\nIt has something to do with screen casting. Dlna Cast Activity.","removal":"replace","type":"oem"},{"id":"com.tct.soundrecorder","label":"Sound Recorder","description":"An audio and screen recorder, lets you change voice.","removal":"replace","suggestions":"audio_recorders","type":"oem"},{"id":"com.tct.systemservice","description":"It should be safe to remove. Only secret code to info about system but not sure.","removal":"caution","type":"oem"},{"id":"com.tct.tctsmartapprecommend","description":"Smart App Recommend\nUseless.","removal":"delete","type":"oem"},{"id":"com.tct.usercare","description":"Support Center\nit's for TCL users support center app.","removal":"delete","type":"oem"},{"id":"com.tct.video","description":"","removal":"caution","type":"oem"},{"id":"com.tct.weather","label":"Weather Forecast","description":"Weather forecasting app.","removal":"replace","suggestions":"weather_apps","type":"oem"},{"id":"com.tct.weather.a_overlay","label":"com.tct.weather.a_overlay","description":"Overlay for com.tct.weather. Usage is not known.","removal":"delete","type":"oem"},{"id":"com.tct.wfcwebiew","label":"WfcWebView","description":"WebView app for TCL","removal":"caution","warning":"Make sure to have another WebView before removing it.","suggestions":"webviews","type":"oem"},{"id":"com.tct.wfcwebview","description":"I found something like entitlementMode but app looks very empty and useless.","removal":"delete","type":"oem"},{"id":"com.ted.number","description":"Identification of Unknown Numbers\nApp for identify unknown numbers.","removal":"delete","type":"oem"},{"id":"com.teksun.factorytest","description":"Hidden testing hardware things.","removal":"delete","type":"oem"},{"id":"com.telephony.service","description":"Needed for WiFi Calling.","removal":"replace","type":"oem"},{"id":"com.tencent.android.location","description":"Useless Tencent Chinese location","removal":"delete","type":"oem"},{"id":"com.tencent.ngjp","description":"Arena of Valor","removal":"delete","type":"oem"},{"id":"com.tencent.qqlivehuawei","description":"HiMoviePlayerPlus\nWeird app without any code and there's nothing in Main Activity.","removal":"delete","type":"oem"},{"id":"com.tencent.soter.soterserver","description":"Soter is a biometric authentication standard and platform by Tencent.\nhttps://github.com/Tencent/soter\nProvides biometric authentication for WeChat Pay. Safe to disable if you don't use it.","removal":"delete","type":"oem"},{"id":"com.test.LTEfunctionality","description":"LTE, VoLTE testing app","removal":"delete","type":"oem"},{"id":"com.theme.icondefaultshape","description":"The shape of the icons can be uninstalled or left alone, it's just the look of the icon.","removal":"replace","type":"oem"},{"id":"com.thinkuem.motolc","description":"ThinkIoT-UEM\nIt has remote service but probably it's unused.","removal":"delete","type":"oem"},{"id":"com.thundercomm.ar.core","description":"Looks like a debugging app.\nIt's AR (Augmented Reality)?","removal":"delete","type":"oem"},{"id":"com.tools.cit","description":"Hidden testing hardware things.","removal":"delete","type":"oem"},{"id":"com.touchscreen.vtstouchscreencheck","description":"VtsTouchscreenCheck\nIt's testing things app.","removal":"delete","type":"oem"},{"id":"com.tran.netcon","description":"Netcon\nit's unneccessary","removal":"delete","type":"oem"},{"id":"com.transsion","description":"Needed for screen unlock? Has Packages, Bluetooth permission. Probably unsafe.","removal":"caution","type":"oem"},{"id":"com.transsion.XOSLauncher","description":"Launcher with ads, tracking.\nAfter remove you will lose recent apps.","removal":"caution","type":"oem"},{"id":"com.transsion.aftersalecalibrationtool","description":"Hidden camera tests, calibration.","removal":"delete","type":"oem"},{"id":"com.transsion.agingfunction","label":"AgingFunction","description":"Another test mode, aging setting. secret code 2828, 2829, 2830.","web":["https://beta.pithus.org/report/02b71ec4be036fe87b5504b4f752a7c7cb45848b5d666c4307e59df754e164c9"],"removal":"delete","type":"oem"},{"id":"com.transsion.aisettings","description":"Adds a shortcut on the Settings app that lets you manage your Folax AI (com.transsion.aivoiceassistant) settings. If you have already removed that package, it just displays a blank page.\nThis can also be safely removed without issues.","removal":"delete","type":"oem"},{"id":"com.transsion.aivoiceassistant","description":"Very shady voice assistant called 'Folax' that comes preinstalled in Infinix phones, and is also packed with ads in the main ui. It needs every permission, and access to everything on your phone to run. It uses OpenAI as its backend.\nIt constantly runs in the background after boot, and periodically phones home.\nYou don't want this on your phone.\nThis can be safely removed without issues.","removal":"delete","type":"oem"},{"id":"com.transsion.aiwallpaper","description":"Mediocre AI Wallpaper Generator.\nCan be accessed from (Settings > Personalization).\nYou can safely remove this without any issues if you don't use it.","removal":"delete","type":"oem"},{"id":"com.transsion.aod","description":"AlwaysOnDisplay","removal":"delete","type":"oem"},{"id":"com.transsion.applock","label":"AppLock","description":"Provides the only way to hide files and lock apps when using Transsion launchers (which the Transsion devices require for certain functionality like multiapps not working with other launchers). These are the only apps that hide files and lock apps with them.","removal":"caution","warning":"Removal breaks the Recents feature.","type":"oem"},{"id":"com.transsion.audioshare","description":"Audio Share\nAllows you to share your device’s Bluetooth audio with wireless headphones or bluetooth speakers, allowing to listen to the same music with multiple people\nPithus analysis: https://beta.pithus.org/report/0f21ba3944663e53da1d37be3c4253c2e89c3685fbff841127fed2a98e0000ec","removal":"delete","type":"oem"},{"id":"com.transsion.auto","description":"Driving Mode\nDriving Mode app.","removal":"delete","type":"oem"},{"id":"com.transsion.autotest.factory","description":"Factory. Testing Hardware Things.","removal":"delete","type":"oem"},{"id":"com.transsion.avatar","description":"Is this for T-moji avatar services? it's useless.","removal":"delete","type":"oem"},{"id":"com.transsion.batterylab","description":"Supposed to improve battery life but logs especially lots of usage info and bind it to your unique android advertiser id...The app tries to send data to a server. The POST request URL and content is obfuscated and I don't have the time to dig deeper. According to a user, no battery impact after months of usage after uninstalling it.\n\nPithus analysis: https://beta.pithus.org/report/7ef2b186a74102828346f23b094ab2aaaad2c57806c7c18e7a23a494f3cc982c","removal":"delete","type":"oem"},{"id":"com.transsion.batterylab.icon","description":"Power Marathon\nIt's only icon app.","removal":"replace","type":"oem"},{"id":"com.transsion.beezedit","description":"Ringtone Maker\nRequire Google Play Services.\nRecord media sound and edit or set it as ringtone.","removal":"delete","type":"oem"},{"id":"com.transsion.bluetooth","description":"Airlink\nBluetooth still work without this.\n(not sure if its needed for transfer files using bluetooth)","removal":"replace","type":"oem"},{"id":"com.transsion.calculator","description":"Calculator\nStock Calculator app. Lot of telemetry and are completely replaceable.","removal":"delete","type":"oem"},{"id":"com.transsion.calendar","description":"Calendar\nStock Calendar app","removal":"replace","type":"oem"},{"id":"com.transsion.camera","description":"Camera\nStock camera app","removal":"replace","type":"oem"},{"id":"com.transsion.carlcare","description":"Carlcare (https://play.google.com/store/apps/details?id=com.transsion.carlcare)\nAfter-sales Service app. Lets you check spare parts price,warranty,repair status and nearest service center. Full of trackers. Talks with Facebook (https://reports.exodus-privacy.eu.org/fr/reports/com.transsion.carlcare/latest/)","removal":"delete","type":"oem"},{"id":"com.transsion.childmode","label":"Kids Mode","description":"Phone monitoring app to control what the user the can do on the phone. Intrusive and use Firebase so it's sends data back to Google servers.","web":["https://beta.pithus.org/report/ca30c6d1d7c7625e0850c4114dfea5aab5118d391191d2c074cde1414bbccd8c"],"removal":"delete","type":"oem"},{"id":"com.transsion.childmode.resoverlay","description":"Unused overlay to childmode.","removal":"delete","type":"oem"},{"id":"com.transsion.chromecustomization","description":"Chrome Assistant\nCustomizes HomePage and uses Advertising ID.","removal":"delete","type":"oem"},{"id":"com.transsion.connectx.mirror.source","description":"it's something for cast setting, file transfer setting, smart connect to pc by TCCP","removal":"delete","type":"oem"},{"id":"com.transsion.datatransfer","description":"Backup and Restore\nBackup Contacts.","removal":"delete","type":"oem"},{"id":"com.transsion.deadcycletest","description":"DeadCycleTest\nHidden tests.","removal":"delete","type":"oem"},{"id":"com.transsion.deskclock","description":"Clock\nCan manage alarms by this app.","removal":"replace","type":"oem"},{"id":"com.transsion.dirac","description":"Improves audio quality depending on your surrounding environment and your headphones.\nHas the GET_TASKS/REAL_GET_TASKS permission which allows it to retrieve information about currently and recently running processes. Not sure why it needs this permission though.\nhttps://www.dirac.com/\nPithus analysis: https://beta.pithus.org/report/b2cf41f579c586468faa0270bf63699cca2b500887dba3a699ddd5e35507a1a9","removal":"replace","type":"oem"},{"id":"com.transsion.dtsaudio","description":"DTS Sound\nVery safe to disable and just serves as a way to modify audio playing and runs in the background and has telemetry can be replaced by other better apps","removal":"delete","type":"oem"},{"id":"com.transsion.dualapp","description":"Dual App\nNeeded for Dual Apps.","removal":"delete","type":"oem"},{"id":"com.transsion.faceid","description":"Needed for Face Unlock lock screen.","removal":"caution","type":"oem"},{"id":"com.transsion.faceidsub","description":"Face Unlock\nSafe to remove if unused.","removal":"replace","type":"oem"},{"id":"com.transsion.filemanagerx","label":"File Manager","description":"Comes with 3 analytics/ads trackers.","web":["https://play.google.com/store/apps/details?id=com.transsion.filemanagerx","https://reports.exodus-privacy.eu.org/fr/reports/com.transsion.filemanagerx/latest/"],"removal":"replace","suggestions":"file_managers","type":"oem"},{"id":"com.transsion.fmradio","description":"WOW FM\nApp for FM RADIO.","removal":"replace","type":"oem"},{"id":"com.transsion.hamal","description":"It seems to be an \"aftersales user experience logging app\". Really shady app with questionable code (judgeWhiteUser() function. See https://github.com/0x192/universal-android-debloater/pull/112).\n\nStart at boot and can access phone number and IMEI (READ_PHONE_STATE).\n\nPithus analysis: https://beta.pithus.org/report/35fd79ebbe51808196605146a62aaef13bc654477d917078a3ae5d3f06ba8836","removal":"delete","type":"oem"},{"id":"com.transsion.health","description":"Tecno health app. Sends your personal data to Firebase google servers and Tecno servers. Those data can be shared with TRANSSION affiliated companies (see https://cdn.shalltry.com/transsionholdings/en/policy.html)\nPithus analysis: https://beta.pithus.org/report/2b7cd35081a9fbc82a1da1741cb476d1edaa3262d46a204ea8456c99c4e1b976","removal":"delete","type":"oem"},{"id":"com.transsion.healthlife","description":"My Health\nProvides you with interesting and professional analyses of running, steps, weight management etc.\nhttps://play.google.com/store/apps/details?id=com.transsion.healthlife","removal":"delete","type":"oem"},{"id":"com.transsion.hilauncher","description":"HiOS Launcher\nIt have google analytics and it's so bloated.\nThe recent apps button does not work after uninstallation.","removal":"unsafe","type":"oem"},{"id":"com.transsion.hiparty","description":"Hi Party\nAllows you to synchronize and play the same song across multiple *supported* devices. The app creates a wifi hotspot. You can connect up to 6 devices via QR code to simultaneously broadcast music.\n\nNeeds permissions you probably doesn't want to give : READ_PHONE_STATE (can read phone number and IMEI) and ACCESS_FINE_LOCATION.\nPithus analysis: https://beta.pithus.org/report/154ee6107d3f5bbb0819719fc7ce5fd17474135081f576f56c29bd26ed70ca14","removal":"delete","type":"oem"},{"id":"com.transsion.infinix.xclub","description":"Pre-installed social network app:\nhttps://www.infinix.club/","removal":"delete","type":"oem"},{"id":"com.transsion.iotservice","description":"WelinkService\nPC Connect unneccessary things","removal":"delete","type":"oem"},{"id":"com.transsion.itel.manual","description":"Manual Guide\nHas guides to phone.","removal":"replace","type":"oem"},{"id":"com.transsion.kolun.aiservice","description":"it's for testing things and debugging.","removal":"delete","type":"oem"},{"id":"com.transsion.kolun.assistant","description":"Smart Assistant App\nNearly no code in the APK I got. Weird\nPithus analysis: https://beta.pithus.org/report/7fbf0abbb2c28de4c976a388e04d206a88db9e6a42a740914c9e893589fd493b","removal":"delete","type":"oem"},{"id":"com.transsion.letswitch","description":"Mobile Cloner\nit's probably useful when you switch to other phone to move your apps.","removal":"delete","type":"oem"},{"id":"com.transsion.livewallpaper.volcano","description":"Tranquil Blue live wallpaper\nspecific live wallpaper","removal":"replace","type":"oem"},{"id":"com.transsion.livewallpaper.wakeup_mirror","description":"Specific Live Wallpaper.","removal":"replace","type":"oem"},{"id":"com.transsion.magazineservice.hios","description":"Shows trending news, games and wallpapers on the lockscreen. Talks to ads services\nPithus analysis: https://beta.pithus.org/report/fcda43fab1ed9cdc95281cdb96b77938afc8ca4b6e0ada418cac282a78f0cc9f","removal":"delete","type":"oem"},{"id":"com.transsion.magazineservice.xos","description":"Magazine Lockscreen XOS\nResource-hog bloatware that uses a lot of telemetry. For lock screen to look at pictures... https://play.google.com/store/apps/details?id=com.transsion.magazineservice.xos","removal":"delete","type":"oem"},{"id":"com.transsion.magicfont","label":"Font Manager","description":"Formerly, Magic Font.\nFonts installer with a lot of trackers obviously.\nFor Transsion devices I'm pretty sure this is the only way you can install fonts, even Zfont uses this app to install fonts.","web":["https://reports.exodus-privacy.eu.org/fr/reports/com.transsion.magicfont/latest/"],"removal":"caution","warning":"Breaks the functionality of changing fonts if removed.","type":"oem"},{"id":"com.transsion.magicshow","label":"Video Player","description":"(Bad) video Player with Ads and weak security (including an unsecured WebView implementation that can lead to XSS attacks.","web":["https://beta.pithus.org/report/33cd478cc18f3a2c0d5f7fd33c7350127ee2cff7acdf87f70641ca21dd2b2dcb"],"removal":"replace","suggestions":"video_players","type":"oem"},{"id":"com.transsion.manualguide","description":"Digital version of your phone manual.\nYou can view it from (Settings > System > Manual Guide)\nCan be safely removed if you don't need it.","removal":"delete","type":"oem"},{"id":"com.transsion.microintelligence","label":"Actions and Gestures","description":"Formerly, Micro Intelligence.\nProvides features like tap to awake, awake on device raise, etc.\nPhones home.","web":["https://beta.pithus.org/report/f7358ad68b27d9fa75a8e742ad43c64f2710b4ba5378ee825215ebbd08549275"],"removal":"caution","warning":"Disabling this app makes you unable to use specific settings like Gesture Navigation and Action & Gesture along with other phone features.","type":"oem"},{"id":"com.transsion.mol","description":"Ella Translate\nUnknown translator.","removal":"delete","type":"oem"},{"id":"com.transsion.multiwindow","description":"Multi Window\nNeeded for multi window.","removal":"replace","type":"oem"},{"id":"com.transsion.nephilim","description":"App provides custom quality settings to game: pubg, call of duty.\nNot very useful.","removal":"delete","type":"oem"},{"id":"com.transsion.netphilim","description":"Chinese useless frameworks.","removal":"delete","type":"oem"},{"id":"com.transsion.notebook","description":"Notepad app\nThese apps have telemetry on them and are completely replaceable.","removal":"delete","type":"oem"},{"id":"com.transsion.os.typeface","description":"FontManager\nNot sure if it's needed or not.","removal":"caution","type":"oem"},{"id":"com.transsion.ossettingsext","description":"OSSettings\nit's needed for some settings and probably it's important app.","removal":"caution","type":"oem"},{"id":"com.transsion.overlaysuw","description":"it's needed only on first-boot setup.","removal":"delete","type":"oem"},{"id":"com.transsion.overlaysuw.resoverlay","description":"Overlay to 'com.transsion.overlaysuw' safe to remove because it's for first-boot setup.","removal":"delete","type":"oem"},{"id":"com.transsion.phoenix","description":"Phoenix Browser\nhttps://play.google.com/store/apps/details?id=com.transsion.phoenix&hl=en","removal":"delete","type":"oem"},{"id":"com.transsion.phonemanager","description":"PhoneMaster Services\nAnother useless frameworks.","removal":"delete","type":"oem"},{"id":"com.transsion.phonemaster","description":"Phone Master.\nProvides features like ram cleaning, storage optimisation, data usage analyser, etc. Has embedded Facebook and Google ads trackers. Has 45 permissions and makes request data to many different companies servers. There even is the usesCleartextTraffic=true flag in the Manifest meaning trafic may not even be encrypted\n\nPithus analysis: https://beta.pithus.org/report/a5346fb5ea4fba5b73a891eae064b2bdecefbc7de4f9a13e3dcf94b0a81a20af","removal":"delete","type":"oem"},{"id":"com.transsion.plat.appupdate","description":"App Update\nUsed to update apps installed from the Palm Store. Uses insecure encryption algorithm.\nNotables permissions: ACCESS_FINE_LOCATION and GET_TASKS (allows to see which apps are running on the phone). Useless background memory hogs if you don't use apps from the Palm Store\n\nPithus analysis: https://beta.pithus.org/report/2584e9529e0988c1c2f9d657c5e2c55d1770e451d4120c176b5a505f2ee1033d","removal":"delete","type":"oem"},{"id":"com.transsion.powercenter","description":"power\nHas WhatsApp mode and ultra powersaving mode.","removal":"replace","type":"oem"},{"id":"com.transsion.quicktools","description":"Launcher Activity Test","removal":"delete","type":"oem"},{"id":"com.transsion.repaircard","description":"E-warranty Card\nFor Chinese users-only.","removal":"delete","type":"oem"},{"id":"com.transsion.scanningrecharger","description":"Smart Scanner\nApp contains telemetry and is completely replaceable.","removal":"delete","type":"oem"},{"id":"com.transsion.screencapture","description":"Needed for screenshots.","removal":"caution","type":"oem"},{"id":"com.transsion.sk","description":"Secure Keyboard\nUseless Secure Keyboard.","removal":"delete","type":"oem"},{"id":"com.transsion.smartmessage","description":"Bloated Messages app with a lot of features.","removal":"replace","type":"oem"},{"id":"com.transsion.smartpanel","description":"Smart Panel (Settings -> Smart Assistant)\nProvides \"easy\" access to your most used apps + features like gamemode and videoAssistant. Collects data and talks with the outside\n\nPithus analysis: https://beta.pithus.org/report/40d4b527fc650a9029e596d14aff7d640a6289e7aa50f471b142391b55eefe4a","removal":"delete","type":"oem"},{"id":"com.transsion.soundrecorder","description":"Sound Recorder\nContains telemetry and is completely replaceable.","removal":"delete","type":"oem"},{"id":"com.transsion.stasticalsales","description":"Sends after-sales telemetry data (including at least the phone number and the IMSI). You don't want this. This app can be launched from this secret dialer code: 862016\n\nPithus analysis: https://beta.pithus.org/report/35fa58c779ac80bcf44875e279cc4a6ba08678b0004e9c8f0816426cf0c584ab","removal":"delete","type":"oem"},{"id":"com.transsion.statisticalsales","description":"Sales Statistics","removal":"delete","type":"oem"},{"id":"com.transsion.systemupdate","description":"System Update\nProvides System Updates","removal":"caution","type":"oem"},{"id":"com.transsion.tabe","description":"APPIOT\nUnused frameworks to permissions app.","removal":"delete","type":"oem"},{"id":"com.transsion.tecnospot","description":"TECNO SPOT (https://play.google.com/store/apps/details?id=com.transsion.tecnospot)\nTecno official app to access the Tecno forum. Useless and full of trackers (https://reports.exodus-privacy.eu.org/fr/reports/com.transsion.tecnospot/latest/)","removal":"delete","type":"oem"},{"id":"com.transsion.teop","description":"it's app for testing and logs","removal":"delete","type":"oem"},{"id":"com.transsion.theme.icon","description":"Has basic apps icons, not sure if needed.","removal":"replace","type":"oem"},{"id":"com.transsion.thunderback","description":"Lightning Multi-Window\nMulti-Window feature for apps.","removal":"caution","type":"oem"},{"id":"com.transsion.tips","description":"Tips\nApp for tips.","removal":"delete","type":"oem"},{"id":"com.transsion.tower","description":"ControlTower test\nTesting things app.","removal":"delete","type":"oem"},{"id":"com.transsion.trancare","description":"Telemetry. Makes requests (with weak crypto) to the Shalltry CDN (https://mi-test.shalltry.com). Collects IMEI, all the apps installed, localisation...\nPithus analysis: https://beta.pithus.org/report/9be13b57bde5620d2ff1824782a2ccc1d6517d437543549c720bc70b6dd02aee","removal":"delete","type":"oem"},{"id":"com.transsion.tranengine","description":"Unused frameworks.","removal":"delete","type":"oem"},{"id":"com.transsion.uxdetector","description":"AI things to trancare","removal":"delete","type":"oem"},{"id":"com.transsion.videocallenhancer","description":"Applies beauty effect in WhatsApp video calls. Lots of permissions. Talks to Google ads service.\nPithus analysis: https://beta.pithus.org/report/47bebb911e9b5b9202030ce599805ebe3e47eb45054264f49cf85971e232bbce","removal":"delete","type":"oem"},{"id":"com.transsion.vishaplayerhd","description":"Visha Player\nNeeded to run videos.","removal":"replace","type":"oem"},{"id":"com.transsion.wezone","description":"Related to 'Gaming Mode' and 'Palm Store'","removal":"delete","type":"oem"},{"id":"com.transsion.wifiplaytogether","description":"Play music together over Wi-Fi","removal":"delete","type":"oem"},{"id":"com.transsion.zahooc","description":"Za-Hooc\nit's for secure card and Theft Alert.","removal":"delete","type":"oem"},{"id":"com.transsnet.moreplus","description":"More+ is a social app with massive videos, images, opinions and tribes, where you can post your personal blogs, share funny moments, chat and make friends.","removal":"delete","type":"oem"},{"id":"com.transsnet.store","description":"Palm Store. App store with unsecure apps and probably malware. Has ads trackers and lot of intrusives permissions. Shows intrusive ads and popups.\nPithus analysis: https://beta.pithus.org/report/35d762b27c9e16703adf1731b74bef2c53a753b6a7475c425bced53b553758e5","removal":"delete","type":"oem"},{"id":"com.trassion.infinix.xclub","description":"XClub\nXStore to buy.","removal":"delete","type":"oem"},{"id":"com.ts.setupwizard.overlay.overlay","description":"","removal":"caution","type":"oem"},{"id":"com.uievolution.gguide.android","description":"This hidden chinese app means nothing","removal":"delete","type":"oem"},{"id":"com.unionpay.tsmservice","description":"UnionPay\nOnly for China.","removal":"delete","type":"oem"},{"id":"com.unionpay.tsmservice.mi","description":"UnionPay\nOnly for China.","removal":"delete","type":"oem"},{"id":"com.unisoc.launcher.customization","description":"Unisoc Home Screen\nPartner Customization launcher.","removal":"delete","type":"oem"},{"id":"com.unisoc.localeupdate","description":"Local System Update\nNeeded for system updates.","removal":"caution","type":"oem"},{"id":"com.unisoc.phone","label":"LockAssistant","description":"Runs before the user unlocks the device (direct-boot aware). Reads IMEI, SMS, call log, uses gps/wifi. Unisoc is a CPU manufacturer. Related to package `com.android.unisoc.telephony.server`.","removal":"caution","type":"oem"},{"id":"com.vendor.frameworkresoverlay","description":"","removal":"caution","type":"oem"},{"id":"com.vewd.core.integration.dia","description":"Sony's browser app. Some apps redirect to the browser so it can be useful. You can use another browser as long as you have another one installed.","removal":"delete","type":"oem"},{"id":"com.visionobjects.resourcemanager","description":"Handwriting DB Updater (discontinued?)\nLanguage Updater from site http://samsungresources.visionobjects.com/\nBut it's tracker. I guess it's to Handwriting Service but not needed.","removal":"delete","type":"oem"},{"id":"com.vivo.SmartKey","description":"Quick action\nChinese smart things.","removal":"delete","type":"oem"},{"id":"com.vivo.abe","description":"Audio EQ(equalizer)\nSome 3rd-party music apps can use it to provide you EQ features.","removal":"delete","type":"oem"},{"id":"com.vivo.alphacamera","description":"AlphaCamera\nEngineering Mode, Log, Debugging service camera.","removal":"delete","type":"oem"},{"id":"com.vivo.android.connectivity.common.resources.overlay","description":"Better keep for connectivity.","removal":"unsafe","type":"oem"},{"id":"com.vivo.android.connectivity.mainline.common.resources.overlay","description":"Better keep for connectivity.","removal":"unsafe","type":"oem"},{"id":"com.vivo.android.connectivity.mainline.manufacturer.resources.overlay","description":"Better keep for connectivity.","removal":"unsafe","type":"oem"},{"id":"com.vivo.android.connectivity.manufacturer.resources.overlay","description":"Better keep for connectivity.","removal":"unsafe","type":"oem"},{"id":"com.vivo.android.wifi.common.resources.overlay","description":"Better keep for connectivity.","removal":"unsafe","type":"oem"},{"id":"com.vivo.android.wifi.mainline.common.resources.overlay","description":"Better keep for connectivity.","removal":"unsafe","type":"oem"},{"id":"com.vivo.android.wifi.mainline.manufacturer.resources.overlay","description":"Better keep for connectivity.","removal":"unsafe","type":"oem"},{"id":"com.vivo.android.wifi.manufacturer.resources.overlay","description":"Better keep for connectivity.","removal":"unsafe","type":"oem"},{"id":"com.vivo.appfilter","description":"AppFilter\nHas something to powersaving like prevent autostart apps.","removal":"caution","type":"oem"},{"id":"com.vivo.appstore","description":"V-Appstore\nvivo app store.","removal":"delete","type":"oem"},{"id":"com.vivo.assistant","description":"Vivo assistant\nAssistant for vivo phones.","removal":"delete","type":"oem"},{"id":"com.vivo.assistantfunction","description":"Steps\nJovi Home kit. Record walk sport app.","removal":"delete","type":"oem"},{"id":"com.vivo.audiofx","description":"Audio effect\nNeeded for sound effects.","removal":"delete","type":"oem"},{"id":"com.vivo.browser","description":"Vivo browser full of trackers (https://play.google.com/store/apps/details?id=com.vivo.browser)\n","removal":"delete","type":"oem"},{"id":"com.vivo.bsptest","description":"BSPTest\nHidden app for testing things.","removal":"delete","type":"oem"},{"id":"com.vivo.calculator","description":"Calculator\nVivo calculator app","removal":"replace","type":"oem"},{"id":"com.vivo.camera.action","description":"Maybe it's needed for the camera app.","removal":"caution","type":"oem"},{"id":"com.vivo.card","description":"Smart sidebar","removal":"replace","type":"oem"},{"id":"com.vivo.compass","description":"Compass\nVivo compass app","removal":"delete","type":"oem"},{"id":"com.vivo.contentcatcher","description":"Unused frameworks.","removal":"delete","type":"oem"},{"id":"com.vivo.cota","description":"COTA\nUseless logs and ads. It's for recommendations.","removal":"delete","type":"oem"},{"id":"com.vivo.crontab","description":"Task timer\nIt's for scheduling tasks.","removal":"delete","type":"oem"},{"id":"com.vivo.daemonService","description":"vivoService\nSecurity thing.","removal":"delete","type":"oem"},{"id":"com.vivo.demovideo","description":"Video Demo\nDemo Video, Demo Switch, and have verify activity","removal":"delete","type":"oem"},{"id":"com.vivo.desktopstickers","description":"Stickers\nCustomize widgets with favorite images.","removal":"delete","type":"oem"},{"id":"com.vivo.devicereg","description":"Hidden debugging app ims.","removal":"delete","type":"oem"},{"id":"com.vivo.doubleinstance","description":"App Clone\nIt's for clone app.","removal":"delete","type":"oem"},{"id":"com.vivo.doubletimezoneclock","description":"i Widget\nDouble Timezone Clock Widget","removal":"delete","type":"oem"},{"id":"com.vivo.dream.clock","description":"Clock\nScreensaver clock","removal":"delete","type":"oem"},{"id":"com.vivo.dream.music","description":"i Music\nScreensaver music","removal":"delete","type":"oem"},{"id":"com.vivo.dream.weather","label":"Weather","description":"Screensaver weather.","removal":"replace","suggestions":"weather_apps","type":"oem"},{"id":"com.vivo.easyshare","description":"Easy share\nSharing apps to another phone.\nhttps://play.google.com/store/apps/details?id=com.vivo.easyshare","removal":"delete","type":"oem"},{"id":"com.vivo.email","description":"Email\nclosed-source Email app","removal":"replace","type":"oem"},{"id":"com.vivo.engineercamera","description":"EngineerCamera\nTesting camera things.","removal":"delete","type":"oem"},{"id":"com.vivo.epm","description":"EPM\nUseless crash app notify.","removal":"delete","type":"oem"},{"id":"com.vivo.ewarranty","description":"E-warranty card\nOnly for China.","removal":"delete","type":"oem"},{"id":"com.vivo.faceui","description":"FaceUI\nIt's used for face unlock.","removal":"caution","type":"oem"},{"id":"com.vivo.faceunlock","description":"Facial recognition\nIt's used for face unlock.","removal":"caution","type":"oem"},{"id":"com.vivo.favorite","description":"Favorites\nIt's used to add text from the clipboard to favorites. Probably requires cloud. Contains untranslated Chinese things.","removal":"delete","type":"oem"},{"id":"com.vivo.feedback","description":"Feedback\nLets you rate your device and share feedback.","removal":"delete","type":"oem"},{"id":"com.vivo.findphone","description":"Find\nIt's used for finding the phone. Log out from account before removing.","removal":"delete","type":"oem"},{"id":"com.vivo.fingerprint","description":"Fingerprints and passwords\nNeeded for fingerprint settings.","removal":"caution","type":"oem"},{"id":"com.vivo.fingerprintengineer","description":"Fingerprint test\nHidden Fingerprint testing not available for normal users.","removal":"delete","type":"oem"},{"id":"com.vivo.fingerprintui","description":"FingerprintUI\nFingerprint test? Contains payment functionality for China. May be needed for fingerprint lockscreen.","removal":"caution","type":"oem"},{"id":"com.vivo.fingerprintvit","description":"Fingerprint testing.","removal":"delete","type":"oem"},{"id":"com.vivo.floatingball","description":"Floating ball\nContains a lot of Chinese code.","removal":"delete","type":"oem"},{"id":"com.vivo.fuelsummary","description":"FuelSummary\nBattery testing.","removal":"delete","type":"oem"},{"id":"com.vivo.gallery","description":"Gallery\nVivo gallery app with Chinese trackers.","removal":"replace","type":"oem"},{"id":"com.vivo.game","description":"Game Space. Not sure if its useful for gaming.","removal":"caution","type":"oem"},{"id":"com.vivo.gamecube","description":"Ultra Game Mode\nUsed for game optimization. Questionable if anyone is using this.","removal":"caution","type":"oem"},{"id":"com.vivo.gamewatch","description":"GameWatch\nContains a lot of Chinese code and it's hidden?","removal":"delete","type":"oem"},{"id":"com.vivo.globalanimation","description":"Global dynamic effects\nThis is for effects and animations.","removal":"delete","type":"oem"},{"id":"com.vivo.globalanimation.resources","description":"GlobalanmationResources\nHas animation booting probably, safe to remove.","removal":"delete","type":"oem"},{"id":"com.vivo.globalsearch","description":"Global Search\nit's chinese searching.","removal":"delete","type":"oem"},{"id":"com.vivo.healthwidget","description":"Health kit\nit's only widgets.","removal":"delete","type":"oem"},{"id":"com.vivo.heduohao","description":"Used for Chinese SIM.","removal":"delete","type":"oem"},{"id":"com.vivo.hiboard","description":"Smart Launcher\nKeyboard app with 62 permissions.","removal":"replace","type":"oem"},{"id":"com.vivo.hybrid","description":"Quick App\nProvides Quick App support. Quick Apps are Javascript+CSS apps that don't need any installation. This technology has its uses but I'm personally not a huge fan on having to rely on a JS engine to run an application\nThis system app has a lot of permissions (including SEND_SMS, CAMERA, READ_EXTERNAL_STORAGE, RECORD_AUDIO... why?)\nMore information: https://www.xda-developers.com/huawei-quick-apps-alternative-google-instant-apps/\n OW2 Quick App whitepaper: https://quick-app-initiative.ow2.io/docs/Quick_App_White_Paper.pdf","removal":"replace","type":"oem"},{"id":"com.vivo.imanager","description":"iManager\nA vivo app consists of Phone Management includes Space Cleanup, Security Scan, Data Traffic Management, Apps & Notifications & Utility Tools includes App encryption, Battery Management, App Clone, Network Management etc.","removal":"caution","type":"oem"},{"id":"com.vivo.iotserver","description":"Related to Jovi Chinese.","removal":"delete","type":"oem"},{"id":"com.vivo.livewallpaper.behavior","description":"Live Wallpaper Behavior.","removal":"replace","type":"oem"},{"id":"com.vivo.livewallpaper.behavior.resources","description":"WallpaperResources\nHas live wallpapers.","removal":"replace","type":"oem"},{"id":"com.vivo.livewallpaper.coffeetime","description":"Specific Live wallpaper","removal":"delete","type":"oem"},{"id":"com.vivo.livewallpaper.coralsea","description":"Specific Live wallpaper","removal":"delete","type":"oem"},{"id":"com.vivo.livewallpaper.floatingcloud","description":"com.vivo.livewallpaper.coffeetime\nSpecific Live wallpaper","removal":"delete","type":"oem"},{"id":"com.vivo.livewallpaper.ocean","description":"Live Wallpaper Ocean.","removal":"replace","type":"oem"},{"id":"com.vivo.livewallpaper.oceanvertical","description":"Live Wallpaper Oceanvertical.","removal":"replace","type":"oem"},{"id":"com.vivo.livewallpaper.plant","description":"Plant live wallpaper\nSpecific live wallpaper.","removal":"delete","type":"oem"},{"id":"com.vivo.livewallpaper.silk","description":"com.vivo.livewallpaper.coffeetime\nSpecific Live wallpaper","removal":"delete","type":"oem"},{"id":"com.vivo.magazine","description":"Lockscreen Poster Service\nUseless things to lockscreen also it has trackers","removal":"delete","type":"oem"},{"id":"com.vivo.minscreen","description":"Small screen mode\nThis feature is available on settings.","removal":"delete","type":"oem"},{"id":"com.vivo.monochromeicons","description":"MonochromeIcons\nIn code found something to vivo icons.\nIn Android 14.","removal":"delete","type":"oem"},{"id":"com.vivo.motionrecognition","description":"Smart motion\nGestures and smart things.","removal":"delete","type":"oem"},{"id":"com.vivo.multinlp","description":"LocationServices\nChinese and Google location. Not sure if it's useless or not.","removal":"caution","type":"oem"},{"id":"com.vivo.musicwidgetmix","description":"Origin Player\nMusic Widget Mix","removal":"delete","type":"oem"},{"id":"com.vivo.networkimprove","description":"NetworkImprove\nMay be only for China because of non-English words. Also has logs.","removal":"delete","type":"oem"},{"id":"com.vivo.networkstate","description":"NetworkState\nVirtual sim things. Only for China.","removal":"delete","type":"oem"},{"id":"com.vivo.nightpearl","description":"Always on Display\nAlways on display, it's available in settings.","removal":"delete","type":"oem"},{"id":"com.vivo.notes","description":"Notes\nVivo notes","removal":"replace","type":"oem"},{"id":"com.vivo.nps","description":"iQOO, vivo experience assessment.\nThis app has nothing necessary in the code.","removal":"delete","type":"oem"},{"id":"com.vivo.numbermark","description":"Number mark\nContains a lot of Chinese code.","removal":"delete","type":"oem"},{"id":"com.vivo.omacp","description":"Configuration SMS","removal":"caution","type":"oem"},{"id":"com.vivo.pem","description":"Power guardian\nFor power-saving, probably. Also has something to google.","removal":"caution","type":"oem"},{"id":"com.vivo.phoneinstructions","description":"Phone Instructions\nit's used to app clone for phone instructions.","removal":"delete","type":"oem"},{"id":"com.vivo.puresearch","description":"Search Widget\nDesktop search bar","removal":"delete","type":"oem"},{"id":"com.vivo.pushservice","description":"Push Notification Engine\nNot needed for notifications. Requires Google Play Services, and it's for VPUSH. Contains trackers and permissions.","removal":"delete","type":"oem"},{"id":"com.vivo.quickpay","description":"Quick Pay\nChinese pay.","removal":"delete","type":"oem"},{"id":"com.vivo.safecenter","description":"Security Center\nUseless security.","removal":"delete","type":"oem"},{"id":"com.vivo.sdkplugin","description":"Needed for Vivo account and Vivo payment. Everything is in Chinese, but not sure how much will not work after removal.","removal":"caution","type":"oem"},{"id":"com.vivo.secime.service","description":"SecIME\nTalkback Chinese.","removal":"delete","type":"oem"},{"id":"com.vivo.setupwizard","description":"It's only for first-boot setup.","removal":"delete","type":"oem"},{"id":"com.vivo.share","description":"Vivo share\nTransfer data between vivo device & PC","removal":"replace","type":"oem"},{"id":"com.vivo.simplelauncher","description":"Simple launcher, Simple View.","removal":"delete","type":"oem"},{"id":"com.vivo.singularity","description":"vivo system webview\nMultidexapplication?","removal":"caution","type":"oem"},{"id":"com.vivo.smartLife","description":"Jovi InLife service\nhome devices and smart interconnectivity.","removal":"delete","type":"oem"},{"id":"com.vivo.smartanswer","description":"smart answer\nA lot of bloated smart answer.","removal":"delete","type":"oem"},{"id":"com.vivo.smartmultiwindow","description":"Smart Split\nSmart multiwindow.","removal":"caution","type":"oem"},{"id":"com.vivo.smartshot","description":"S-capture\nUsed for screenshots?","removal":"caution","type":"oem"},{"id":"com.vivo.smartunlock","description":"smart unlock device\nit's used for unlock device using bluetooth devices probably.","removal":"delete","type":"oem"},{"id":"com.vivo.soundrecorder","description":"Recorder\nVivo sound recorder","removal":"replace","type":"oem"},{"id":"com.vivo.space","description":"Open Vivo official website.\nUseless app","removal":"delete","type":"oem"},{"id":"com.vivo.sps","description":"SuperProcessSystem\nUseless app statistics, logs, crash handler, security, error memory management. Also have something to com.vivo.abe but some phones don't have com.vivo.sps installed. And where is the optimization code?","removal":"delete","type":"oem"},{"id":"com.vivo.stepcount","description":"Step Count Application\nit's hard to say what app actually doing\nlooks like a useless frameworks to widgets and swipe menu","removal":"delete","type":"oem"},{"id":"com.vivo.systemblur.server","description":"System blur render engine\nSystem blur render engine, logs","removal":"delete","type":"oem"},{"id":"com.vivo.timerwidget","description":"Alarm widget\nWidget clock in launcher, home screen.","removal":"delete","type":"oem"},{"id":"com.vivo.unionpay","description":"Payment v-Coin\nit's for china probably.","removal":"delete","type":"oem"},{"id":"com.vivo.upnpserver","description":"Phone Mirroring\nSmart things and needed for broadcast?","removal":"delete","type":"oem"},{"id":"com.vivo.upslide","description":"System navigation\nNeeded for secure and smart things.","removal":"delete","type":"oem"},{"id":"com.vivo.vhome","description":"Smart Remote\nit's for smart things.","removal":"delete","type":"oem"},{"id":"com.vivo.vhomeguide","description":"VHome\nNot needed vhome guide suggestions.","removal":"delete","type":"oem"},{"id":"com.vivo.vibrator4d","description":"vibrator4d\nUsed for games? Contains a lot of feedback things.","removal":"delete","type":"oem"},{"id":"com.vivo.video.floating","description":"Face beauty for video call\nit's a features for video call vivo.","removal":"delete","type":"oem"},{"id":"com.vivo.videoeditor","description":"Video Trim\nVideo editor","removal":"delete","type":"oem"},{"id":"com.vivo.virtuallight","description":"Ambient light effect\nIt's music light effect (Settings: Dynamic Effects: Ambient Light Effect)","removal":"delete","type":"oem"},{"id":"com.vivo.vivo3rdalgoservice","description":"ImageAlgoService\nHDR and High contrast in the camera?\nProbably safe to remove, so it's debugging app?","removal":"replace","type":"oem"},{"id":"com.vivo.vivokaraoke","description":"Mobile KTV\nVivo karaoke.","removal":"delete","type":"oem"},{"id":"com.vivo.vms","description":"Unused frameworks. Chinese.","removal":"delete","type":"oem"},{"id":"com.vivo.voicewakeup","description":"Voice wake-up. Has a lot of permissions (REQUEST_INSTALL_PACKAGES, READ_EXTERNAL_STORAGE, RECORD_AUDIO...). Kind of a \"smart assistant\" ? It is constantly listening waiting for a trigger word [MORE INFO NEEDED]","removal":"delete","type":"oem"},{"id":"com.vivo.vtouch","description":"Smart Vision\nSpying a lot. Smart things.","removal":"delete","type":"oem"},{"id":"com.vivo.wallet","description":"Vivo wallet\nEverything is in Chinese.","removal":"delete","type":"oem"},{"id":"com.vivo.weather","description":"Weather\nVivo weather app, have Chinese trackers.","removal":"delete","type":"oem"},{"id":"com.vivo.weather.provider","description":"Weather storage\nVivo weather app, has Chinese trackers.","removal":"delete","type":"oem"},{"id":"com.vivo.widget.cleanspeed","description":"Clean up acceleration components\nClean speed widget","removal":"delete","type":"oem"},{"id":"com.vivo.widget.gallery","description":"Album Highlights\nGallery widget","removal":"delete","type":"oem"},{"id":"com.vivo.widgetweather","description":"Weather components\nWeather widget","removal":"delete","type":"oem"},{"id":"com.vivo.wifiengineermode","description":"Needed for WiFi info? it's engineermode so probably not.","removal":"delete","type":"oem"},{"id":"com.vivotouchscreen.synadeltadiff","description":"tests touch screen","removal":"delete","type":"oem"},{"id":"com.vlife.vivo.wallpaper","description":"live wallpaper\nNeeded for live wallpapers.","removal":"replace","type":"oem"},{"id":"com.vlingo.midas","description":"Speech recognition app for the Vlingo personal assistant\nVlingo : https://en.wikipedia.org/wiki/Vlingo\nFYI : In January 2012 AndroidPit discovered that Vlingo sent packets of information containing the users GPS co-ordinates,\nIMEI (unique device identifier), contact list and the title of every song stored on the device back to Nuance without.\nSource : https://www.androidpit.com/Vlingo-Privacy-Breach\n","removal":"delete","type":"oem"},{"id":"com.vmall.client","description":"Vmall is online platform of Huawei. It's online shopping app.","removal":"delete","type":"oem"},{"id":"com.volte.config","description":"VoLTEconfig\nVoLTE config. Probably hidden and not needed. Looks like a Chinese.","removal":"delete","type":"oem"},{"id":"com.vos.as.vit","description":"App for testing things.","removal":"replace","type":"oem"},{"id":"com.vos.user.vit","description":"App for testing things.","removal":"delete","type":"oem"},{"id":"com.wapi.wapicertmanage","description":"WAPI certificate manager\nWAPI = WLAN Authentication and Privacy Infrastructure.\nA Chinese national standard for Wireless LAN within a limited area such as a home. Not very useful if you don't live in China.\nhttps://en.wikipedia.org/wiki/WLAN_Authentication_and_Privacy_Infrastructure\nDigital certificates identify devices and apps for security. Just like your driver’s license shows that you can legally drive, a digital certificate identifies your device and confirms that it should be able to access something.\nhttps://security.stackexchange.com/questions/102550/what-are-wifi-certificates-used-for-what-are-they","removal":"delete","type":"oem"},{"id":"com.wapi.wapicertmanager","description":"WAPI Certificates Manager\nWAPI = WLAN Authentication Privacy Infrastructure (https://en.wikipedia.org/wiki/WLAN_Authentication_and_Privacy_Infrastructure\nIt was designed to replace WEP and become the new Standard but it was't rejected by the ISO (International Organization for Standardization)\nIt is currently only used in China\nThis app most likely manage certificates (they are used to make sure you're not connecting to a rogue Access Point)\nNote: If you live in China, you most likely want to keep it.\n","removal":"delete","type":"oem"},{"id":"com.wdstechnology.android.kryten","description":"OTA Access Point Configuration. It's only hidden configurations to OTA, it's not needed for updates.\nIn the case of OTA updates, \"com.miui.core\" is responsible.","removal":"delete","type":"oem"},{"id":"com.westalgo.factorycamera","description":"Hidden camera tests, calibration.","removal":"delete","type":"oem"},{"id":"com.wifi.rxsenstest","description":"Wifi Rx Sens Test.","removal":"delete","type":"oem"},{"id":"com.wiko.packageinstaller","description":"Provides first time app install for Wiko mobile","removal":"delete","type":"oem"},{"id":"com.wiko.services","label":"Wiko Support","description":"Support app for Wiko mobile","removal":"delete","type":"oem"},{"id":"com.wingtech.catchlog","description":"Cit App to show Battery info, Secret Code:6485","removal":"delete","type":"oem"},{"id":"com.wingtech.stability","description":"Stability Test. Tests hardware things.","removal":"delete","type":"oem"},{"id":"com.wingtech.standard","description":"WTStandardTest\nWingtech is a chinese Original Design Manufacturer (ODM) involved in the manufacturing of Xiaomi devices.\nThere is very high chances this app is only a hardware conformance test app used during production process\nyou don't need as an end-user.\nCan someone share the apk just to be 100% sure?\n","removal":"delete","type":"oem"},{"id":"com.ws.dm","description":"it's the same like com.wssyncmldm\nSoftware update\nFetch System OTA updates\nWorks along with com.sec.android.soagent\nRequired on Samsung Smartphones for OTA.","removal":"caution","type":"oem"},{"id":"com.wsomacp","description":"omacp = OMA Client Provisioning. It is a protocol specified by the Open Mobile Alliance (OMA).\nConfiguration messages parser. Used for provisioning APN settings to Samsung devices via SMS \nIn my case, it was automatic and I never needed configuration messages.\nMaybe it's useful if carriers change their APN. But you still can change the config manually, it's not difficult.\nKeep in mind these special types of SMS can be abused (though you would need to select to apply the APN in the popup).","web":["https://research.checkpoint.com/2019/advanced-sms-phishing-attacks-against-modern-android-based-smartphones/","https://www.zdnet.com/article/samsung-huawei-lg-and-sony-phones-vulnerable-to-rogue-provisioning-messages/"],"removal":"delete","type":"oem"},{"id":"com.wssnps","description":"Samsung Backup and restore Manager (on Samsung Galaxy S7)\nWas replaced by \"com.sec.android.easyMover\" (Samsung Smart Switch Mover)\n","removal":"delete","type":"oem"},{"id":"com.wssyncmldm","description":"Software update\nFetch System OTA updates\nWorks along with com.sec.android.soagent\nRequired on Samsung Smartphones for OTA.","removal":"caution","type":"oem"},{"id":"com.wt.secret_code_manager","description":"Hidden app which associates an action (display logging info) to a secret code.\nThis secret codes have to be dialed from the Xiaomi dialer.\n","removal":"delete","type":"oem"},{"id":"com.wt.version_query","description":"theres something about updates but this apk size is too small\nand includes some secret codes, founded in xiaomi.","removal":"delete","type":"oem"},{"id":"com.wtk.factory","label":"FactoryMode","description":"Hardware testing things.","web":["https://nvd.nist.gov/vuln/detail/CVE-2018-14999"],"removal":"delete","warning":"On Leagoo P1 devices, triggering a certain broadcast receiver may factory reset the device.","type":"oem"},{"id":"com.wtk.stresstest","label":"StressTest","description":"Testing Hardware things.","removal":"delete","type":"oem"},{"id":"com.xiaomi.NetworkBoost","description":"Network Boost\nNetwork acceleration not available in settings wifi?\nPeople said it's placebo and it doesn't speed up the network.\nIt has a lot of Chinese code and has permission to MIUI analytics.","removal":"delete","type":"oem"},{"id":"com.xiaomi.ab","label":"Mi Store System Components","description":"Formerly, mab.\nSomething about login, paying in Mi Store China.","removal":"delete","type":"oem"},{"id":"com.xiaomi.account","description":"Mi Account\nHas a LOT of permissions + Facebook trackers. Collects many information, including your phone number, your unique International mobile subscriber identity (IMSI) and your clipboard).\nYou should remove this if you don't have or don't want a Mi account.\nWARNING: Make sure to log out of your Mi Account and unbind your phone from it. If you don't you could be locked out from your phone after removing this package.\nRemove Mi Account: https://xiaomiui.net/how-to-remove-mi-account-7606/\n\nPithus analysis: https://beta.pithus.org/report/3f5abc9d7215dd0be5c3ac137b0cd528217640b5778e9f849a9beb0a34eda8dc","removal":"replace","type":"oem"},{"id":"com.xiaomi.aiasst.service","label":"AI Call Assistant","description":"Useless Call settings.","removal":"delete","type":"oem"},{"id":"com.xiaomi.aiasst.vision","label":"AiasstVision","description":"Not needed if you removed AI Call Assistant.","removal":"delete","type":"oem"},{"id":"com.xiaomi.aicr","label":"Mi AI Engine","description":"Another app to Mi AI from MIUI China.","web":["https://hyperosupdates.com/apps/com.xiaomi.aicr/"],"removal":"delete","type":"oem"},{"id":"com.xiaomi.aireco","description":"XiaoaiRecommendation\nThis app does nothing, totally random frameworks unused.","removal":"delete","type":"oem"},{"id":"com.xiaomi.android.tvsetup.partnercustomizer","description":"SetupCustomizer\nOn first boot setup installs bloatware.","removal":"delete","type":"oem"},{"id":"com.xiaomi.aon","description":"'I always on' found in code and a lot statistics. Also Miui analytics permissions.\nRegion of the tracking device, found things Mi Face, spy on your face and everything you do on your phone?\nAONEventTracking uses and sends to 'com.miui.analytics'.","removal":"delete","type":"oem"},{"id":"com.xiaomi.barrage","description":"Bullet screen notifications\nPop-up notifications (feature inside the game service)\nHave a lot of Chinese things in this code but it's for the game service, NOT gamespace.\nSo it's for China only.","removal":"delete","type":"oem"},{"id":"com.xiaomi.bluetooth","description":"MIUI Bluetooth Extension. Doesn't seem to affect bluetooth functionality. Tested on Poco M4 Pro 5G HyperOS 1.0.1.0. Note: If you deleted (com.xiaomi.xmsf) this package will likely send \"Bluetooth extension stopped working\" errors, uninstalling it removes them.","removal":"caution","type":"oem"},{"id":"com.xiaomi.bluetooth.overlay","description":"It has only unused png files, webp.\nImages in it: wireless headphones from all angles.","removal":"caution","type":"oem"},{"id":"com.xiaomi.bsp.gps.nps","description":"GPS location\nI think bsp = board system package (https://en.wikipedia.org/wiki/Board_support_package)\nNot sure about nps (It might be Non-Permanent GPS station)\nIt's a small package which seems to display a notification when an app is using GPS.\nMore precisely, there is a receiver (GnssEventReceiver) which listen to com.xiaomi.bsp.gps.nps.GetEvent \nThis event most likely happen when an app use the GPS and refers to the state of the communication with the GNSS:\nFIX, LOSE, RECOVER, START, STOP\nIt's safe to remove if you really want to.\n","removal":"replace","type":"oem"},{"id":"com.xiaomi.bttester","description":"BTCIT\nBluetooth Test Service","removal":"delete","type":"oem"},{"id":"com.xiaomi.calendar","description":"Mi Calendar. Google trackers inside and needs 48 permissions! Obviously talks to Xiaomi servers. The com.mi.health.provider.permission.read_menstruation permissions is really creepy... There are better alternatives.\nPithus analysis: https://beta.pithus.org/report/6c68ddd1f9e2d1f9e1df2eab572c07f1e34c4a6490c0ba98554a7356ca2a351d\n\nNote: Since MIUI 12, you can no longer uninstall this app. Disabling it still works fine.","removal":"delete","type":"oem"},{"id":"com.xiaomi.cameratools","description":"CameraTools\nCamera calibration. Deleting it does not affect the camera.","removal":"delete","type":"oem"},{"id":"com.xiaomi.channel","description":"Mi Talk \nMi instant messaging app that lets you do practically the same thing as Whatsapp. \nNOTE: You should use Signal or Wire instead Whatsapp/Mi Talk for more privacy.\n","removal":"delete","type":"oem"},{"id":"com.xiaomi.digitalkey","label":"digitalkey","description":"Smart door locks, can also be used as a car key. Only for China.","removal":"delete","type":"oem"},{"id":"com.xiaomi.discover","description":"System Apps Updater\nWARNING: Disable System app updates (but not firmware updates)\n","removal":"caution","type":"oem"},{"id":"com.xiaomi.entitlement.o2","description":"Unknown, seems to be United Kingdom, Germany specific to the O2 carrier.\nhttps://en.wikipedia.org/wiki/O2_(brand)\nIf you don't use O2 carrier, it's safe to remove.","removal":"delete","type":"oem"},{"id":"com.xiaomi.finddevice","label":"Mi Find Device","description":"Find My Device feature (in the Settings)\nEnables you to locate your lost phone and erase your data remotely.\nYour phone needs to be connected to internet (Wifi/mobile data) for this feature to work.","web":["https://github.com/0x192/universal-android-debloater/issues/641"],"removal":"unsafe","warning":"Depending on MIUI version, removing the app may cause bootloops.","type":"oem"},{"id":"com.xiaomi.gamecenter","description":"Games\nAnother app with a lot tracking and not needed for gamespace.","removal":"delete","type":"oem"},{"id":"com.xiaomi.gamecenter.sdk.service","label":"Game Service","description":"It's not needed for gamespace, disabled by default. It has activities such as AliPay, login to account. I'm not sure what it's needed for.","removal":"delete","type":"oem"},{"id":"com.xiaomi.glgm","description":"Xiaomi Games\nNot sure if this app still exists.\n","removal":"delete","type":"oem"},{"id":"com.xiaomi.joyose","label":"Joyose","description":"GPU Tuner\nOptimizes your game for gaming. Some people have noticed that it locks the fps at 60 after selecting 90.","web":["https://youtu.be/gavEuH3Ck5o?t=550"],"removal":"caution","type":"oem"},{"id":"com.xiaomi.jr","description":"Help you getting loans when shopping.\n","removal":"delete","type":"oem"},{"id":"com.xiaomi.lens","description":"Related to camera app ?\nSafe to remove (according to a lot of users)\nI'd like to have more info about it. Can a Xiaomi user help ? \n","removal":"delete","type":"oem"},{"id":"com.xiaomi.location.fused","description":"It has china location, ads & analytics.\nYou dont need it for location.","removal":"delete","type":"oem"},{"id":"com.xiaomi.macro","description":"MiMacro is an automation task from Xiaomi like touch on MIUI Game Turbo.\nHas INTERNET and READ_PHONE_STATE permission allowing access to the phone number, serial number, whether a call is active, the number that a call is connected to...\nWhat is sure (from the code) is that the app collects the IMEI.\n\nPithus analysis: https://beta.pithus.org/report/2b056ed84fe500552a58184035b962ba68af29457c24930c0aa8c9eba4af7bcf","removal":"delete","type":"oem"},{"id":"com.xiaomi.market","label":"GetApps","description":"China Mi App Store.\nI used it only to install google play.","removal":"delete","type":"oem"},{"id":"com.xiaomi.mbnloader","description":"Modem Config\nHidden app for Choosing Country vowifi?","removal":"replace","type":"oem"},{"id":"com.xiaomi.metoknlp","description":"Network location provider\nUseless, only for China, have analytics things.","removal":"delete","type":"oem"},{"id":"com.xiaomi.mi_connect_service","description":"MiConnectService\nHandles connection to IoT stuff\nSeems to be linked to Mi Home (com.xiaomi.smarthome)\n","removal":"delete","type":"oem"},{"id":"com.xiaomi.miaudiovisual","description":"MiAudioVisual\nSafe to remove if you not use audio visuals when screen is off.","removal":"delete","type":"oem"},{"id":"com.xiaomi.mibrain.speech","description":"Mi AI Speech Engine\nAnother app to Mi AI Chinese app.","removal":"delete","type":"oem"},{"id":"com.xiaomi.micloud.sdk","label":"com.xiaomi.micloud.sdk","description":"Mi Cloud sdk \nsdk = Software development kit\nSeems to be a dependency for \"com.miui.gallery\".","removal":"unsafe","warning":"MIUI auto reboots android after removing this package","type":"oem"},{"id":"com.xiaomi.midrop","description":"Share Me (Mi Drop) (https://play.google.com/store/apps/details?id=com.xiaomi.midrop)\nP2P file transfer tool.\n","removal":"delete","type":"oem"},{"id":"com.xiaomi.midrop.overlay","description":"Mi Drop overlay\nOverlays are usually themes.","removal":"replace","type":"oem"},{"id":"com.xiaomi.migameservice","label":"Mi Game Service","description":"Chinese app made to test game service things.","removal":"delete","type":"oem"},{"id":"com.xiaomi.mipicks","description":"Mi Picks (becomed Mi Apps Store and now Get Apps -- Xiaomi app store)\nI believe this package is discontinued.\nhttps://play.google.com/store/apps/details?id=com.mi.global.shop\n","removal":"delete","type":"oem"},{"id":"com.xiaomi.miplay_client","description":"MiPlay Client\nProvides support for Miracast (https://en.wikipedia.org/wiki/Miracast).\nIt provides the Wireless Display feature (Settings - Connection & sharing - Cast).","removal":"replace","type":"oem"},{"id":"com.xiaomi.mircs","description":"Mi RCS\nHidden unused Xiaomi free web messaging.","removal":"delete","type":"oem"},{"id":"com.xiaomi.mirecycle","description":"Mi Recycle \nXiaomi has extended its partnership with Cashify to launch the 'Mi Recycle' feature through its MIUI Security app. \nIt will let Xiaomi phone users check the health of their smartphone and get their resale value directly from Cashify, \nthe online re-commerce company based out of New Delhi.\nSource : https://gadgets.ndtv.com/mobiles/news/xiaomi-mi-recycle-cashify-miui-security-app-2018024\n","removal":"delete","type":"oem"},{"id":"com.xiaomi.mirror","label":"MIUI+ Beta","description":"Transfer files, sync copy text to PC without USB.","web":["https://plus.miui.com"],"removal":"delete","type":"oem"},{"id":"com.xiaomi.mis","description":"Xiaomi Connected Car Service for China.","removal":"delete","type":"oem"},{"id":"com.xiaomi.misettings","description":"Xiaomi Settings app\n","removal":"unsafe","type":"oem"},{"id":"com.xiaomi.mitv.res","description":"MiUtilRes\nLooks like mitv api.\nProbably Unsafe to remove.","removal":"caution","type":"oem"},{"id":"com.xiaomi.mtb","description":"Rueban(MTB)V2.4\nHidden debugging baseband tools, not available for users.\nhttps://i.postimg.cc/GpSxmNyj/Bez-n-zvu.png","removal":"delete","type":"oem"},{"id":"com.xiaomi.o2o","description":"o2o = online-to-offline\n==> Describes systems enticing consumers within a digital environment to make purchases of goods or services from physical businesses.\nhttps://en.wikipedia.org/wiki/Online_to_offline\nNOTE: This package can make phone calls without user intervention.\n","removal":"delete","type":"oem"},{"id":"com.xiaomi.oobhelper","description":"OOBHelper\nUseless frameworks and logs.","removal":"delete","type":"oem"},{"id":"com.xiaomi.otrpbroker","description":"TAMservice\nOTRP Protocol Negotiation Program (Internet of Things)\nOnly useful in China.","removal":"delete","type":"oem"},{"id":"com.xiaomi.oversea.ecom","description":"Xiaomi ShopPlus.\nGiven its name I think this package is useless.\n","removal":"delete","type":"oem"},{"id":"com.xiaomi.pass","description":"Mi Pass is an App allows Xiaomi NFC phones to replace cards and keys in real life usage. \nSupport NFC payment, bus card, key card, door and car lock features all together.\n","removal":"delete","type":"oem"},{"id":"com.xiaomi.payment","description":"Old package name for Mi Credit (https://play.google.com/store/apps/details?id=com.micredit.in.gp)\nMi Credit is a personal loan platform from Xiaomi.\n","removal":"delete","type":"oem"},{"id":"com.xiaomi.powerchecker","label":"Power Detector","description":"Located at Security> Battery> Activity Control.\nDetects abnormal power usage by apps (not all. Some Xiaomi apps are whitelisted).\nNeeded for 'com.miui.powerkeeper' to work.","removal":"caution","type":"oem"},{"id":"com.xiaomi.providers.appindex","description":"Provider for app index?\nI believe it is a provider for the settings but can't confirm (I don't have a Xiaomi device).\nA lot of people debloat this but I'd like to know more about this one.\nContent providers encapsulate data, providing centralized management of data shared between apps.\nhttps://developer.android.com/guide/topics/providers/content-providers.html","removal":"delete","type":"oem"},{"id":"com.xiaomi.scanner","description":"Mi Scanner\nQR code scanner with a lot of questionable permissions : `ACCESS_FINE_LOCATION`, `CALL_PHONE`, `READ_CONTACTS`, `REQUEST_INSTALL_PACKAGES`, `QUERY_ALL_PACKAGES`, `FOREGROUND_SERVICE`, `INTERNET`\n","removal":"delete","type":"oem"},{"id":"com.xiaomi.security.onetrack","description":"SecurityOnetrackService\nOnly uses MIUI analytics.","removal":"delete","type":"oem"},{"id":"com.xiaomi.shop","description":"Xiaomi app store (I thinks it's discontinued)\nNow com.mi.global.shop (https://play.google.com/store/apps/details?id=com.mi.global.shop)\n","removal":"delete","type":"oem"},{"id":"com.xiaomi.simactivate.service","description":"Xiaomi SIM Activation Service\nSIM authentication process to access exclusive features in certain MIUI applications.\nFor the activation to work you need to send a international SMS to China.\nYour carrier may block this by default and/or you'll probably need to pay extra for this.\nAfter SIM activation, you can send text messages (Mi Messages) to other Mi users using internet connection (like i-messages).\nYou will be able to synchronize your messages into Mi Cloud and this also enables the Mi Find Device feature which allows you to track your phone’s location from your online Mi account.\n\nNote: To enable/disable Mi Messages go to Settings -> System Apps -> Messaging and reboot","removal":"delete","type":"oem"},{"id":"com.xiaomi.smarthome","description":"Mi Home (https://play.google.com/store/apps/details?id=com.xiaomi.smarthome)\nIoT. Lets you control with Xiaomi Smart Home Suite devices.\n","removal":"delete","type":"oem"},{"id":"com.xiaomi.touchservice","description":"No activities, uses miui analytics looks like a tracking touch.","removal":"delete","type":"oem"},{"id":"com.xiaomi.trustservice","description":"MiTrustService\nIFAASecCam, security things or 'Remote Control trust'.","removal":"delete","type":"oem"},{"id":"com.xiaomi.ugd","description":"GPU Driver Updater\nIt's weird when this app cameback on HyperOS(from MIUI 12).\nUpdates GPU driver.","removal":"caution","type":"oem"},{"id":"com.xiaomi.upnp","description":"UpnpService\nUPnP = Universal Plug and Play\nIt’s a protocol that lets UPnP-enabled devices on your network automatically discover and communicate with each other\nFor example it works with the Xiaomi Network Speaker (and probably a lot more Xiaomi IoT stuff)\nUPnP has a lot of security issues and you proably should disable it on your router.\nhttps://nakedsecurity.sophos.com/2020/06/10/billions-of-devices-affected-by-upnp-vulnerability/\nThis package is the Xiaomi implementation on Android (no AOSP support)\n","removal":"delete","type":"oem"},{"id":"com.xiaomi.vipaccount","description":"Xiaomi VIP account\nhttps://www.mi.com/in/service/privilegefaq/\n","removal":"delete","type":"oem"},{"id":"com.xiaomi.xaee","description":"XiaoaiEdgeEngine\nThis app has something to Mi AI.","removal":"delete","type":"oem"},{"id":"com.xiaomi.xmsf","description":"Xiaomi Service Framework\nContains a set of API's for Xiaomi apps. Expect widespread breakage of Xiaomi apps/functionality if disabled.\nDisabling will mess with Alarm clock functionality(according to issue#136) and break Mi Cloud and Mi account (and all features that depend on them).\nI don't know about now, but in 2016 this app constantly tried to establish tcp connections in the background.","removal":"caution","type":"oem"},{"id":"com.xiaomi.xmsfkeeper","description":"Xiaomi Service Framework Keeper\nLogger service for 'com.xiaomi.xmsf'\n","removal":"delete","type":"oem"},{"id":"com.xiaomi.youpin","description":"Xiaomi Yipin\nMi Shop China.","removal":"delete","type":"oem"},{"id":"com.ximalaya.ting.android","description":"Chinese Ximalaya.","removal":"delete","type":"oem"},{"id":"com.xingin.xhs","description":"Chinese Little Red Book.","removal":"delete","type":"oem"},{"id":"com.xm.webcontent","description":"WebContent\nNeeded for messages like 'App installing'. Also WebView.","removal":"caution","type":"oem"},{"id":"com.xui.xhide","description":"XHide\nProvides the only way to hide files and lock apps when using Transsion launchers (which the Transsion devices require for certain functionality like multiapps not working with other launchers). These are the only apps that hide files and lock apps with them.","removal":"replace","type":"oem"},{"id":"com.xunmeng.pinduoduo","description":"Chinese JD.com.","removal":"delete","type":"oem"},{"id":"com.xy.smartmmsplugin.remote","description":"SmartMmsPlugin\nConnects to Chinese maps.\nLoads Flight, Train, Movie information.\nIt's only for China.","removal":"delete","type":"oem"},{"id":"com.yandex.browser","description":"Yandex' browser\nMay be installed with Samsung firmware update to comply with http://publication.pravo.gov.ru/Document/View/0001202011230051 if you're Russian. Can be installed manually from Google Play.","removal":"delete","type":"oem"},{"id":"com.yha.engineersecret","description":"Reads/writes to External Storage. Gets the IMEI, uses gps/wifi lots of activities (hardware tests?):\nIMeiAndPcbCheck, DeviceListActivity, CheckSoftwareInfo, GpsActivity, BluetoothTest, BluetoothSearch, LteSarTest, NetworkSearch, DecryptionActivity","removal":"replace","type":"oem"},{"id":"com.yha.factory","description":"related to com.yha.runtime","removal":"replace","type":"oem"},{"id":"com.yha.runtime","description":"RunTimeTest\nReads/writes to External Storage. Uses wifi, camera, sensors, mic, etc. Lots of activities (hardware tests?):\nFlashTest, VibratorTest, WIFITest, Reboot, LCDTest, AudioLoop, VideoTest, CameraTest, Tester3dTeapot, FullMemTest, ScreenSaver.","removal":"replace","type":"oem"},{"id":"com.youku.phone","description":"Chinese Youku video.","removal":"delete","type":"oem"},{"id":"com.youview.poa","description":"Youview live tv app\nhttps://play.google.com/store/apps/details?id=com.youview.poa&hl=en_US/\nSometimes a remote on your button opens this but that can be changed in the settings. Safe to remove.","removal":"delete","type":"oem"},{"id":"com.yozo.vivo.office","description":"Vivo document reader\nA lot of permissions for a simple document reader. It can access to internet, can list all the apps you installed, can get your phone number, current cellular network information, the status of any ongoing calls and more!\n\nPithus analysis: https://beta.pithus.org/report/8902163722f5df1ae6228b80124cfa94c2b8a0210a8f6bbb3441e05d69a76d0b","removal":"replace","type":"oem"},{"id":"com.yulore.framework","description":"Chinese payment things.","removal":"delete","type":"oem"},{"id":"com.zhihu.android","description":"Chinese know.","removal":"delete","type":"oem"},{"id":"com.zte.assistant","description":"ZTE Voice Assistant\n","removal":"delete","type":"oem"},{"id":"com.zte.beautify","description":"ZTE Theme\nNeeded for themes","removal":"replace","type":"oem"},{"id":"com.zte.burntest.camera","description":"Possibly a hidden stress testing app for camera.","removal":"replace","type":"oem"},{"id":"com.zte.contacts.sub","description":"Possibly related to contact sync.","removal":"replace","type":"oem"},{"id":"com.zte.easymode","description":"Easy mode\nEasy Mode settings","removal":"delete","type":"oem"},{"id":"com.zte.emode","description":"Testing Hardware Components","removal":"delete","type":"oem"},{"id":"com.zte.emodeservice","description":"Logs to emode Testing Hardware Components","removal":"delete","type":"oem"},{"id":"com.zte.faceverify","description":"Needed for face unlock screen","removal":"delete","type":"oem"},{"id":"com.zte.fingerprints","description":"Fingerprint service\nFingerprint settings.","removal":"caution","type":"oem"},{"id":"com.zte.heartyservice.strategy","description":"Process killer, unloads programs from memory.","removal":"replace","type":"oem"},{"id":"com.zte.mifavor.launcher","description":"Stock ZTE Launcher","removal":"caution","type":"oem"},{"id":"com.zte.mifavor.launcher.adapter","description":"Needed for Stock ZTE Launcher","removal":"caution","type":"oem"},{"id":"com.zte.mifavor.launcher.resource","description":"Needed for Stock ZTE Launcher","removal":"caution","type":"oem"},{"id":"com.zte.mifavor.theme.resource","description":"Needed for themes?","removal":"caution","type":"oem"},{"id":"com.zte.onekeycp","description":"Phone Switch\nPC Connect things, not needed.","removal":"delete","type":"oem"},{"id":"com.zte.powersavemode","description":"Has battery settings.","removal":"unsafe","type":"oem"},{"id":"com.zte.privacypolicy","description":"ZTE privacy policy app.","removal":"delete","type":"oem"},{"id":"com.zte.privacyzone","description":"App Lock\nApp Lock, Password Manager","removal":"delete","type":"oem"},{"id":"com.zte.setupwizard","description":"Setup Wizard\nNeeded for first-boot setup","removal":"delete","type":"oem"},{"id":"com.zte.videoplayer","label":"Video Player","description":"ZTE Video Player with INTERNET and ACCESS_NETWORK_STATE permissions.","web":["https://beta.pithus.org/report/caf2da956d33c5550e42d4250b0fa31dc605f39545c2eff36438fd88a0fc7c28"],"removal":"replace","suggestions":"video_players","type":"oem"},{"id":"com.zte.weather","label":"Weather","description":"ZTE Weather app.","web":["https://play.google.com/store/apps/details?id=com.zte.weather"],"removal":"replace","suggestions":"weather_apps","type":"oem"},{"id":"com.zte.zbackup.platservice","description":"ZteBackupService\nZteBackupService with a lot of permissions.","removal":"delete","type":"oem"},{"id":"com.zte.zdm.omacp","description":"For configuring SMS settings.","removal":"caution","type":"oem"},{"id":"com.zte.zdmdaemon","description":"Useless logs, data collection.","removal":"delete","type":"oem"},{"id":"com.zte.zdmdaemon.install","description":"Useless logs, data collection.","removal":"delete","type":"oem"},{"id":"com.ztefingerprint.service","description":"Probably needed for fingerprint.","removal":"caution","type":"oem"},{"id":"com.zui.android.overlay.common","description":"Useless config to Chinese location.","removal":"delete","type":"oem"},{"id":"com.zui.android.overlay.product","description":"Config to refresh rate 120? Better don't risk.","removal":"unsafe","type":"oem"},{"id":"com.zui.antitheft","description":"Find the phone\nFind the phone. (Log out from account before remove)","removal":"delete","type":"oem"},{"id":"com.zui.browser","description":"Lenovo Browser\nStock Lenovo Browser with Chinese trackers.","removal":"delete","type":"oem"},{"id":"com.zui.bugtogo","description":"Hidden collecting user data.","removal":"delete","type":"oem"},{"id":"com.zui.callsettings","description":"Additional Call Settings also WiFi calling.","removal":"replace","type":"oem"},{"id":"com.zui.camera","description":"Lenovo ZUI Camera\nLenovo Stock Camera App","removal":"replace","type":"oem"},{"id":"com.zui.camera.assistant","description":"It's for camera app or WiFi calling?","removal":"replace","type":"oem"},{"id":"com.zui.camera.avatar","description":"MakeAvatar\nMakeAvatar? It's something for face recognition.","removal":"delete","type":"oem"},{"id":"com.zui.camera.plugin.dolphin","description":"Needed for camera settings?","removal":"replace","type":"oem"},{"id":"com.zui.contacts","description":"Lenovo Contacts\nStock Lenovo Contacts app","removal":"replace","type":"oem"},{"id":"com.zui.continuity","description":"Lenovo One\nConnects phone to PC and more. Not needed.","removal":"delete","type":"oem"},{"id":"com.zui.cores","description":"Removing will affect a lot of other ZUI functions like the game assistant, etc. so it's a bad idea.","removal":"caution","type":"oem"},{"id":"com.zui.davdroid","description":"Login to iCloud and sync contacts, calendars.","removal":"delete","type":"oem"},{"id":"com.zui.deskclock","description":"Lenovo ZUI Clock\nStock Lenovo clock app.","removal":"replace","type":"oem"},{"id":"com.zui.deskclock.overlay.lime","description":"Overlay to theme icon Clock app.","removal":"delete","type":"oem"},{"id":"com.zui.deskclock.overlay.mostbeautiful","description":"Overlay to theme icon Clock app.","removal":"delete","type":"oem"},{"id":"com.zui.deskclock.overlay.phantom","description":"Overlay to theme icon Clock app.","removal":"delete","type":"oem"},{"id":"com.zui.deskclock.overlay.redstorm","description":"Overlay to theme icon Clock app.","removal":"delete","type":"oem"},{"id":"com.zui.deskclock.overlay.steellegion","description":"Overlay to theme icon Clock app.","removal":"delete","type":"oem"},{"id":"com.zui.deskclock.overlay.thinarmor","description":"Overlay to theme icon Clock app.","removal":"delete","type":"oem"},{"id":"com.zui.deviceidservice","description":"Analytics and logs.","removal":"delete","type":"oem"},{"id":"com.zui.filemanager","description":"File manager app","removal":"replace","type":"oem"},{"id":"com.zui.freeform.sidebar","description":"Unused frameworks? Probably split screen for apps will not work, not tested.","removal":"replace","type":"oem"},{"id":"com.zui.gallery","description":"Gallery\nStock Gallery app for lenovo phones.","removal":"delete","type":"oem"},{"id":"com.zui.game.service","description":"A lot of login things and Chinese.","removal":"delete","type":"oem"},{"id":"com.zui.homesettings","description":"Badges? It's Chinese, so not needed.","removal":"delete","type":"oem"},{"id":"com.zui.launcher","description":"Lenovo Launcher ZUI HOME.","removal":"caution","type":"oem"},{"id":"com.zui.legalinfo","description":"I found only hidden privacy policy activity.","removal":"delete","type":"oem"},{"id":"com.zui.mms","description":"Stock Messaging App","removal":"replace","type":"oem"},{"id":"com.zui.mms.overlay.mostbeautiful","description":"Overlay to theme icon Messages app.","removal":"replace","type":"oem"},{"id":"com.zui.net.data.monitor","description":"Needed for Network control","removal":"delete","type":"oem"},{"id":"com.zui.networkaclr","description":"Network manager?","removal":"caution","type":"oem"},{"id":"com.zui.oneime","description":"Input method for Lenovo One\nIt's input method for Lenovo One.","removal":"delete","type":"oem"},{"id":"com.zui.ota","description":"Lenovo system update\nProvides system updates.","removal":"caution","type":"oem"},{"id":"com.zui.radioinfo","description":"Hidden testing hardware things.","removal":"delete","type":"oem"},{"id":"com.zui.safecenter","description":"Security Center\nUseless security app? You will lose optimization probably.","removal":"caution","type":"oem"},{"id":"com.zui.safecenter.overlay.lime","description":"Overlay to theme icon Security app.","removal":"delete","type":"oem"},{"id":"com.zui.safecenter.overlay.mostbeautiful","description":"Overlay to theme icon Security app.","removal":"delete","type":"oem"},{"id":"com.zui.safecenter.overlay.phantom","description":"Overlay to theme icon Security app.","removal":"delete","type":"oem"},{"id":"com.zui.safecenter.overlay.redstorm","description":"Overlay to theme icon Security app.","removal":"delete","type":"oem"},{"id":"com.zui.safecenter.overlay.steellegion","description":"Overlay to theme icon Security app.","removal":"delete","type":"oem"},{"id":"com.zui.safecenter.overlay.thinarmor","description":"Overlay to theme icon Security app.","removal":"delete","type":"oem"},{"id":"com.zui.sdac","description":"Not needed for notifications and it's probably testing notification.","removal":"delete","type":"oem"},{"id":"com.zui.setupwizard","description":"Setup Wizard\nNeeded for first-boot setup.","removal":"delete","type":"oem"},{"id":"com.zui.simcontacts","description":"It should be named com.qualcomm.simcontacts.\nIt's hidden and not available for users.","removal":"delete","type":"oem"},{"id":"com.zui.tassistent","description":"Assistant Tracking, Logs.","removal":"delete","type":"oem"},{"id":"com.zui.theme.overlay.lime","description":"Wallpaper to Theme App.","removal":"replace","type":"oem"},{"id":"com.zui.theme.overlay.mostbeautiful","description":"Wallpaper to Theme App.","removal":"replace","type":"oem"},{"id":"com.zui.theme.overlay.phantom","description":"Wallpaper to Theme App.","removal":"replace","type":"oem"},{"id":"com.zui.theme.overlay.redstorm","description":"Wallpaper to Theme App.","removal":"replace","type":"oem"},{"id":"com.zui.theme.overlay.steellegion","description":"Wallpaper to Theme App.","removal":"replace","type":"oem"},{"id":"com.zui.theme.overlay.thinarmor","description":"Wallpaper to Theme App.","removal":"replace","type":"oem"},{"id":"com.zui.theme.settings","description":"Lenovo theme settings\nNeeded for theme settings.","removal":"caution","type":"oem"},{"id":"com.zui.thirdparty.sdk","description":"Third party useless frameworks.","removal":"delete","type":"oem"},{"id":"com.zui.tuface","description":"It's for faceid and probably needed for face unlock lock screen.","removal":"delete","type":"oem"},{"id":"com.zui.udevice","description":"Useless preview activity to earphones.","removal":"delete","type":"oem"},{"id":"com.zui.userexperience","description":"It's for spying: app usage, device usage.","removal":"delete","type":"oem"},{"id":"com.zui.wallet","description":"Chinese payment","removal":"delete","type":"oem"},{"id":"com.zui.wallpapercropper","description":"Crop wallpaper\nWallpaper cropper.","removal":"replace","type":"oem"},{"id":"com.zui.wallpapersetting","description":"Needed for wallpaper settings","removal":"replace","type":"oem"},{"id":"com.zui.weather","description":"Weather\nStock weather app.","removal":"replace","type":"oem"},{"id":"com.zui.wifip2p","description":"Lenovo One file transfer\nAnother app for Lenovo One things.","removal":"delete","type":"oem"},{"id":"com.zui.xlog","description":"Hidden logs, package usage collector.","removal":"delete","type":"oem"},{"id":"ctrip.android.view","description":"Chinese Ctrip.","removal":"delete","type":"oem"},{"id":"eu.xiaomi.ext","description":"Xiaomi.eu Extension\nCalendar app things. Only for China.","removal":"delete","type":"oem"},{"id":"eu.xiaomi.module.inject","description":"Inject fields\nFields name of device info like which os version or security date.\nNot sure if it's needed.","removal":"replace","type":"oem"},{"id":"fr.bouyguestelecom.agent.custo","description":"This app wanna include ads to your gallery only.\nFounded only ads and crashlytics components.","removal":"delete","type":"oem"},{"id":"huawei.android.widget","description":"App that actually doesnt do anything? Remove it doesnt affect on your widgets.","removal":"delete","type":"oem"},{"id":"jp.co.omronsoft.mushroom.CommonPhrase","description":"Japanese Phrases app","removal":"delete","type":"oem"},{"id":"jp.co.radius.neplayer_asus","description":"NePLAYER\nJapanese NePLAYER Lite for ASUS","removal":"delete","type":"oem"},{"id":"ma.android.com.mafactory","label":"Factory Test","description":"It's for hardware components testing.","removal":"delete","type":"oem"},{"id":"me.phh.treble.app","description":"Used for settings GSI rom that based by Phh Treble. This should not be remove.","removal":"unsafe","type":"oem"},{"id":"mitv.service","description":"TvService\nHas shutdown delay.\nSafe to remove.","removal":"replace","type":"oem"},{"id":"miui.systemui.plugin","label":"System UI Plug-in","description":"When using HyperOS, removing this package breaks the iOS-style quick settings and Android will use the AOSP-version of the volume bar & reboot screen (AKA the option to power off/reboot your device). If your device is using MIUI, only the volume bar will change.","removal":"caution","type":"oem"},{"id":"miuix.stub","description":"It has something to unknown miuix FrequentPhrase, Chinese things found.","removal":"delete","type":"oem"},{"id":"net.oneplus.commonlogtool","description":"OnePlus Common Log Tool\n9 permissions and given what we know about OnePlus logging apps, it's a good idea to disable this.\nSee com.oneplus.opbugreportlite, com.oem.oemlogkit and net.oneplus.odm","removal":"delete","type":"oem"},{"id":"net.oneplus.forums","description":"OnePlus Community (https://play.google.com/store/apps/details?id=net.oneplus.forums)\nLiterally just their forum... in an app.\nJust use a Browser if you wanna access the forums.","removal":"delete","type":"oem"},{"id":"net.oneplus.launcher","description":"Oneplus Launcher\nRuns in the background as part of the system.\nAside from obviously handling the default launcher itself, it also handles the Recents UI on Android 9, the home&recents gestures in Android 11, some submenus in the Settings app and possibly more that I'm unaware of.\nProbably not a good idea to disable.","removal":"caution","type":"oem"},{"id":"net.oneplus.launcher.black.overlay","description":"Theme overlay for the Oneplus Launcher?","removal":"caution","type":"oem"},{"id":"net.oneplus.launcher.white.overlay","description":"Theme overlay for the Oneplus Launcher?","removal":"caution","type":"oem"},{"id":"net.oneplus.odm","description":"\"OnePlus System Service\"\nShady telemetry app.\nSends loads of data to OnePlus' servers, including IMEI, phone number, MAC addresses, mobile network names and IMSI prefixes, Wi-Fi connection info, the phone's serial number and every time an app was opened.\nSource: https://www.chrisdcmoore.co.uk/post/oneplus-analytics/\nPress: https://www.androidpolice.com/2017/10/10/never-settle-oneplus-found-collecting-personally-identifiable-analytics-data-phone-owners/","removal":"delete","type":"oem"},{"id":"net.oneplus.odm.provider","description":"Insight Provider\nProvider for net.oneplus.odm? (shady telemetry app)\nContent providers encapsulate data, providing centralized management of data shared between apps.\nhttps://developer.android.com/guide/topics/providers/content-providers.html","removal":"delete","type":"oem"},{"id":"net.oneplus.provider.appcategoryprovider","description":"AppCategoryProvider\nRuns in the background.\nI think this categorizes apps for use with system functionality, for example: automatically adding games to Game Mode.","removal":"delete","type":"oem"},{"id":"net.oneplus.push","description":"Push\nOnePlus push notifications.\nOnly used by OnePlus' pre-installed apps. Pushes \"surveys and other junk\" according to a user.\nhttps://forums.oneplus.com/threads/psa-non-root-root-stop-oneplus-push-notifications.580058/\nOnePlus can remotely send push notifications:\nhttps://www.androidpolice.com/2019/07/01/oneplus-accidentally-pushed-a-cryptic-notification-to-all-7-pro-users/","removal":"delete","type":"oem"},{"id":"net.oneplus.wallpaperresources","description":"Resources for some live wall papers? Not sure.\nOnly contains a \"WallpaperResourceProvider\", no services, activities or receivers.","removal":"caution","type":"oem"},{"id":"net.oneplus.weather","label":"Weather","description":"Occasionally runs in the background; I think it runs every now and then to change the app icon to current weather conditions.","web":["https://play.google.com/store/apps/details?id=net.oneplus.weather"],"removal":"replace","suggestions":"weather_apps","type":"oem"},{"id":"net.oneplus.weather.basiccolorblack.overlay","description":"Theme overlay for Oneplus Weather app?","removal":"caution","type":"oem"},{"id":"net.oneplus.weather.basiccolorwhite.overlay","description":"Theme overlay for Oneplus Weather app?","removal":"caution","type":"oem"},{"id":"net.oneplus.widget","label":"OnePlus Widget","description":"Lets you use OnePlus widgets on the home screen.","removal":"delete","type":"oem"},{"id":"nubia.camera.dualcamtest","description":"Dual Camera Test","removal":"delete","type":"oem"},{"id":"ohos.media.medialibrary","description":"MediaLibrary\nhas hwddmp components `com.huawei.hwddmp` debugging errors, remote device manager that depends on Huawei account","removal":"delete","type":"oem"},{"id":"om.xiaomi.gnss.polaris","description":"Polaris\nBeidou satellite navigation system. Only for China.","removal":"delete","type":"oem"},{"id":"om.zui.resolver","description":"Lenovo Share\nLenovo Share for Chinese","removal":"delete","type":"oem"},{"id":"oplus.frameworkres.overlay.display.product","description":"Display cutout.\nA display cutout is an area on some devices that extends into the display surface. It allows for an edge-to-edge experience while providing space for important sensors on the front of the device.\nhttps://developer.android.com/develop/ui/views/layout/display-cutout","removal":"caution","type":"oem"},{"id":"org.codeaurora.btmultisim","description":"it's app without code.","removal":"delete","type":"oem"},{"id":"org.codeaurora.qti.nrNetworkSettingApp","description":"Needed for 5G?","removal":"caution","type":"oem"},{"id":"org.dtx.aidl.manager","description":"DtxService\nAnt Dtx Service, unknown app, but safe to remove, probably useful in China.","removal":"delete","type":"oem"},{"id":"org.ifaa.aidl.manager","label":"IfaaManagerService","description":"IFAA = (China’s) Internet Finance Authentication Alliance\nProvides biometric authentication for Alipay. Probably safe to disable if you don't use it.","removal":"delete","type":"oem"},{"id":"org.mipay.android.manager","description":"MipayService\nXiaomi Payment related services, not used!","removal":"delete","type":"oem"},{"id":"product.lge.data.server.LgDataServiceMain","description":"Needed for iwlan? Very unknown app.","removal":"caution","type":"oem"},{"id":"ru.yandex.disk","description":"Yandex' file cloud storage\nMay be installed with Samsung firmware update to comply with http://publication.pravo.gov.ru/Document/View/0001202011230051 if you're Russian. Can be installed manually from Google Play.","removal":"delete","type":"oem"},{"id":"ru.yandex.searchplugin","description":"Yandex' search engine plugin\nMay be installed with Samsung firmware update to comply with http://publication.pravo.gov.ru/Document/View/0001202011230051 if you're Russian. Can be installed manually from Google Play.","removal":"delete","type":"oem"},{"id":"ru.yandex.yandexmaps","description":"Yandex' Maps\nMay be installed with Samsung firmware update to comply with http://publication.pravo.gov.ru/Document/View/0001202011230051 if you're Russian. Can be installed manually from Google Play.","removal":"delete","type":"oem"},{"id":"screnmirroring.com","description":"Sony's screen mirroring app. Can be removed as chromecasting still works without it","removal":"delete","type":"oem"},{"id":"se.dirac.acs","label":"Dirac Control Service","description":"Sound-system backend?\nRuns in the background as part of the system. Runs even if disabled.","removal":"caution","type":"oem"},{"id":"sg.gov.mnd.OneService","description":"map app with a lot tracking\nhttps://play.google.com/store/apps/details?id=sg.gov.mnd.OneService","removal":"delete","type":"oem"},{"id":"tech.palm.id","description":"TECNO ID\nit's for account? A lot bloated.\nInstead Uninstall better Disable app because Uninstalling cause Settings app crash","removal":"delete","type":"oem"},{"id":"tv.alphonso.alphonso_eula","description":"Alphonso Recommendations\nEnhanced viewing, personalized experience of watching TV.","removal":"delete","type":"oem"},{"id":"tv.danmaku.bili","description":"Chinese app 'bilibili'.","removal":"delete","type":"oem"},{"id":"tv.peel.samsung.app","label":"Peel Smart Remote (WatchON)","description":"It's an application that turns your smart phone or tablet into a TV remote.\nThe app uses the IR Blaster of your device, so devices not equipped with that feature will not be able to use all of Peel Smart Remote's functions.","web":["https://www.samsung.com/za/support/mobile-devices/what-is-the-peel-smart-remote-application/"],"removal":"delete","type":"oem"},{"id":"udc.lenovo.com.udclient","description":"Lenovo Universal Device Client\nAnother Mobile Device Management (MDM) allows company’s IT department to reach inside your phone in the background, allowing them to ensure\nyour device is secure, know where it is, and remotely erase your data if the phone is stolen.\nIt's a way to ensure employees stay productive and do not breach corporate policies\nYou should NEVER have a MDM tool on your personal phone.","removal":"delete","type":"oem"},{"id":"vendor.mediatek.iwlanservice","description":"iWlan service, not sure if needed.","removal":"caution","type":"oem"},{"id":"zte.com.cn.alarmclock","description":"Clock\nStock ZTE Clock App","removal":"replace","type":"oem"},{"id":"zte.com.cn.filer","description":"File Manager\nStock File Manager","removal":"replace","type":"oem"}] ================================================ FILE: app/src/main/assets/editor_themes/dark.tmTheme.json ================================================ { "name": "darcula", "settings": [ { "settings": { "background": "#1F1A1B", "foreground": "#cccccc", "lineHighlight": "#2B2B2B", "blockLineColor": "#575757", "currentBlockLineColor": "#7a7a7a", "selection": "#214283", "completionWindowBackground": "#1F1A1B", "completionWindowStroke": "#555555" } }, { "name": "Package declaration", "scope": "storage.modifier.package", "settings": { "foreground": "#CCCCCC" } }, { "name": "Import declaration", "scope": "storage.modifier.import", "settings": { "foreground": "#CCCCCC" } }, { "name": "Class names (Identifiers starting with uppercase)", "scope": "storage.type.java", "settings": { "foreground": "#CCCCCC" } }, { "name": "Annotation", "scope": "storage.type.annotation", "settings": { "foreground": "#BBB529" } }, { "name": "Comment", "scope": "comment", "settings": { "foreground": "#707070" } }, { "name": "Operator Keywords", "scope": "keyword.operator,keyword.operator.logical,keyword.operator.relational,keyword.operator.assignment,keyword.operator.comparison,keyword.operator.ternary,keyword.operator.arithmetic,keyword.operator.spread", "settings": { "foreground": "#CCCCCC" } }, { "name": "Strings", "scope": "string,string.character.escape,string.template.quoted,string.template.quoted.punctuation,string.template.quoted.punctuation.single,string.template.quoted.punctuation.double,string.type.declaration.annotation,string.template.quoted.punctuation.tag", "settings": { "foreground": "#6A8759" } }, { "name": "String Interpolation Begin and End", "scope": "punctuation.definition.template-expression.begin,punctuation.definition.template-expression.end", "settings": { "foreground": "#CC8242" } }, { "name": "String Interpolation Body", "scope": "expression.string,meta.template.expression", "settings": { "foreground": "#CCCCCC" } }, { "name": "Number", "scope": "constant.numeric", "settings": { "foreground": "#7A9EC2" } }, { "name": "Built-in constant", "scope": "constant.language,variable.language", "settings": { "foreground": "#CC8242" } }, { "name": "User-defined constant", "scope": "constant.character, constant.other", "settings": { "foreground": "#9E7BB0" } }, { "name": "Keyword", "scope": "keyword,keyword.operator.new,keyword.operator.delete,keyword.operator.static,keyword.operator.this,keyword.operator.expression", "settings": { "foreground": "#CC8242" } }, { "name": "Method return type", "scope": "meta.method.return-type", "settings": { "foreground": "#A9B7C6" } }, { "name": "Method call identifier", "scope": "meta.method-call", "settings": { "foreground": "#A9B7C6" } }, { "name": "Types, Class Types", "scope": "entity.name.type,meta.return.type,meta.type.annotation,meta.type.parameters,support.type.primitive", "settings": { "foreground": "#7A9EC2" } }, { "name": "Storage type", "scope": "storage,storage.type,storage.modifier,storage.arrow", "settings": { "foreground": "#CC8242" } }, { "name": "Class constructor", "scope": "class.instance.constructor,new.expr entity.name.type", "settings": { "foreground": "#FFC66D" } }, { "name": "Function", "scope": "support.function, entity.name.function", "settings": { "foreground": "#FFC66D" } }, { "name": "Function Types", "scope": "annotation.meta.ts, annotation.meta.tsx", "settings": { "foreground": "#CCCCCC" } }, { "name": "Function Argument", "scope": "variable.parameter, operator.rest.parameters", "settings": { "foreground": "#A9B7C6" } }, { "name": "Variable, Property", "scope": "variable.property,variable.other.property,variable.other.object.property,variable.object.property,support.variable.property", "settings": { "foreground": "#9E7BB0" } }, { "name": "Variable name", "scope": "entity.name.variable", "settings": { "foreground": "#A9B7C6" } }, { "name": "CONSTANT", "scope": "variable.other.constant", "settings": { "foreground": "#9876AA" } }, { "name": "Module Name", "scope": "quote.module", "settings": { "foreground": "#6A8759" } }, { "name": "Markup Headings", "scope": "markup.heading", "settings": { "foreground": "#CC8242" } }, { "name": "Tag name", "scope": "punctuation.definition.tag.html, punctuation.definition.tag.begin, punctuation.definition.tag.end, entity.name.tag", "settings": { "foreground": "#FFC66D" } }, { "name": "Tag attribute", "scope": "entity.other.attribute-name", "settings": { "foreground": "#CCCCCC" } }, { "name": "Object Keys", "scope": "meta.object-literal.key", "settings": { "foreground": "#9E7BB0" } }, { "name": "TypeScript Class Modifiers", "scope": "storage.modifier.ts", "settings": { "foreground": "#CC8242" } }, { "name": "TypeScript Type Casting", "scope": "ts.cast.expr,ts.meta.entity.class.method.new.expr.cast,ts.meta.entity.type.name.new.expr.cast,ts.meta.entity.type.name.var-single-variable.annotation,tsx.cast.expr,tsx.meta.entity.class.method.new.expr.cast,tsx.meta.entity.type.name.new.expr.cast,tsx.meta.entity.type.name.var-single-variable.annotation", "settings": { "foreground": "#7A9EC2" } }, { "name": "TypeScript Type Declaration", "scope": "ts.meta.type.support,ts.meta.type.entity.name,ts.meta.class.inherited-class,tsx.meta.type.support,tsx.meta.type.entity.name,tsx.meta.class.inherited-class,type-declaration,enum-declaration", "settings": { "foreground": "#7A9EC2" } }, { "name": "TypeScript Method Declaration", "scope": "function-declaration,method-declaration,method-overload-declaration,type-fn-type-parameters", "settings": { "foreground": "#FFC66D" } }, { "name": "Documentation Block", "scope": "comment.block.documentation", "settings": { "foreground": "#6A8759" } }, { "name": "Documentation Highlight (JSDoc)", "scope": "storage.type.class.jsdoc", "settings": { "foreground": "#CC8242" } }, { "name": "Import-Export-All (*) Keyword", "scope": "constant.language.import-export-all", "settings": { "foreground": "#CCCCCC" } }, { "name": "Object Key Seperator", "scope": "objectliteral.key.separator, punctuation.separator.key-value", "settings": { "foreground": "#CCCCCC" } }, { "name": "Regex", "scope": "regex", "settings": { "fontStyle": " italic" } }, { "name": "Typescript Namespace", "scope": "ts.meta.entity.name.namespace,tsx.meta.entity.name.namespace", "settings": { "foreground": "#CCCCCC" } }, { "name": "Regex Character-class", "scope": "regex.character-class", "settings": { "foreground": "#CCCCCC" } }, { "name": "Class Name", "scope": "entity.name.type.class", "settings": { "foreground": "#A9B7C6" } }, { "name": "Class Inheritances", "scope": "entity.other.inherited-class", "settings": { "foreground": "#7A9EC2" } }, { "name": "Documentation Entity", "scope": "entity.name.type.instance.jsdoc", "settings": { "foreground": "#FFC66D" } }, { "name": "YAML entity", "scope": "yaml.entity.name,yaml.string.entity.name", "settings": { "foreground": "#CC8242" } }, { "name": "YAML string value", "scope": "yaml.string.out", "settings": { "foreground": "#CCCCCC" } }, { "name": "Ignored (Exceptions Rules)", "scope": "meta.brace.square.ts,block.support.module,block.support.type.module,block.support.function.variable,punctuation.definition.typeparameters.begin,punctuation.definition.typeparameters.end", "settings": { "foreground": "#CCCCCC" } }, { "name": "Regex", "scope": "string.regexp", "settings": { "foreground": "#CC8242" } }, { "name": "Regex Group/Set", "scope": "punctuation.definition.group.regexp,punctuation.definition.character-class.regexp", "settings": { "foreground": "#FFC66D" } }, { "name": "Regex Character Class", "scope": "constant.other.character-class.regexp, constant.character.escape.ts", "settings": { "foreground": "#CCCCCC" } }, { "name": "Regex Or Operator", "scope": "expr.regex.or.operator", "settings": { "foreground": "#CCCCCC" } }, { "name": "Tag string", "scope": "string.template.tag,string.template.punctuation.tag,string.quoted.punctuation.tag,string.quoted.embedded.tag, string.quoted.double.tag", "settings": { "foreground": "#6A8759" } }, { "name": "Tag function parenthesis", "scope": "tag.punctuation.begin.arrow.parameters.embedded,tag.punctuation.end.arrow.parameters.embedded", "settings": { "foreground": "#CCCCCC" } }, { "name": "Object-literal key class", "scope": "object-literal.object.member.key.field.other,object-literal.object.member.key.accessor,object-literal.object.member.key.array.brace.square", "settings": { "foreground": "#CCCCCC" } }, { "name": "CSS Property-value", "scope": "property-list.property-value,property-list.constant", "settings": { "foreground": "#A5C261" } }, { "name": "CSS Property variable", "scope": "support.type.property-name.variable.css,support.type.property-name.variable.scss,variable.scss", "settings": { "foreground": "#7A9EC2" } }, { "name": "CSS Property entity", "scope": "entity.other.attribute-name.class.css,entity.other.attribute-name.class.scss,entity.other.attribute-name.parent-selector-suffix.css,entity.other.attribute-name.parent-selector-suffix.scss", "settings": { "foreground": "#FFC66D" } }, { "name": "CSS Property-value", "scope": "property-list.property-value.rgb-value, keyword.other.unit.css,keyword.other.unit.scss", "settings": { "foreground": "#7A9EC2" } }, { "name": "CSS Property-value function", "scope": "property-list.property-value.function", "settings": { "foreground": "#FFC66D" } }, { "name": "CSS constant variables", "scope": "support.constant.property-value.css,support.constant.property-value.scss", "settings": { "foreground": "#A5C261" } }, { "name": "CSS Tag", "scope": "css.entity.name.tag,scss.entity.name.tag", "settings": { "foreground": "#CC8242" } }, { "name": "CSS ID, Selector", "scope": "meta.selector.css, entity.attribute-name.id, entity.other.attribute-name.pseudo-class.css,entity.other.attribute-name.pseudo-element.css", "settings": { "foreground": "#FFC66D" } }, { "name": "CSS Keyword", "scope": "keyword.scss,keyword.css", "settings": { "foreground": "#CC8242" } }, { "name": "Triple-slash Directive Tag", "scope": "triple-slash.tag", "settings": { "foreground": "#CCCCCC", "fontStyle": "italic" } }, { "scope": "token.info-token", "settings": { "foreground": "#6796e6" } }, { "scope": "token.warn-token", "settings": { "foreground": "#cd9731" } }, { "scope": "token.error-token", "settings": { "foreground": "#f44747" } }, { "scope": "token.debug-token", "settings": { "foreground": "#b267e6" } }, { "name": "Python operators", "scope": "keyword.operator.logical.python", "settings": { "foreground": "#CC8242" } }, { "name": "Dart class type", "scope": "support.class.dart", "settings": { "foreground": "#CC8242" } }, { "name": "PHP variables", "scope": [ "variable.language.php", "variable.other.php" ], "settings": { "foreground": "#9E7BB0" } }, { "name": "Perl specific", "scope": [ "variable.other.readwrite.perl" ], "settings": { "foreground": "#9E7BB0" } }, { "name": "PHP variables", "scope": [ "variable.other.property.php" ], "settings": { "foreground": "#CC8242" } }, { "name": "PHP variables", "scope": [ "support.variable.property.php" ], "settings": { "foreground": "#FFC66D" } }, { "name": "XML Namespace prefix", "scope": "entity.name.tag.namesapce.xml", "settings": { "foreground": "#9876AA" } } ] } ================================================ FILE: app/src/main/assets/editor_themes/light.tmTheme ================================================ author Martin Kühl comment Based on the Quiet Light theme for Espresso by Ian Beck. name Quiet Light settings settings background #F5F5F5 caret #000000 foreground #333333 invisibles #AAAAAA lineHighlight #E4F6D4 selection #C9D0D9 name Comments scope comment, punctuation.definition.comment settings fontStyle italic foreground #AAAAAA name Comments: Preprocessor scope comment.block.preprocessor settings fontStyle foreground #AAAAAA name Comments: Documentation scope comment.documentation, comment.block.documentation settings foreground #448C27 name Invalid - Deprecated scope invalid.deprecated settings background #96000014 name Invalid - Illegal scope invalid.illegal settings background #96000014 foreground #660000 name Operators scope keyword.operator settings foreground #777777 name Keywords scope keyword, storage settings foreground #4B83CD name Types scope storage.type, support.type settings foreground #7A3E9D name Language Constants scope constant.language, support.constant, variable.language settings foreground #AB6526 name Variables scope variable, support.variable settings foreground #7A3E9D name Functions scope entity.name.function, support.function settings fontStyle bold foreground #AA3731 name Classes scope entity.name.type, entity.other.inherited-class, support.class settings fontStyle bold foreground #7A3E9D name Exceptions scope entity.name.exception settings foreground #660000 name Sections scope entity.name.section settings fontStyle bold name Numbers, Characters scope constant.numeric, constant.character, constant settings foreground #AB6526 name Strings scope string settings foreground #448C27 name Strings: Escape Sequences scope constant.character.escape settings foreground #777777 name Strings: Regular Expressions scope string.regexp settings foreground #4B83CD name Strings: Symbols scope constant.other.symbol settings foreground #AB6526 name Punctuation scope punctuation settings foreground #777777 name Embedded Source scope string source, text source settings background #EAEBE6 name ----------------------------------- settings name HTML: Doctype Declaration scope meta.tag.sgml.doctype, meta.tag.sgml.doctype string, meta.tag.sgml.doctype entity.name.tag, meta.tag.sgml punctuation.definition.tag.html settings foreground #AAAAAA name HTML: Tags scope meta.tag, punctuation.definition.tag.html, punctuation.definition.tag.begin.html, punctuation.definition.tag.end.html settings foreground #91B3E0 name HTML: Tag Names scope entity.name.tag settings foreground #4B83CD name HTML: Attribute Names scope meta.tag entity.other.attribute-name, entity.other.attribute-name.html settings fontStyle italic foreground #91B3E0 name HTML: Entities scope constant.character.entity, punctuation.definition.entity settings foreground #AB6526 name ----------------------------------- settings name CSS: Selectors scope meta.selector, meta.selector entity, meta.selector entity punctuation, entity.name.tag.css settings foreground #7A3E9D name CSS: Property Names scope meta.property-name, support.type.property-name settings foreground #AB6526 name CSS: Property Values scope meta.property-value, meta.property-value constant.other, support.constant.property-value settings foreground #448C27 name CSS: Important Keyword scope keyword.other.important settings fontStyle bold name ----------------------------------- settings name Markup: Changed scope markup.changed settings background #FFFFDD foreground #000000 name Markup: Deletion scope markup.deleted settings background #FFDDDD foreground #000000 name Markup: Emphasis scope markup.italic settings fontStyle italic name Markup: Error scope markup.error settings background #96000014 foreground #660000 name Markup: Insertion scope markup.inserted settings background #DDFFDD foreground #000000 name Markup: Link scope meta.link settings foreground #4B83CD name Markup: Output scope markup.output, markup.raw settings foreground #777777 name Markup: Prompt scope markup.prompt settings foreground #777777 name Markup: Heading scope markup.heading settings foreground #AA3731 name Markup: Strong scope markup.bold settings fontStyle bold name Markup: Traceback scope markup.traceback settings foreground #660000 name Markup: Underline scope markup.underline settings fontStyle underline name Markup Quote scope markup.quote settings foreground #7A3E9D name Markup Lists scope markup.list settings foreground #4B83CD name Markup Styling scope markup.bold, markup.italic settings foreground #448C27 name Markup Inline scope markup.inline.raw settings fontStyle foreground #AB6526 name ----------------------------------- settings name Extra: Diff Range scope meta.diff.range, meta.diff.index, meta.separator settings background #DDDDFF foreground #434343 name Extra: Diff From scope meta.diff.header.from-file settings background #FFDDDD foreground #434343 name Extra: Diff To scope meta.diff.header.to-file settings background #DDFFDD foreground #434343 uuid 231D6A91-5FD1-4CBE-BD2A-0F36C08693F1 ================================================ FILE: app/src/main/assets/languages/java/language-configuration.json ================================================ { "comments": { "lineComment": "//", "blockComment": [ "/*", "*/" ] }, "brackets": [ ["{", "}"], ["[", "]"], ["(", ")"] ], "autoClosingPairs": [ ["{", "}"], ["[", "]"], ["(", ")"], { "open": "\"", "close": "\"", "notIn": ["string"] }, { "open": "'", "close": "'", "notIn": ["string"] }, { "open": "/**", "close": " */", "notIn": ["string"] } ], "surroundingPairs": [ ["{", "}"], ["[", "]"], ["(", ")"], ["\"", "\""], ["'", "'"], ["<", ">"] ], "folding": { "markers": { "start": "^\\s*//\\s*(?:(?:#?region\\b)|(?:))" } } } ================================================ FILE: app/src/main/assets/languages/java/tmLanguage.json ================================================ { "information_for_contributors": [ "This file has been converted from https://github.com/atom/language-java/blob/master/grammars/java.cson", "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], "version": "https://github.com/atom/language-java/commit/29f977dc42a7e2568b39bb6fb34c4ef108eb59b3", "name": "Java", "scopeName": "lngpck.source.java", "patterns": [ { "begin": "\\b(package)\\b\\s*", "beginCaptures": { "1": { "name": "keyword.other.package.java" } }, "end": "\\s*(;)", "endCaptures": { "1": { "name": "punctuation.terminator.java" } }, "name": "meta.package.java", "contentName": "storage.modifier.package.java", "patterns": [ { "include": "#comments" }, { "match": "(?<=\\.)\\s*\\.|\\.(?=\\s*;)", "name": "invalid.illegal.character_not_allowed_here.java" }, { "match": "(?", "endCaptures": { "0": { "name": "punctuation.bracket.angle.java" } }, "patterns": [ { "match": "\\b(extends|super)\\b", "name": "storage.modifier.$1.java" }, { "match": "(?>>?|~|\\^)", "name": "keyword.operator.bitwise.java" }, { "match": "((&|\\^|\\||<<|>>>?)=)", "name": "keyword.operator.assignment.bitwise.java" }, { "match": "(===?|!=|<=|>=|<>|<|>)", "name": "keyword.operator.comparison.java" }, { "match": "([+*/%-]=)", "name": "keyword.operator.assignment.arithmetic.java" }, { "match": "(=)", "name": "keyword.operator.assignment.java" }, { "match": "(\\-\\-|\\+\\+)", "name": "keyword.operator.increment-decrement.java" }, { "match": "(\\-|\\+|\\*|\\/|%)", "name": "keyword.operator.arithmetic.java" }, { "match": "(!|&&|\\|\\|)", "name": "keyword.operator.logical.java" }, { "match": "(\\||&)", "name": "keyword.operator.bitwise.java" }, { "match": "\\b(const|goto)\\b", "name": "keyword.reserved.java" } ] }, "lambda-expression": { "patterns": [ { "match": "->", "name": "storage.type.function.arrow.java" } ] }, "member-variables": { "begin": "(?=private|protected|public|native|synchronized|abstract|threadsafe|transient|static|final)", "end": "(?=\\=|;)", "patterns": [ { "include": "#storage-modifiers" }, { "include": "#variables" }, { "include": "#primitive-arrays" }, { "include": "#object-types" } ] }, "method-call": { "begin": "(\\.)\\s*([A-Za-z_$][\\w$]*)\\s*(\\()", "beginCaptures": { "1": { "name": "punctuation.separator.period.java" }, "2": { "name": "entity.name.function.java" }, "3": { "name": "punctuation.definition.parameters.begin.bracket.round.java" } }, "end": "\\)", "endCaptures": { "0": { "name": "punctuation.definition.parameters.end.bracket.round.java" } }, "name": "meta.method-call.java", "patterns": [ { "include": "#code" } ] }, "methods": { "begin": "(?!new)(?=[\\w<].*\\s+)(?=([^=/]|/(?!/))+\\()", "end": "(})|(?=;)", "endCaptures": { "1": { "name": "punctuation.section.method.end.bracket.curly.java" } }, "name": "meta.method.java", "patterns": [ { "include": "#storage-modifiers" }, { "begin": "(\\w+)\\s*(\\()", "beginCaptures": { "1": { "name": "entity.name.function.java" }, "2": { "name": "punctuation.definition.parameters.begin.bracket.round.java" } }, "end": "\\)", "endCaptures": { "0": { "name": "punctuation.definition.parameters.end.bracket.round.java" } }, "name": "meta.method.identifier.java", "patterns": [ { "include": "#parameters" }, { "include": "#parens" }, { "include": "#comments" } ] }, { "include": "#generics" }, { "begin": "(?=\\w.*\\s+\\w+\\s*\\()", "end": "(?=\\s+\\w+\\s*\\()", "name": "meta.method.return-type.java", "patterns": [ { "include": "#all-types" }, { "include": "#parens" }, { "include": "#comments" } ] }, { "include": "#throws" }, { "begin": "{", "beginCaptures": { "0": { "name": "punctuation.section.method.begin.bracket.curly.java" } }, "end": "(?=})", "contentName": "meta.method.body.java", "patterns": [ { "include": "#code" } ] }, { "include": "#comments" } ] }, "module": { "begin": "((open)\\s)?(module)\\s+(\\w+)", "end": "}", "beginCaptures": { "1": { "name": "storage.modifier.java" }, "3": { "name": "storage.modifier.java" }, "4": { "name": "entity.name.type.module.java" } }, "endCaptures": { "0": { "name": "punctuation.section.module.end.bracket.curly.java" } }, "name": "meta.module.java", "patterns": [ { "begin": "{", "beginCaptures": { "0": { "name": "punctuation.section.module.begin.bracket.curly.java" } }, "end": "(?=})", "contentName": "meta.module.body.java", "patterns": [ { "include": "#comments" }, { "include": "#comments-javadoc" }, { "match": "\\b(requires|transitive|exports|opens|to|uses|provides|with)\\b", "name": "keyword.module.java" } ] } ] }, "numbers": { "patterns": [ { "match": "(?x)\n\\b(?)?(\\()", "beginCaptures": { "1": { "name": "storage.modifier.java" }, "2": { "name": "entity.name.type.record.java" }, "3": { "patterns": [ { "include": "#generics" } ] }, "4": { "name": "punctuation.definition.parameters.begin.bracket.round.java" } }, "end": "\\)", "endCaptures": { "0": { "name": "punctuation.definition.parameters.end.bracket.round.java" } }, "name": "meta.record.identifier.java", "patterns": [ { "include": "#code" } ] }, { "begin": "(implements)\\s", "beginCaptures": { "1": { "name": "storage.modifier.implements.java" } }, "end": "(?=\\s*\\{)", "name": "meta.definition.class.implemented.interfaces.java", "patterns": [ { "include": "#object-types-inherited" }, { "include": "#comments" } ] }, { "include": "#record-body" } ] }, "record-body": { "begin": "{", "beginCaptures": { "0": { "name": "punctuation.section.class.begin.bracket.curly.java" } }, "end": "(?=})", "name": "meta.record.body.java", "patterns": [ { "include": "#record-constructor" }, { "include": "#class-body" } ] }, "record-constructor": { "begin": "(?!new)(?=[\\w<].*\\s+)(?=([^\\(=/]|/(?!/))+(?={))", "end": "(})|(?=;)", "endCaptures": { "1": { "name": "punctuation.section.method.end.bracket.curly.java" } }, "name": "meta.method.java", "patterns": [ { "include": "#storage-modifiers" }, { "begin": "(\\w+)", "beginCaptures": { "1": { "name": "entity.name.function.java" } }, "end": "(?=\\s*{)", "name": "meta.method.identifier.java", "patterns": [ { "include": "#comments" } ] }, { "include": "#comments" }, { "begin": "{", "beginCaptures": { "0": { "name": "punctuation.section.method.begin.bracket.curly.java" } }, "end": "(?=})", "contentName": "meta.method.body.java", "patterns": [ { "include": "#code" } ] } ] }, "static-initializer": { "patterns": [ { "include": "#anonymous-block-and-instance-initializer" }, { "match": "static", "name": "storage.modifier.java" } ] }, "storage-modifiers": { "match": "\\b(public|private|protected|static|final|native|synchronized|abstract|threadsafe|transient|volatile|default|strictfp|sealed|non-sealed)\\b", "name": "storage.modifier.java" }, "strings": { "patterns": [ { "begin": "\"", "beginCaptures": { "0": { "name": "punctuation.definition.string.begin.java" } }, "end": "\"", "endCaptures": { "0": { "name": "punctuation.definition.string.end.java" } }, "name": "string.quoted.double.java", "patterns": [ { "match": "\\\\.", "name": "constant.character.escape.java" } ] }, { "begin": "'", "beginCaptures": { "0": { "name": "punctuation.definition.string.begin.java" } }, "end": "'", "endCaptures": { "0": { "name": "punctuation.definition.string.end.java" } }, "name": "string.quoted.single.java", "patterns": [ { "match": "\\\\.", "name": "constant.character.escape.java" } ] } ] }, "throws": { "begin": "throws", "beginCaptures": { "0": { "name": "storage.modifier.java" } }, "end": "(?={|;)", "name": "meta.throwables.java", "patterns": [ { "match": ",", "name": "punctuation.separator.delimiter.java" }, { "match": "[a-zA-Z$_][\\.a-zA-Z0-9$_]*", "name": "storage.type.java" } ] }, "try-catch-finally": { "patterns": [ { "begin": "\\btry\\b", "beginCaptures": { "0": { "name": "keyword.control.try.java" } }, "end": "}", "endCaptures": { "0": { "name": "punctuation.section.try.end.bracket.curly.java" } }, "name": "meta.try.java", "patterns": [ { "begin": "\\(", "beginCaptures": { "0": { "name": "punctuation.section.try.resources.begin.bracket.round.java" } }, "end": "\\)", "endCaptures": { "0": { "name": "punctuation.section.try.resources.end.bracket.round.java" } }, "name": "meta.try.resources.java", "patterns": [ { "include": "#code" } ] }, { "begin": "{", "beginCaptures": { "0": { "name": "punctuation.section.try.begin.bracket.curly.java" } }, "end": "(?=})", "contentName": "meta.try.body.java", "patterns": [ { "include": "#code" } ] } ] }, { "begin": "\\b(catch)\\b", "beginCaptures": { "1": { "name": "keyword.control.catch.java" } }, "end": "}", "endCaptures": { "0": { "name": "punctuation.section.catch.end.bracket.curly.java" } }, "name": "meta.catch.java", "patterns": [ { "include": "#comments" }, { "begin": "\\(", "beginCaptures": { "0": { "name": "punctuation.definition.parameters.begin.bracket.round.java" } }, "end": "\\)", "endCaptures": { "0": { "name": "punctuation.definition.parameters.end.bracket.round.java" } }, "contentName": "meta.catch.parameters.java", "patterns": [ { "include": "#comments" }, { "include": "#storage-modifiers" }, { "begin": "[a-zA-Z$_][\\.a-zA-Z0-9$_]*", "beginCaptures": { "0": { "name": "storage.type.java" } }, "end": "(\\|)|(?=\\))", "endCaptures": { "1": { "name": "punctuation.catch.separator.java" } }, "patterns": [ { "include": "#comments" }, { "match": "\\w+", "captures": { "0": { "name": "variable.parameter.java" } } } ] } ] }, { "begin": "{", "beginCaptures": { "0": { "name": "punctuation.section.catch.begin.bracket.curly.java" } }, "end": "(?=})", "contentName": "meta.catch.body.java", "patterns": [ { "include": "#code" } ] } ] }, { "begin": "\\bfinally\\b", "beginCaptures": { "0": { "name": "keyword.control.finally.java" } }, "end": "}", "endCaptures": { "0": { "name": "punctuation.section.finally.end.bracket.curly.java" } }, "name": "meta.finally.java", "patterns": [ { "begin": "{", "beginCaptures": { "0": { "name": "punctuation.section.finally.begin.bracket.curly.java" } }, "end": "(?=})", "contentName": "meta.finally.body.java", "patterns": [ { "include": "#code" } ] } ] } ] }, "variables": { "begin": "(?x)\n(?=\n \\b\n (\n (void|boolean|byte|char|short|int|float|long|double)\n |\n (?>(\\w+\\.)*[A-Z_]+\\w*) # e.g. `javax.ws.rs.Response`, or `String`\n )\n \\b\n \\s*\n (\n <[\\w<>,\\.?\\s\\[\\]]*> # e.g. `HashMap`, or `List`\n )?\n \\s*\n (\n (\\[\\])* # int[][]\n )?\n \\s+\n [A-Za-z_$][\\w$]* # At least one identifier after space\n ([\\w\\[\\],$][\\w\\[\\],\\s]*)? # possibly primitive array or additional identifiers\n \\s*(=|:|;)\n)", "end": "(?=\\=|:|;)", "name": "meta.definition.variable.java", "patterns": [ { "match": "([A-Za-z$_][\\w$]*)(?=\\s*(\\[\\])*\\s*(;|:|=|,))", "captures": { "1": { "name": "variable.other.definition.java" } } }, { "include": "#all-types" }, { "include": "#code" } ] }, "variables-local": { "begin": "(?=\\b(var)\\b\\s+[A-Za-z_$][\\w$]*\\s*(=|:|;))", "end": "(?=\\=|:|;)", "name": "meta.definition.variable.local.java", "patterns": [ { "match": "\\bvar\\b", "name": "storage.type.local.java" }, { "match": "([A-Za-z$_][\\w$]*)(?=\\s*(\\[\\])*\\s*(=|:|;))", "captures": { "1": { "name": "variable.other.definition.java" } } }, { "include": "#code" } ] } } } ================================================ FILE: app/src/main/assets/languages/json/language-configuration.json ================================================ { "brackets": [ ["{", "}"], ["[", "]"] ], "autoClosingPairs": [ ["{", "}"], ["[", "]"], { "open": "\"", "close": "\"", "notIn": ["string"] }, { "open": "'", "close": "'", "notIn": ["string"] }, ], "surroundingPairs": [ ["{", "}"], ["[", "]"], ["\"", "\""], ], "folding": { "markers": { "start": "^\\s*//\\s*(?:(?:#?region\\b)|(?:))" } } } ================================================ FILE: app/src/main/assets/languages/json/tmLanguage.json ================================================ { "scopeName": "source.json", "name": "JSON", "fileTypes": [ "avsc", "babelrc", "bowerrc", "composer.lock", "geojson", "gltf", "htmlhintrc", "ipynb", "jscsrc", "jshintrc", "jslintrc", "json", "jsonl", "jsonld", "languagebabel", "ldj", "ldjson", "Pipfile.lock", "schema", "stylintrc", "template", "tern-config", "tern-project", "tfstate", "tfstate.backup", "topojson", "webapp", "webmanifest" ], "patterns": [ { "include": "#value" } ], "repository": { "array": { "begin": "\\[", "beginCaptures": { "0": { "name": "punctuation.definition.array.begin.json" } }, "end": "(,)?[\\s\\n]*(\\])", "endCaptures": { "1": { "name": "invalid.illegal.trailing-array-separator.json" }, "2": { "name": "punctuation.definition.array.end.json" } }, "name": "meta.structure.array.json", "patterns": [ { "include": "#value" }, { "match": ",", "name": "punctuation.separator.array.json" }, { "match": "[^\\s\\]]", "name": "invalid.illegal.expected-array-separator.json" } ] }, "constant": { "match": "\\b(true|false|null)\\b", "name": "constant.language.json" }, "number": { "match": "-?(?=[1-9]|0(?!\\d))\\d+(\\.\\d+)?([eE][+-]?\\d+)?", "name": "constant.numeric.json" }, "object": { "begin": "{", "beginCaptures": { "0": { "name": "punctuation.definition.dictionary.begin.json" } }, "end": "}", "endCaptures": { "0": { "name": "punctuation.definition.dictionary.end.json" } }, "name": "meta.structure.dictionary.json", "patterns": [ { "begin": "(?=\")", "end": "(?<=\")", "name": "meta.structure.dictionary.key.json", "patterns": [ { "include": "#string" } ] }, { "begin": ":", "beginCaptures": { "0": { "name": "punctuation.separator.dictionary.key-value.json" } }, "end": "(,)(?=[\\s\\n]*})|(,)|(?=})", "endCaptures": { "1": { "name": "invalid.illegal.trailing-dictionary-separator.json" }, "2": { "name": "punctuation.separator.dictionary.pair.json" } }, "name": "meta.structure.dictionary.value.json", "patterns": [ { "include": "#value" }, { "match": "[^\\s,]", "name": "invalid.illegal.expected-dictionary-separator.json" } ] }, { "match": "[^\\s}]", "name": "invalid.illegal.expected-dictionary-separator.json" } ] }, "string": { "begin": "\"", "beginCaptures": { "0": { "name": "punctuation.definition.string.begin.json" } }, "end": "\"", "endCaptures": { "0": { "name": "punctuation.definition.string.end.json" } }, "name": "string.quoted.double.json", "patterns": [ { "match": "(?x)\n\\\\ # a literal backslash\n( # followed by\n [\"\\\\/bfnrt] # one of these characters\n | # or\n u[0-9a-fA-F]{4} # a u and four hex digits\n)", "name": "constant.character.escape.json" }, { "match": "\\\\.", "name": "invalid.illegal.unrecognized-string-escape.json" } ] }, "value": { "patterns": [ { "include": "#constant" }, { "include": "#number" }, { "include": "#string" }, { "include": "#array" }, { "include": "#object" } ] } } } ================================================ FILE: app/src/main/assets/languages/kotlin/language-configuration.json ================================================ { "comments": { "lineComment": "//", "blockComment": [ "/*", "*/" ] }, "brackets": [ ["{", "}"], ["[", "]"], ["(", ")"], ["<", ">"] ], "autoClosingPairs": [ { "open": "{", "close": "}" }, { "open": "[", "close": "]" }, { "open": "(", "close": ")" }, { "open": "'", "close": "'", "notIn": ["string", "comment"] }, { "open": "\"", "close": "\"", "notIn": ["string"] }, { "open": "/*", "close": " */", "notIn": ["string"] } ], "surroundingPairs": [ ["{", "}"], ["[", "]"], ["(", ")"], ["<", ">"], ["'", "'"], ["\"", "\""] ], "folding": { "offSide": false, "markers": { "start": "^\\s*//\\s*#region", "end": "^\\s*//\\s*#endregion" } } } ================================================ FILE: app/src/main/assets/languages/kotlin/tmLanguage.json ================================================ { "name": "Kotlin", "scopeName": "source.kotlin", "fileTypes": [ "kt", "kts" ], "patterns": [ { "include": "#comments" }, { "captures": { "1": { "name": "keyword.other.kotlin" }, "2": { "name": "entity.name.package.kotlin" } }, "match": "^\\s*(package)\\b(?:\\s*([^ ;$]+)\\s*)?" }, { "include": "#imports" }, { "include": "#statements" } ], "repository": { "statements": { "patterns": [ { "include": "#namespaces" }, { "include": "#typedefs" }, { "include": "#classes" }, { "include": "#functions" }, { "include": "#variables" }, { "include": "#getters-and-setters" }, { "include": "#expressions" } ] }, "comments": { "patterns": [ { "begin": "/\\*", "captures": { "0": { "name": "punctuation.definition.comment.kotlin" } }, "end": "\\*/", "name": "comment.block.kotlin", "patterns": [ { "include": "#comments" } ] }, { "captures": { "1": { "name": "comment.line.double-slash.kotlin" }, "2": { "name": "punctuation.definition.comment.kotlin" } }, "match": "\\s*((//).*$\\n?)" } ] }, "imports": { "patterns": [ { "captures": { "1": { "name": "keyword.other.kotlin" }, "2": { "name": "keyword.other.kotlin" } }, "match": "^\\s*(import)\\s+[^ $]+\\s+(as)?" } ] }, "namespaces": { "patterns": [ { "match": "\\b(namespace)\\b", "name": "keyword.other.kotlin" }, { "begin": "\\{", "end": "\\}", "patterns": [ { "include": "#statements" } ] } ] }, "types": { "patterns": [ { "match": "\\b(Any|Unit|String|Int|Boolean|Char|Long|Double|Float|Short|Byte|dynamic)\\b", "name": "storage.type.buildin.kotlin" }, { "match": "\\b(IntArray|BooleanArray|CharArray|LongArray|DoubleArray|FloatArray|ShortArray|ByteArray)\\b", "name": "storage.type.buildin.array.kotlin" }, { "begin": "\\b(Array|List|Map)<\\b", "beginCaptures": { "1": { "name": "storage.type.buildin.collection.kotlin" } }, "end": ">", "patterns": [ { "include": "#types" }, { "include": "#keywords" } ] }, { "begin": "\\w+<", "end": ">", "patterns": [ { "include": "#types" }, { "include": "#keywords" } ] }, { "begin": "(#)\\(", "beginCaptures": { "1": { "name": "keyword.operator.tuple.kotlin" } }, "end": "\\)", "patterns": [ { "include": "#expressions" } ] }, { "begin": "\\{", "end": "\\}", "patterns": [ { "include": "#statements" } ] }, { "begin": "\\(", "end": "\\)", "patterns": [ { "include": "#types" } ] }, { "match": "(->)", "name": "keyword.operator.declaration.kotlin" } ] }, "generics": { "patterns": [ { "begin": "(:)", "beginCaptures": { "1": { "name": "keyword.operator.declaration.kotlin" } }, "end": "(?=,|>)", "patterns": [ { "include": "#types" } ] }, { "include": "#keywords" }, { "match": "\\w+", "name": "storage.type.generic.kotlin" } ] }, "typedefs": { "begin": "(?=\\s*(?:type))", "end": "(?=$)", "patterns": [ { "match": "\\b(type)\\b", "name": "keyword.other.kotlin" }, { "begin": "<", "end": ">", "patterns": [ { "include": "#generics" } ] }, { "include": "#expressions" } ] }, "classes": { "begin": "(?=\\s*(?:companion|class|object|interface))", "end": "}|(?=$)", "patterns": [ { "begin": "\\b(companion\\s*)?(class|object|interface)\\b", "beginCaptures": { "1": { "name": "keyword.other.kotlin" } }, "end": "(?=<|{|\\(|:)", "patterns": [ { "match": "\\b(object)\\b", "name": "keyword.other.kotlin" }, { "match": "\\w+", "name": "entity.name.type.class.kotlin" } ] }, { "begin": "<", "end": ">", "patterns": [ { "include": "#generics" } ] }, { "begin": "\\(", "end": "\\)", "patterns": [ { "include": "#parameters" } ] }, { "begin": "(:)", "beginCaptures": { "1": { "name": "keyword.operator.declaration.kotlin" } }, "end": "(?={|$)", "patterns": [ { "match": "\\w+", "name": "entity.other.inherited-class.kotlin" }, { "begin": "\\(", "end": "\\)", "patterns": [ { "include": "#expressions" } ] } ] }, { "begin": "\\{", "end": "\\}", "patterns": [ { "include": "#statements" } ] } ] }, "variables": { "begin": "(?=\\s*(?:var|val))", "end": "(?=:|=|$)", "patterns": [ { "begin": "\\b(var|val)\\b", "beginCaptures": { "1": { "name": "keyword.other.kotlin" } }, "end": "(?=:|=|$)", "patterns": [ { "begin": "<", "end": ">", "patterns": [ { "include": "#generics" } ] }, { "captures": { "2": { "name": "entity.name.variable.kotlin" } }, "match": "([\\.<\\?>\\w]+\\.)?(\\w+)" } ] }, { "begin": "(:)", "beginCaptures": { "1": { "name": "keyword.operator.declaration.kotlin" } }, "end": "(?==|$)", "patterns": [ { "include": "#types" }, { "include": "#getters-and-setters" } ] }, { "begin": "(=)", "beginCaptures": { "1": { "name": "keyword.operator.assignment.kotlin" } }, "end": "(?=$)", "patterns": [ { "include": "#expressions" }, { "include": "#getters-and-setters" } ] } ] }, "getters-and-setters": { "patterns": [ { "begin": "\\b(get)\\b\\s*\\(\\s*\\)", "beginCaptures": { "1": { "name": "entity.name.function.kotlin" } }, "end": "\\}|(?=\\bset\\b)|$", "patterns": [ { "begin": "(=)", "beginCaptures": { "1": { "name": "keyword.operator.assignment.kotlin" } }, "end": "(?=$|\\bset\\b)", "patterns": [ { "include": "#expressions" } ] }, { "begin": "\\{", "end": "\\}", "patterns": [ { "include": "#expressions" } ] } ] }, { "begin": "\\b(set)\\b\\s*(?=\\()", "beginCaptures": { "1": { "name": "entity.name.function.kotlin" } }, "end": "\\}|(?=\\bget\\b)|$", "patterns": [ { "begin": "\\(", "end": "\\)", "patterns": [ { "include": "#parameters" } ] }, { "begin": "(=)", "beginCaptures": { "1": { "name": "keyword.operator.assignment.kotlin" } }, "end": "(?=$|\\bset\\b)", "patterns": [ { "include": "#expressions" } ] }, { "begin": "\\{", "end": "\\}", "patterns": [ { "include": "#expressions" } ] } ] } ] }, "functions": { "begin": "(?=\\s*(?:fun))", "end": "}|(?=$)", "patterns": [ { "begin": "\\b(fun)\\b", "beginCaptures": { "1": { "name": "keyword.other.kotlin" } }, "end": "(?=\\()", "patterns": [ { "begin": "<", "end": ">", "patterns": [ { "include": "#generics" } ] }, { "captures": { "2": { "name": "entity.name.function.kotlin" } }, "match": "([\\.<\\?>\\w]+\\.)?(\\w+)" } ] }, { "begin": "\\(", "end": "\\)", "patterns": [ { "include": "#parameters" } ] }, { "begin": "(:)", "beginCaptures": { "1": { "name": "keyword.operator.declaration.kotlin" } }, "end": "(?={|=|$)", "patterns": [ { "include": "#types" } ] }, { "begin": "\\{", "end": "(?=\\})", "patterns": [ { "include": "#statements" } ] }, { "begin": "(=)", "beginCaptures": { "1": { "name": "keyword.operator.assignment.kotlin" } }, "end": "(?=$)", "patterns": [ { "include": "#expressions" } ] } ] }, "parameters": { "patterns": [ { "begin": "(:)", "beginCaptures": { "1": { "name": "keyword.operator.declaration.kotlin" } }, "end": "(?=,|\\)|=)", "patterns": [ { "include": "#types" } ] }, { "begin": "(=)", "beginCaptures": { "1": { "name": "keyword.operator.declaration.kotlin" } }, "end": "(?=,|\\))", "patterns": [ { "include": "#expressions" } ] }, { "include": "#keywords" }, { "match": "\\w+", "name": "variable.parameter.function.kotlin" } ] }, "expressions": { "patterns": [ { "begin": "\\(", "end": "\\)", "patterns": [ { "include": "#expressions" } ] }, { "include": "#types" }, { "include": "#strings" }, { "include": "#constants" }, { "include": "#comments" }, { "include": "#keywords" } ] }, "strings": { "patterns": [ { "begin": "\"\"\"", "beginCaptures": { "0": { "name": "punctuation.definition.string.begin.kotlin" } }, "end": "\"\"\"", "endCaptures": { "0": { "name": "punctuation.definition.string.end.kotlin" } }, "name": "string.quoted.third.kotlin", "patterns": [ { "match": "(\\$\\w+|\\$\\{[^\\}]+\\})", "name": "variable.parameter.template.kotlin" }, { "match": "\\\\.", "name": "constant.character.escape.kotlin" } ] }, { "begin": "\"", "beginCaptures": { "0": { "name": "punctuation.definition.string.begin.kotlin" } }, "end": "\"", "endCaptures": { "0": { "name": "punctuation.definition.string.end.kotlin" } }, "name": "string.quoted.double.kotlin", "patterns": [ { "match": "(\\$\\w+|\\$\\{[^\\}]+\\})", "name": "variable.parameter.template.kotlin" }, { "match": "\\\\.", "name": "constant.character.escape.kotlin" } ] }, { "begin": "'", "beginCaptures": { "0": { "name": "punctuation.definition.string.begin.kotlin" } }, "end": "'", "endCaptures": { "0": { "name": "punctuation.definition.string.end.kotlin" } }, "name": "string.quoted.single.kotlin", "patterns": [ { "match": "\\\\.", "name": "constant.character.escape.kotlin" } ] }, { "begin": "`", "beginCaptures": { "0": { "name": "punctuation.definition.string.begin.kotlin" } }, "end": "`", "endCaptures": { "0": { "name": "punctuation.definition.string.end.kotlin" } }, "name": "string.quoted.single.kotlin" } ] }, "constants": { "patterns": [ { "match": "\\b(true|false|null|this|super)\\b", "name": "constant.language.kotlin" }, { "match": "\\b((0(x|X)[0-9a-fA-F]*)|(([0-9]+\\.?[0-9]*)|(\\.[0-9]+))((e|E)(\\+|-)?[0-9]+)?)([LlFfUuDd]|UL|ul)?\\b", "name": "constant.numeric.kotlin" }, { "match": "\\b([A-Z][A-Z0-9_]+)\\b", "name": "constant.other.kotlin" } ] }, "keywords": { "patterns": [ { "match": "\\b(var|val|public|private|protected|abstract|final|enum|open|attribute|annotation|override|inline|var|val|vararg|lazy|in|out|internal|data|tailrec|operator|infix|const|yield|typealias|typeof)\\b", "name": "storage.modifier.kotlin" }, { "match": "\\b(try|catch|finally|throw)\\b", "name": "keyword.control.catch-exception.kotlin" }, { "match": "\\b(if|else|while|for|do|return|when|where|break|continue)\\b", "name": "keyword.control.kotlin" }, { "match": "\\b(in|is|as|assert)\\b", "name": "keyword.operator.kotlin" }, { "match": "(==|!=|===|!==|<=|>=|<|>)", "name": "keyword.operator.comparison.kotlin" }, { "match": "(=)", "name": "keyword.operator.assignment.kotlin" }, { "match": "(:)", "name": "keyword.operator.declaration.kotlin" }, { "match": "(\\.)", "name": "keyword.operator.dot.kotlin" }, { "match": "(\\-\\-|\\+\\+)", "name": "keyword.operator.increment-decrement.kotlin" }, { "match": "(\\+=|\\-=|\\*=|\\/=)", "name": "keyword.operator.arithmetic.assign.kotlin" }, { "match": "(\\.\\.)", "name": "keyword.operator.range.kotlin" }, { "match": "(\\-|\\+|\\*|\\/|%)", "name": "keyword.operator.arithmetic.kotlin" }, { "match": "(!|&&|\\|\\|)", "name": "keyword.operator.logical.kotlin" }, { "match": "(;)", "name": "punctuation.terminator.kotlin" } ] } } } ================================================ FILE: app/src/main/assets/languages/properties/language-configuration.json ================================================ { "comments": { "lineComment": "#" } } ================================================ FILE: app/src/main/assets/languages/properties/tmLanguage.json ================================================ { "fileTypes": [ "properties" ], "foldingStartMarker": "^[a-zA-Z0-9.-_]+=.*\\\n", "foldingStopMarker": "^(.*(?|&&|\\|\\|", "name": "keyword.operator.logical.shell" }, { "match": "(?[>=]?|==|!=|^|\\|{1,2}|&{1,2}|\\?|\\:|,|=|[*/%+\\-&^|]=|<<=|>>=", "name": "keyword.operator.arithmetic.shell" }, { "match": "0[xX][0-9A-Fa-f]+", "name": "constant.numeric.hex.shell" }, { "match": "0\\d+", "name": "constant.numeric.octal.shell" }, { "match": "\\d{1,2}#[0-9a-zA-Z@_]+", "name": "constant.numeric.other.shell" }, { "match": "\\d+", "name": "constant.numeric.integer.shell" } ] }, "pathname": { "patterns": [ { "match": "(?<=\\s|:|=|^)~", "name": "keyword.operator.tilde.shell" }, { "match": "\\*|\\?", "name": "keyword.operator.glob.shell" }, { "begin": "([?*+@!])(\\()", "beginCaptures": { "1": { "name": "keyword.operator.extglob.shell" }, "2": { "name": "punctuation.definition.extglob.shell" } }, "end": "\\)", "endCaptures": { "0": { "name": "punctuation.definition.extglob.shell" } }, "name": "meta.structure.extglob.shell", "patterns": [ { "include": "$self" } ] } ] }, "pipeline": { "patterns": [ { "match": "(?<=^|;|&|\\s)(time)(?=\\s|;|&|$)", "name": "keyword.other.shell" }, { "match": "[|!]", "name": "keyword.operator.pipe.shell" } ] }, "redirection": { "patterns": [ { "begin": "[><]\\(", "beginCaptures": { "0": { "name": "punctuation.definition.string.begin.shell" } }, "end": "\\)", "endCaptures": { "0": { "name": "punctuation.definition.string.end.shell" } }, "name": "string.interpolated.process-substitution.shell", "patterns": [ { "include": "$self" } ] }, { "match": "(?])(&>|\\d*>&\\d*|\\d*(>>|>|<)|\\d*<&|\\d*<>)(?![<>])", "name": "keyword.operator.redirect.shell" } ] }, "string": { "patterns": [ { "match": "\\\\.", "name": "constant.character.escape.shell" }, { "begin": "'", "beginCaptures": { "0": { "name": "punctuation.definition.string.begin.shell" } }, "end": "'", "endCaptures": { "0": { "name": "punctuation.definition.string.end.shell" } }, "name": "string.quoted.single.shell" }, { "begin": "\\$?\"", "beginCaptures": { "0": { "name": "punctuation.definition.string.begin.shell" } }, "end": "\"", "endCaptures": { "0": { "name": "punctuation.definition.string.end.shell" } }, "name": "string.quoted.double.shell", "patterns": [ { "match": "\\\\[\\$`\"\\\\\\n]", "name": "constant.character.escape.shell" }, { "include": "#variable" }, { "include": "#interpolation" } ] }, { "begin": "\\$'", "beginCaptures": { "0": { "name": "punctuation.definition.string.begin.shell" } }, "end": "'", "endCaptures": { "0": { "name": "punctuation.definition.string.end.shell" } }, "name": "string.quoted.single.dollar.shell", "patterns": [ { "match": "\\\\(a|b|e|f|n|r|t|v|\\\\|')", "name": "constant.character.escape.ansi-c.shell" }, { "match": "\\\\[0-9]{3}", "name": "constant.character.escape.octal.shell" }, { "match": "\\\\x[0-9a-fA-F]{2}", "name": "constant.character.escape.hex.shell" }, { "match": "\\\\c.", "name": "constant.character.escape.control-char.shell" } ] } ] }, "support": { "patterns": [ { "match": "(?<=^|;|&|\\s)(?::|\\.)(?=\\s|;|&|$)", "name": "support.function.builtin.shell" }, { "match": "(?<=^|;|&|\\s)(?:alias|bg|bind|break|builtin|caller|cd|command|compgen|complete|dirs|disown|echo|enable|eval|exec|exit|false|fc|fg|getopts|hash|help|history|jobs|kill|let|logout|popd|printf|pushd|pwd|read|readonly|set|shift|shopt|source|suspend|test|times|trap|true|type|ulimit|umask|unalias|unset|wait)(?=\\s|;|&|$)", "name": "support.function.builtin.shell" } ] }, "variable": { "patterns": [ { "captures": { "1": { "name": "punctuation.definition.variable.shell" } }, "match": "(\\$)[a-zA-Z_][a-zA-Z0-9_]*", "name": "variable.other.normal.shell" }, { "captures": { "1": { "name": "punctuation.definition.variable.shell" } }, "match": "(\\$)[-*@#?$!0_]", "name": "variable.other.special.shell" }, { "captures": { "1": { "name": "punctuation.definition.variable.shell" } }, "match": "(\\$)[1-9]", "name": "variable.other.positional.shell" }, { "begin": "\\${", "beginCaptures": { "0": { "name": "punctuation.definition.variable.shell" } }, "end": "}", "endCaptures": { "0": { "name": "punctuation.definition.variable.shell" } }, "name": "variable.other.bracket.shell", "patterns": [ { "match": "!|:[-=?]?|\\*|@|#{1,2}|%{1,2}|/", "name": "keyword.operator.expansion.shell" }, { "captures": { "1": { "name": "punctuation.section.array.shell" }, "3": { "name": "punctuation.section.array.shell" } }, "match": "(\\[)([^\\]]+)(\\])" }, { "include": "#variable" }, { "include": "#string" } ] } ] } } } ================================================ FILE: app/src/main/assets/languages/smali/language-configuration.json ================================================ { "comments": { "lineComment": "#" }, "brackets": [ ["{", "}"], ["(", ")"] ], "autoClosingPairs": [ ["{", "}"], ["(", ")"], { "open": "\"", "close": "\"", "notIn": ["string"] } ], "surroundingPairs": [ ["{", "}"], ["(", ")"], ["\"", "\""], ["<", ">"] ], "folding": { "markers": { "start": "^\\s*\\.method", "end": "^\\s*\\.end method" } } } ================================================ FILE: app/src/main/assets/languages/smali/tmLanguage.json ================================================ { "name": "Smali", "version": "https://github.com/QuinnWilton/sublime-smali/commit/36add49df8c7d8dde1d5cf0d68c4098183b2714f", "scopeName": "source.smali", "fileTypes": [ "Smali" ], "foldingStartMarker": "[\\s\\t]*\\.method", "foldingStopMarker": "[\\s\\t]*\\.end method", "patterns": [ { "include": "#annotation" }, { "include": "#annotation-end" }, { "include": "#annotation-value_list" }, { "include": "#annotation-value" }, { "include": "#annotation-name" }, { "include": "#annotation-access" }, { "include": "#comment-alone" }, { "include": "#comment-inline" }, { "include": "#field" }, { "include": "#field-end" }, { "comment": "Class name", "match": "^[\\s\\t]*(\\.class)[\\s\\t]*((?:(?:interface|public|protected|private|abstract|static|final|synchronized|transient|volatile|native|strictfp|synthetic|enum|annotation)[\\s\\t]+)*)[\\s\\t]*(L)([\\p{L}_\\$][\\p{L}\\d_\\$]*(?:\/[\\p{L}_\\$][\\p{L}\\d_\\$]*)*)(;)(?=[\\s\\t]*(#.*)?$)", "captures":{ "1": { "name": "constant.language.smali" }, "2": { "name": "variable.parameter.smali" }, "3": { "name": "entity.name.tag.smali" }, "4": { "name": "string.quoted.double.smali" }, "5": { "name": "entity.name.tag.smali" } } }, { "comment": "Super / implements class name", "match": "^[\\s\\t]*(\\.(?:super|implements))[\\s\\t]+(L)([\\p{L}_\\$][\\p{L}\\d_\\$]*(?:\/[\\p{L}_\\$][\\p{L}\\d_\\$]*)*)(;)(?=[\\s\\t]*(#.*)?$)", "captures":{ "1": { "name": "constant.language.smali" }, "2": { "name": "entity.name.tag.smali" }, "3": { "name": "string.quoted.double.smali" }, "4": { "name": "entity.name.tag.smali" } } }, { "comment": "Source file", "match": "^[\\s\\t]*(\\.source)[\\s\\t]+(\")(.*?)((?||(?:[\\$\\p{L}_\\-][\\p{L}\\d_\\$]*))\\(((?:[\\[]*(?:Z|B|S|C|I|J|F|D|L(?:[\\p{L}_\\$][\\p{L}\\d_\\$]*(?:\/[\\p{L}_\\$][\\p{L}\\d_\\$]*)*);))*)\\)(?:(V)|[\\[]*(Z|B|S|C|I|J|F|D)|[\\[]*(?:(L)([\\p{L}_\\$][\\p{L}\\d_\\$]*(?:\/[\\p{L}_\\$][\\p{L}\\d_\\$]*)*)(;)))(?=[\\s\\t]*(#.*)?$)", "beginCaptures": { "1": { "name": "constant.language.smali" }, "2": { "name": "variable.parameter.smali" }, "3": { "name": "variable.parameter.smali" }, "4": { "name": "entity.name.function.smali" }, "5": { "name": "constant.numeric.smali" }, "6": { "name": "constant.numeric.smali" }, "7": { "name": "constant.numeric.smali" }, "8": { "name": "entity.name.tag.smali" }, "9": { "name": "constant.numeric.smali" }, "10": { "name": "entity.name.tag.smali" }, "11": { "name": "constant.numeric.smali" }, "12": { "name": "entity.name.tag.smali" } }, "end": "^[\\s\\t]*(\\.end method)(?=[\\s\\t]*(#.*)?$)", "endCaptures": { "1": { "name": "constant.language.smali" } }, "patterns": [ { "include": "#comment-inline" }, { "comment": "Prologue", "name": "constant.language.smali", "match": "^[\\s\\t]*(\\.prologue)(?=[\\s\\t]*(#.*)?$)" }, { "comment": "Local", "match": "^[\\s\\t]*(\\.local)[\\s\\t]+([vp]\\d+),[\\s\\t]+(\"[\\p{L}_\\$][\\w\\$]*\"):[\\[]*(?:(?:(Z|B|S|C|I|J|F|D)|(?:(L)([\\p{L}_\\$][\\p{L}\\d_\\$]*(?:\/[\\p{L}_\\$][\\p{L}\\d_\\$]*)*)(;))))(?:,(\")(.*?)((?[\\s\\t]+(:[A-Za-z_\\d]+)(?=[\\s\\t]*(#.*)?$)", "captures": { "1": { "name": "variable.parameter.smali" }, "2": { "name": "keyword.control.smali" } } } ] }, { "comment": "Begin Packed Switch, no idea what literal limit is for these. Have seen up to 0x7f090005", "match": "^[\\s\\t]*(\\.packed-switch)[\\s\\t]+(-0x1|0x(?i:0|[1-9a-f][\\da-f]*))(?=[\\s\\t]*(#.*)?$)", "captures": { "1": { "name": "constant.language.smali" }, "2": { "name": "variable.parameter.smali" } } }, { "comment": "End Packed Switch", "name": "constant.language.smali", "match": "^[\\s\\t]*(\\.end packed-switch)(?=[\\s\\t]*(#.*)?$)" }, { "comment": "Array data", "begin": "^[\\s\\t]*(\\.array-data)[\\s\\t]+(1|2|4|8)(?=[\\s\\t]*(#.*)?$)", "beginCaptures": { "1": { "name": "constant.language.smali" }, "2": { "name": "variable.parameter.smali" } }, "end": "^[\\s\\t]*(\\.end array-data)(?=[\\s\\t]*(#.*)?$)", "endCaptures": { "1": { "name": "constant.language.smali" } }, "patterns": [ { "include": "#comment-inline" }, { "match": "^[\\s\\t]*(?i:((?:-0x(?:0|[1-9a-f][\\da-f]{0,6}|[1-7][\\da-f]{7}|8[0]{7})|0x(?:0|[1-9a-f][\\da-f]{0,6}|[1-7][\\da-f]{7}))[st]?|(?:(?:-0x(?:0|[1-9a-f][\\da-f]{0,14}|[1-7][\\da-f]{15}|8[0]{15})|0x(?:0|[1-9a-f][\\da-f]{0,14}|[1-7][\\da-f]{15}))L))\\b)(?=[\\s\\t]*(#.*)?$)", "captures": { "1": { "name": "variable.parameter.smali" } } } ] }, { "include": "#field" }, { "include": "#field-end" }, { "include": "#annotation" }, { "include": "#annotation-end" }, { "include": "#annotation-value_list" }, { "include": "#annotation-value" }, { "include": "#annotation-name" }, { "include": "#annotation-access" }, { "include": "#comment-alone" }, { "include": "#directive-method-line" }, { "include": "#directive-method-registers_locals" }, { "include": "#directive-method-label" }, { "include": "#directive-method-parameter" }, { "include": "#directive-method-parameter-end" }, { "include": "#directives-method-relaxed" }, { "include": "#opcode-format-10x" }, { "include": "#opcode-format-10x-relaxed" }, { "include": "#opcode-format-11n" }, { "include": "#opcode-format-11n-relaxed" }, { "include": "#opcode-format-11x" }, { "include": "#opcode-format-11x-relaxed" }, { "include": "#opcode-format-22x" }, { "include": "#opcode-format-22x-relaxed" }, { "include": "#opcode-format-32x" }, { "include": "#opcode-format-32x-relaxed" }, { "include": "#opcode-format-12x" }, { "include": "#opcode-format-12x-relaxed" }, { "include": "#opcode-format-21c-string" }, { "include": "#opcode-format-21c-type" }, { "include": "#opcode-format-21c-field" }, { "include": "#opcode-format-21c-relaxed" }, { "include": "#opcode-format-21h" }, { "include": "#opcode-format-21h-relaxed" }, { "include": "#opcode-format-21s" }, { "include": "#opcode-format-21s-relaxed" }, { "include": "#opcode-format-21t" }, { "include": "#opcode-format-21t-relaxed" }, { "include": "#opcode-format-31t" }, { "include": "#opcode-format-31t-relaxed" }, { "include": "#opcode-format-22b" }, { "include": "#opcode-format-22b-relaxed" }, { "include": "#opcode-format-22c-type" }, { "include": "#opcode-format-22c-type_array" }, { "include": "#opcode-format-22c-field" }, { "include": "#opcode-format-22c-relaxed" }, { "include": "#opcode-format-22s" }, { "include": "#opcode-format-22s-relaxed" }, { "include": "#opcode-format-22t" }, { "include": "#opcode-format-22t-relaxed" }, { "include": "#opcode-format-23x" }, { "include": "#opcode-format-23x-relaxed" }, { "include": "#opcode-format-3rc-type" }, { "include": "#opcode-format-3rc-meth" }, { "include": "#opcode-format-3rc-relaxed" }, { "include": "#opcode-format-35c-type" }, { "include": "#opcode-format-35c-meth" }, { "include": "#opcode-format-35c-relaxed" }, { "include": "#opcode-format-51l" }, { "include": "#opcode-format-51l-relaxed" }, { "include": "#opcode-format-31i" }, { "include": "#opcode-format-31i-relaxed" }, { "include": "#opcode-format-10t-20t-30t" }, { "include": "#opcode-format-10t-20t-30t-relaxed" } ] }, { "comment": "Method directives - relaxed", "match": "^[\\s\\t]*(\\.(?:class|super|implements|method|(end )?(?:method|annotation|field)))", "captures": { "1": { "name": "invalid.illegal.smali" }} } ], "repository": { "field": { "comment": "Field", "match": "^[\\s\\t]*(\\.field)[\\s\\t]+((?:(?:bridge|varargs|declared-synchronized|public|protected|private|abstract|static|final|synchronized|transient|volatile|native|strictfp|synthetic|enum)[\\s\\t]+)*)([\\p{L}_\\$\\-][\\w\\$]*):[\\[]*(?:(?:(Z|B|S|C|I|J|F|D)|(?:(L)([\\p{L}_\\$][\\p{L}\\d_\\$]*(?:\/[\\p{L}_\\$][\\p{L}\\d_\\$]*)*)(;))))(?:[\\s\\t]+=[\\s\\t]+(?:(null|true|false)|(?i:(\\d+(?:\\.\\d+)?[fldst]?))|(?i:((?:-0x(?:0|[1-9a-f][\\da-f]{0,6}|[1-7][\\da-f]{7}|8[0]{7})|0x(?:0|[1-9a-f][\\da-f]{0,6}|[1-7][\\da-f]{7}))|(?:(?:-0x(?:0|[1-9a-f][\\da-f]{0,14}|[1-7][\\da-f]{15}|8[0]{15})|0x(?:0|[1-9a-f][\\da-f]{0,14}|[1-7][\\da-f]{15}))[fldst]?))\\b)|([\"'])(.*?)((?(?:([\\p{L}_\\$][\\w\\$]*):[\\[]*(?:(?:(Z|B|S|C|I|J|F|D)|(?:(L)([\\p{L}_\\$][\\p{L}\\d_\\$]*(?:\/[\\p{L}_\\$][\\p{L}\\d_\\$]*)*)(;))))|(||(?:[\\$\\p{L}_][\\p{L}\\d_\\$]*))\\(((?:[\\[]*(?:Z|B|S|C|I|J|F|D|L(?:[\\p{L}_\\$][\\p{L}\\d_\\$]*(?:\/[\\p{L}_\\$][\\p{L}\\d_\\$]*)*);))*)\\)(?:(V)|[\\[]*(Z|B|S|C|I|J|F|D)|[\\[]*(?:(L)([\\p{L}_\\$][\\p{L}\\d_\\$]*(?:\/[\\p{L}_\\$][\\p{L}\\d_\\$]*)*)(;)))))?(?=[\\s\\t]*(#.*)?$)", "captures": { "1": { "name": "support.function.smali" }, "2": { "name": "entity.name.tag.smali" }, "3": { "name": "string.quoted.double.smali" }, "4": { "name": "entity.name.tag.smali" }, "5": { "name": "entity.name.tag.smali" }, "6": { "name": "entity.name.tag.smali" }, "7": { "name": "constant.numeric.smali" }, "8": { "name": "entity.name.tag.smali" }, "9": { "name": "string.interpolated.smali" }, "10": { "name": "constant.numeric.smali" }, "11": { "name": "entity.name.tag.smali" }, "12": { "name": "constant.numeric.smali" }, "13": { "name": "entity.name.tag.smali" }, "14": { "name": "entity.name.function.smali" }, "15": { "name": "constant.numeric.smali" }, "16": { "name": "constant.numeric.smali" }, "17": { "name": "constant.numeric.smali" }, "18": { "name": "entity.name.tag.smali" }, "19": { "name": "constant.numeric.smali" }, "20": { "name": "entity.name.tag.smali" }, "21": { "name": "constant.numeric.smali" }, "22": { "name": "entity.name.tag.smali" } } }, "annotation-value_list": { "comment": "This is another hack. Deals.", "begin": "^[\\s\\t]*(value)[\\s\\t]*=[\\s\\t]*{(?=[\\s\\t]*(#.*)?$)", "beginCaptures": { "1": { "name": "support.function.smali" } }, "end": "^[\\s\\t]*}(?=[\\s\\t]*(#.*)?$)", "patterns": [ { "include": "#comment-inline" }, { "match": "(?:(\")(.*?)((?([\\p{L}_\\$][\\w\\$]*):[\\[]*(?:(?:(Z|B|S|C|I|J|F|D)|(?:(L)([\\p{L}_\\$][\\p{L}\\d_\\$]*(?:\/[\\p{L}_\\$][\\p{L}\\d_\\$]*)*)(;))))(?=[\\s\\t]*(#.*)?$)", "captures":{ "1": { "name": "support.function.smali" }, "2": { "name": "variable.parameter.smali" }, "3": { "name": "entity.name.tag.smali" }, "4": { "name": "constant.numeric.smali" }, "5": { "name": "entity.name.tag.smali" }, "6": { "name": "string.interpolated.smali" }, "7": { "name": "constant.numeric.smali" }, "8": { "name": "entity.name.tag.smali" }, "9": { "name": "constant.numeric.smali" }, "10": { "name": "entity.name.tag.smali" } } }, "opcode-format-21c-relaxed": { "match": "^[\\s\\t]*(const-string|const-class|check-cast|new-instance|(?:sget|sput)(?:-wide|-object|-boolean|-byte|-char|-short)?)", "captures": { "1": { "name": "invalid.illegal.smali" }} }, "opcode-format-21h": { "comment": "Format: op vAA, #+BBBB0000(00000000)", "match": "^[\\s\\t]*(const(?:-wide)?\/high16)[\\s\\t]+([vp](?:0|[1-9][\\d]?|1[\\d]{2}|2[0-4][\\d]|25[0-5])\\b),[\\s\\t]*((?i:-?0x(?:0|[1-9a-f][\\da-f]{0,2}|[1-7][\\da-f]{3}|8000)[0]{0,12}L?))\\b(?=[\\s\\t]*(#.*)?$)", "captures":{ "1": { "name": "support.function.smali" }, "2": { "name": "variable.parameter.smali" }, "3": { "name": "constant.numeric.smali" } } }, "opcode-format-21h-relaxed": { "match": "^[\\s\\t]*(const(?:-wide)?\/high16)", "captures": { "1": { "name": "invalid.illegal.smali" }} }, "opcode-format-21s": { "comment": "Format: op vAA, #+BBBB", "match": "^[\\s\\t]*(const(?:-wide)?\/16)[\\s\\t]+([vp](?:0|[1-9][\\d]?|1[\\d]{2}|2[0-4][\\d]|25[0-5])\\b),[\\s\\t]*(?i:(-0x(?:0|[1-9a-f][\\da-f]{0,2}|[1-7][\\da-f]{3}|8000)|0x(?:0|[1-9a-f][\\da-f]{0,2}|[1-7][\\da-f]{3})))\\b(?=[\\s\\t]*(#.*)?$)", "captures":{ "1": { "name": "support.function.smali" }, "2": { "name": "variable.parameter.smali" }, "3": { "name": "constant.numeric.smali" } } }, "opcode-format-21s-relaxed": { "match": "^[\\s\\t]*(const(?:-wide)?\/16)", "captures": { "1": { "name": "invalid.illegal.smali" }} }, "opcode-format-21t": { "comment": "Format: op vAA, +BBBB", "match": "^[\\s\\t]*(if-(?:eq|ne|lt|ge|gt|le)z)[\\s\\t]+([vp](?:0|[1-9][\\d]?|1[\\d]{2}|2[0-4][\\d]|25[0-5])\\b),[\\s\\t]*(:[A-Za-z_\\d]+)(?=[\\s\\t]*(#.*)?$)", "captures":{ "1": { "name": "support.function.smali" }, "2": { "name": "variable.parameter.smali" }, "3": { "name": "keyword.control.smali" } } }, "opcode-format-21t-relaxed": { "match": "^[\\s\\t]*(if-(?:eq|ne|lt|ge|gt|le)z)", "captures": { "1": { "name": "invalid.illegal.smali" }} }, "opcode-format-31t": { "comment": "Format: op vAA, +BBBBBBBB", "match": "^[\\s\\t]*(fill-array-data|(?:packed|sparse)-switch)[\\s\\t]+([vp](?:0|[1-9][\\d]?|1[\\d]{2}|2[0-4][\\d]|25[0-5])\\b),[\\s\\t]*(:[A-Za-z_\\d]+)(?=[\\s\\t]*(#.*)?$)", "captures":{ "1": { "name": "support.function.smali" }, "2": { "name": "variable.parameter.smali" }, "3": { "name": "keyword.control" } } }, "opcode-format-31t-relaxed": { "match": "^[\\s\\t]*(fill-array-data|(?:packed|sparse)-switch)", "captures": { "1": { "name": "invalid.illegal.smali" }} }, "opcode-format-22b": { "comment": "Format: op vAA, vBB, #+CC", "match": "^[\\s\\t]*((?:add|rsub|mul|div|rem|and|or|xor|shl|shr|ushr)-int\/lit8)[\\s\\t]+([vp](?:0|[1-9][\\d]?|1[\\d]{2}|2[0-4][\\d]|25[0-5])\\b),[\\s\\t]*([vp](?:0|[1-9][\\d]?|1[\\d]{2}|2[0-4][\\d]|25[0-5])\\b),[\\s\\t]*(?i:(-0x(?:[\\da-f]|[1-7][\\da-f]|80)|0x(?:[\\da-f]|[1-7][\\da-f])))\\b(?=[\\s\\t]*(#.*)?$)", "captures":{ "1": { "name": "support.function.smali" }, "2": { "name": "variable.parameter.smali" }, "3": { "name": "variable.parameter.smali" }, "4": { "name": "constant.numeric.smali" } } }, "opcode-format-22b-relaxed": { "match": "^[\\s\\t]*((?:add|rsub|mul|div|rem|and|or|xor|shl|shr|ushr)-int\/lit8)", "captures": { "1": { "name": "invalid.illegal.smali" }} }, "opcode-format-22c-type": { "comment": "Format: op vA, vB, type@CCCC", "match": "^[\\s\\t]*(instance-of)[\\s\\t]+([vp](?:0|[1-9]|1[0-5])\\b),[\\s\\t]*([vp](?:0|[1-9]|1[0-5])\\b),[\\s\\t]*[\\[]*(?:(Z|B|S|C|I|J|F|D)|(L)([\\p{L}_\\$][\\p{L}\\d_\\$]*(?:\/[\\p{L}_\\$][\\p{L}\\d_\\$]*)*)(;))(?=[\\s\\t]*(#.*)?$)", "captures":{ "1": { "name": "support.function.smali" }, "2": { "name": "variable.parameter.smali" }, "3": { "name": "variable.parameter.smali" }, "4": { "name": "constant.numeric.smali" }, "5": { "name": "entity.name.tag.smali" }, "6": { "name": "constant.numeric.smali" }, "7": { "name": "entity.name.tag.smali" } } }, "opcode-format-22c-type_array": { "comment": "Format: op vA, vB, [type@CCCC", "match": "^[\\s\\t]*(new-array)[\\s\\t]+([vp](?:0|[1-9]|1[0-5])\\b),[\\s\\t]*([vp](?:0|[1-9]|1[0-5])\\b),[\\s\\t]*[\\[]+(?:(Z|B|S|C|I|J|F|D)|(L)([\\p{L}_\\$][\\p{L}\\d_\\$]*(?:\/[\\p{L}_\\$][\\p{L}\\d_\\$]*)*)(;))(?=[\\s\\t]*(#.*)?$)", "captures":{ "1": { "name": "support.function.smali" }, "2": { "name": "variable.parameter.smali" }, "3": { "name": "variable.parameter.smali" }, "4": { "name": "constant.numeric.smali" }, "5": { "name": "entity.name.tag.smali" }, "6": { "name": "constant.numeric.smali" }, "7": { "name": "entity.name.tag.smali" } } }, "opcode-format-22c-field": { "comment": "Format: op vA, vB, field@CCCC", "match": "^[\\s\\t]*((?:iget|iput)(?:-wide|-object|-boolean|-byte|-char|-short)?)[\\s\\t]+([vp](?:0|[1-9]|1[0-5])\\b),[\\s\\t]*([vp](?:0|[1-9]|1[0-5])\\b),[\\s\\t]*(L)([\\p{L}_\\$][\\p{L}\\d_\\$]*(?:\/[\\p{L}_\\$][\\p{L}\\d_\\$]*)*)(;)->([\\p{L}_\\$][\\w\\$]*):[\\[]*(?:(Z|B|S|C|I|J|F|D|(?:(L)([\\p{L}_\\$][\\p{L}\\d_\\$]*(?:\/[\\p{L}_\\$][\\p{L}\\d_\\$]*)*)(;))))(?=[\\s\\t]*(#.*)?$)", "captures":{ "1": { "name": "support.function.smali" }, "2": { "name": "variable.parameter.smali" }, "3": { "name": "variable.parameter.smali" }, "4": { "name": "entity.name.tag.smali" }, "5": { "name": "constant.numeric.smali" }, "6": { "name": "entity.name.tag.smali" }, "7": { "name": "string.interpolated.smali" }, "8": { "name": "constant.numeric.smali" }, "9": { "name": "entity.name.tag.smali" }, "10": { "name": "constant.numeric.smali" }, "11": { "name": "entity.name.tag.smali" } } }, "opcode-format-22c-relaxed": { "match": "^[\\s\\t]*(instance-of|new-array|(?:iget|iput)(?:-wide|-object|-boolean|-byte|-char|-short)?)", "captures": { "1": { "name": "invalid.illegal.smali" }} }, "opcode-format-22s": { "comment": "Format: op vA, vB, #+CCCC", "match": "^[\\s\\t]*((?:(?:add|mul|div|rem|and|or|xor)-int\/lit16)|rsub-int)[\\s\\t]+([vp](?:0|[1-9][\\d]?|1[\\d]{2}|2[0-4][\\d]|25[0-5])\\b),[\\s\\t]*([vp](?:0|[1-9][\\d]?|1[\\d]{2}|2[0-4][\\d]|25[0-5])\\b),[\\s\\t]*(?i:(-0x(?:0|[1-9a-f][\\da-f]{0,2}|[1-7][\\da-f]{3}|8000)|0x(?:0|[1-9a-f][\\da-f]{0,2}|[1-7][\\da-f]{3})))\\b(?=[\\s\\t]*(#.*)?$)", "captures":{ "1": { "name": "support.function.smali" }, "2": { "name": "variable.parameter.smali" }, "3": { "name": "variable.parameter.smali" }, "4": { "name": "constant.numeric.smali" } } }, "opcode-format-22s-relaxed": { "match": "^[\\s\\t]*((?:(?:add|mul|div|rem|and|or|xor)-int\/lit16)|rsub-int)", "captures": { "1": { "name": "invalid.illegal.smali" }} }, "opcode-format-22t": { "comment": "*Format: op vA, vB, +CCCC", "match": "^[\\s\\t]*(if-(?:eq|ne|lt|ge|gt|le))[\\s\\t]+([vp](?:0|[1-9]|1[0-5])\\b),[\\s\\t]*([vp](?:0|[1-9]|1[0-5])\\b),[\\s\\t]*(:[A-Za-z_\\d]+)(?=[\\s\\t]*(#.*)?$)", "captures":{ "1": { "name": "support.function.smali" }, "2": { "name": "variable.parameter.smali" }, "3": { "name": "variable.parameter.smali" }, "4": { "name": "keyword.control" } } }, "opcode-format-22t-relaxed": { "match": "(if-(?:eq|ne|lt|ge|gt|le))", "captures": { "1": { "name": "invalid.illegal.smali" }} }, "opcode-format-23x": { "comment": "Format: op vAA, vBB, vCC", "match": "^[\\s\\t]*((?:cmpl|cmpg)-(?:float|double)|cmp-long|(?:aget|aput)(?:-wide|-object|-boolean|-byte|-char|-short)?|(?:add|sub|mul|div|rem|and|or|xor|shl|shr|ushr)-(?:int|long)|(?:add|sub|mul|div|rem)-(?:float|double))[\\s\\t]+([vp](?:0|[1-9][\\d]?|1[\\d]{2}|2[0-4][\\d]|25[0-5])\\b),[\\s\\t]*([vp](?:0|[1-9][\\d]?|1[\\d]{2}|2[0-4][\\d]|25[0-5])\\b),[\\s\\t]*([vp](?:0|[1-9][\\d]?|1[\\d]{2}|2[0-4][\\d]|25[0-5])\\b)(?=[\\s\\t]*(#.*)?$)", "captures":{ "1": { "name": "support.function.smali" }, "2": { "name": "variable.parameter.smali" }, "3": { "name": "variable.parameter.smali" }, "4": { "name": "variable.parameter.smali" } } }, "opcode-format-23x-relaxed": { "match": "^[\\s\\t]*((?:cmpl|cmpg)-(float|double)|cmp-long|(?:aget|aput)(?:-wide|-object|-boolean|-byte|-char|-short)?|(?:add|sub|mul|div|rem|and|or|xor|shl|shr|ushr)-(?:int|long)|(?:add|sub|mul|div|rem)-(?:float|double))", "captures": { "1": { "name": "invalid.illegal.smali" }} }, "opcode-format-3rc-type": { "comment": "Format: op {vCCCC .. vNNNN}, type@BBBB", "match": "^[\\s\\t]*(filled-new-array\/range) {([vp](?:0|[1-9][\\d]{0,3}|[1-5][\\d]{4}|6[0-4][\\d]{3}|65[0-4][\\d]{2}|655[0-2][\\d]|6553[0-5])\\b) \\.\\. ([vp](?:0|[1-9][\\d]{0,3}|[1-5][\\d]{4}|6[0-4][\\d]{3}|65[0-4][\\d]{2}|655[0-2][\\d]|6553[0-5])\\b)},[\\s\\t]*[\\[]+(?:(Z|B|S|C|I|J|F|D)|(L)([\\p{L}_\\$][\\p{L}\\d_\\$]*(?:\/[\\p{L}_\\$][\\p{L}\\d_\\$]*)*)(;))(?=[\\s\\t]*(#.*)?$)", "captures":{ "1": { "name": "support.function.smali" }, "2": { "name": "variable.parameter.smali" }, "3": { "name": "variable.parameter.smali" }, "4": { "name": "constant.numeric.smali" }, "5": { "name": "entity.name.tag.smali" }, "6": { "name": "constant.numeric.smali" }, "7": { "name": "entity.name.tag.smali" } } }, "opcode-format-3rc-meth": { "comment": "Format: op {vCCCC .. vNNNN}, meth@BBBB", "match": "^[\\s\\t]*(invoke-(?:virtual|super|direct|static|interface)\/range) {[\\s\\t]*([vp](?:0|[1-9][\\d]{0,3}|[1-5][\\d]{4}|6[0-4][\\d]{3}|65[0-4][\\d]{2}|655[0-2][\\d]|6553[0-5])\\b) \\.\\. ([vp](?:0|[1-9][\\d]{0,3}|[1-5][\\d]{4}|6[0-4][\\d]{3}|65[0-4][\\d]{2}|655[0-2][\\d]|6553[0-5])\\b)[\\s\\t]*},[\\s\\t]*[\\[]*(L)([\\p{L}_\\$][\\p{L}\\d_\\$]*(?:\/[\\p{L}_\\$][\\p{L}\\d_\\$]*)*)(;)->(||(?:[\\$\\p{L}_][\\p{L}\\d_\\$]*))\\(((?:[\\[]*(?:Z|B|S|C|I|J|F|D|L(?:[\\p{L}_\\$][\\p{L}\\d_\\$]*(?:\/[\\p{L}_\\$][\\p{L}\\d_\\$]*)*);))*)\\)(?:(V)|[\\[]*(Z|B|S|C|I|J|F|D)|[\\[]*(?:(L)([\\p{L}_\\$][\\p{L}\\d_\\$]*(?:\/[\\p{L}_\\$][\\p{L}\\d_\\$]*)*)(;)))(?=[\\s\\t]*(#.*)?$)", "captures":{ "1": { "name": "support.function.smali" }, "2": { "name": "variable.parameter.smali" }, "3": { "name": "variable.parameter.smali" }, "4": { "name": "entity.name.tag.smali" }, "5": { "name": "constant.numeric.smali" }, "6": { "name": "entity.name.tag.smali" }, "7": { "name": "entity.name.function.smali" }, "8": { "name": "constant.numeric.smali" }, "9": { "name": "constant.numeric.smali" }, "10": { "name": "constant.numeric.smali" }, "11": { "name": "entity.name.tag.smali" }, "12": { "name": "constant.numeric.smali" }, "13": { "name": "entity.name.tag.smali" }, "14": { "name": "constant.numeric.smali" }, "15": { "name": "entity.name.tag.smali" } } }, "opcode-format-3rc-relaxed": { "match": "^[\\s\\t]*((?:filled-new-array|invoke-(?:virtual|super|direct|static|interface))\/range)", "captures": { "1": { "name": "invalid.illegal.smali" }} }, "opcode-format-35c-type": { "comment": "Format: op {vC, vD, vE, vF, vG}, type@BBBB", "match": "^[\\s\\t]*(filled-new-array) {([vp](?:0|[1-9]|1[0-5])\\b),[\\s\\t]*([vp](?:0|[1-9]|1[0-5])\\b)(?:,[\\s\\t]*([vp](?:0|[1-9]|1[0-5])\\b))?(?:,[\\s\\t]*([vp](?:0|[1-9]|1[0-5])\\b))?(?:,[\\s\\t]*([vp](?:0|[1-9]|1[0-5])\\b))?},[\\s\\t]*[\\[]+(?:(Z|B|S|C|I|J|F|D)|(L)([\\p{L}_\\$][\\p{L}\\d_\\$]*(?:\/[\\p{L}_\\$][\\p{L}\\d_\\$]*)*)(;))(?=[\\s\\t]*(#.*)?$)", "captures":{ "1": { "name": "support.function.smali" }, "2": { "name": "variable.parameter.smali" }, "3": { "name": "variable.parameter.smali" }, "4": { "name": "variable.parameter.smali" }, "5": { "name": "variable.parameter.smali" }, "6": { "name": "variable.parameter.smali" }, "7": { "name": "constant.numeric.smali" }, "8": { "name": "entity.name.tag.smali" }, "9": { "name": "constant.numeric.smali" }, "10": { "name": "entity.name.tag.smali" }, "11": { "name": "constant.numeric.smali" } } }, "opcode-format-35c-meth": { "comment": "Format: op {vC, vD, vE, vF, vG}, meth@BBBB", "match": "^[\\s\\t]*(invoke-(?:virtual|super|direct|static|interface)) {[\\s\\t]*([vp](?:0|[1-9]|1[0-5])\\b)?(?:,[\\s\\t]*([vp](?:0|[1-9]|1[0-5])\\b))?(?:,[\\s\\t]*([vp](?:0|[1-9]|1[0-5])\\b))?(?:,[\\s\\t]*([vp](?:0|[1-9]|1[0-5])\\b))?(?:,[\\s\\t]*([vp](?:0|[1-9]|1[0-5])\\b))?[\\s\\t]*},[\\s\\t]*[\\[]*(L)([\\p{L}_\\$][\\p{L}\\d_\\$]*(?:\/[\\p{L}_\\$][\\p{L}\\d_\\$]*)*)(;)->(||(?:[\\$\\p{L}_][\\p{L}\\d_\\$]*))\\((?:[\\[]*(Z|B|S|C|I|J|F|D)|(?:[\\[]*(L)([\\p{L}_\\$][\\p{L}\\d_\\$]*(?:\/[\\p{L}_\\$][\\p{L}\\d_\\$]*)*)(;)))?(?:[\\[]*(Z|B|S|C|I|J|F|D)|(?:[\\[]*(L)([\\p{L}_\\$][\\p{L}\\d_\\$]*(?:\/[\\p{L}_\\$][\\p{L}\\d_\\$]*)*)(;)))?(?:[\\[]*(Z|B|S|C|I|J|F|D)|(?:[\\[]*(L)([\\p{L}_\\$][\\p{L}\\d_\\$]*(?:\/[\\p{L}_\\$][\\p{L}\\d_\\$]*)*)(;)))?(?:[\\[]*(Z|B|S|C|I|J|F|D)|(?:[\\[]*(L)([\\p{L}_\\$][\\p{L}\\d_\\$]*(?:\/[\\p{L}_\\$][\\p{L}\\d_\\$]*)*)(;)))?(?:[\\[]*(Z|B|S|C|I|J|F|D)|(?:[\\[]*(L)([\\p{L}_\\$][\\p{L}\\d_\\$]*(?:\/[\\p{L}_\\$][\\p{L}\\d_\\$]*)*)(;)))?\\)(?:(?:(V)|[\\[]*(Z|B|S|C|I|J|F|D))|(?:[\\[]*(L)([\\p{L}_\\$][\\p{L}\\d_\\$]*(?:\/[\\p{L}_\\$][\\p{L}\\d_\\$]*)*)(;)))(?=[\\s\\t]*(#.*)?$)", "captures":{ "1": { "name": "support.function.smali" }, "2": { "name": "variable.parameter.smali" }, "3": { "name": "variable.parameter.smali" }, "4": { "name": "variable.parameter.smali" }, "5": { "name": "variable.parameter.smali" }, "6": { "name": "variable.parameter.smali" }, "7": { "name": "entity.name.tag.smali" }, "8": { "name": "constant.numeric.smali" }, "9": { "name": "entity.name.tag.smali" }, "10": { "name": "entity.name.function.smali" }, "11": { "name": "constant.numeric.smali" }, "12": { "name": "entity.name.tag.smali" }, "13": { "name": "constant.numeric.smali" }, "14": { "name": "entity.name.tag.smali" }, "15": { "name": "constant.numeric.smali" }, "16": { "name": "entity.name.tag.smali" }, "17": { "name": "constant.numeric.smali" }, "18": { "name": "entity.name.tag.smali" }, "19": { "name": "constant.numeric.smali" }, "20": { "name": "entity.name.tag.smali" }, "21": { "name": "constant.numeric.smali" }, "22": { "name": "entity.name.tag.smali" }, "23": { "name": "constant.numeric.smali" }, "24": { "name": "entity.name.tag.smali" }, "25": { "name": "constant.numeric.smali" }, "26": { "name": "entity.name.tag.smali" }, "27": { "name": "constant.numeric.smali" }, "28": { "name": "entity.name.tag.smali" }, "29": { "name": "constant.numeric.smali" }, "30": { "name": "entity.name.tag.smali" }, "31": { "name": "constant.numeric.smali" }, "32": { "name": "constant.numeric.smali" }, "33": { "name": "entity.name.tag.smali" }, "34": { "name": "constant.numeric.smali" }, "35": { "name": "entity.name.tag.smali" } } }, "opcode-format-35c-relaxed": { "match": "^[\\s\\t]*(filled-new-array|invoke-(?:virtual|super|direct|static|interface))", "captures": { "1": { "name": "invalid.illegal.smali" }} }, "opcode-format-51l": { "comment": "Format: op vAA, #+BBBBBBBBBBBBBBBB", "match": "^[\\s\\t]*(const-wide)(?!\/32)[\\s\\t]+([vp](?:0|[1-9][\\d]?|1[\\d]{2}|2[0-4][\\d]|25[0-5])\\b),[\\s\\t]*(?i:((?:-0x(?:0|[1-9a-f][\\da-f]{0,6}|[1-7][\\da-f]{7}|8[0]{7})|0x(?:0|[1-9a-f][\\da-f]{0,6}|[1-7][\\da-f]{7}))|(?:(?:-0x(?:0|[1-9a-f][\\da-f]{0,14}|[1-7][\\da-f]{15}|8[0]{15})|0x(?:0|[1-9a-f][\\da-f]{0,14}|[1-7][\\da-f]{15}))L))\\b)(?=[\\s\\t]*(#.*)?$)", "captures":{ "1": { "name": "support.function.smali" }, "2": { "name": "variable.parameter.smali" }, "3": { "name": "constant.numeric.smali" } } }, "opcode-format-51l-relaxed": { "match": "^[\\s\\t]*(const-wide)(?!\\\/32)", "captures": { "1": { "name": "invalid.illegal.smali" }} }, "opcode-format-31i": { "comment": "Format: op vAA, #+BBBBBBBB", "match": "^[\\s\\t]*(const(?:-wide\/32)?)[\\s\\t]+([vp](?:0|[1-9][\\d]?|1[\\d]{2}|2[0-4][\\d]|25[0-5])\\b),[\\s\\t]*(?i:(-0x(?:0|[1-9a-f][\\da-f]{0,6}|[1-7][\\da-f]{7}|8[0]{7})|0x(?:0|[1-9a-f][\\da-f]{0,6}|[1-7][\\da-f]{7}))\\b)(?=[\\s\\t]*(#.*)?$)", "captures":{ "1": { "name": "support.function.smali" }, "2": { "name": "variable.parameter.smali" }, "3": { "name": "constant.numeric.smali" } } }, "opcode-format-31i-relaxed": { "match": "^[\\s\\t]*(const(?:-wide\/32)?)", "captures": { "1": { "name": "invalid.illegal.smali" }} }, "opcode-format-10t-20t-30t": { "comment": "Format: op +AA(AA(AAAA))", "match": "^[\\s\\t]*(goto(?:\/16|\/32)?) (:[A-Za-z_\\d]+)(?=[\\s\\t]*(#.*)?$)", "captures":{ "1": { "name": "support.function.smali" }, "2": { "name": "keyword.control" } } }, "opcode-format-10t-20t-30t-relaxed": { "match": "^[\\s\\t]*(goto(?:\/16|\/32)?)", "captures": { "1": { "name": "invalid.illegal.smali" }} } }, "uuid": "d6fe4632-f21a-4533-8908-723df0b58ac0" } ================================================ FILE: app/src/main/assets/languages/xml/language-configuration.json ================================================ { "comments": { "blockComment": [ "" ] }, "brackets": [ [""], ["<", ">"], ["{", "}"], ["(", ")"] ], "autoClosingPairs": [ { "open": "{", "close": "}"}, { "open": "[", "close": "]"}, { "open": "(", "close": ")" }, { "open": "\"", "close": "\"", "notIn": ["string"] }, { "open": "'", "close": "'", "notIn": ["string"] }, { "open": "", "notIn": [ "comment", "string" ]}, { "open": "", "notIn": [ "comment", "string" ]} ], "surroundingPairs": [ { "open": "'", "close": "'" }, { "open": "\"", "close": "\"" }, { "open": "{", "close": "}"}, { "open": "[", "close": "]"}, { "open": "(", "close": ")" }, { "open": "<", "close": ">" } ], "colorizedBracketPairs": [ ], "folding": { "markers": { "start": "^\\s*", "end": "^\\s*" } }, "wordPattern": "[:A-Z_a-z\\u{C0}-\\u{D6}\\u{D8}-\\u{F6}\\u{F8}-\\u{2FF}\\u{370}-\\u{37D}\\u{37F}-\\u{1FFF}\\u{200C}-\\u{200D}\\u{2070}-\\u{218F}\\u{2C00}-\\u{2FEF}\\u{3001}-\\u{D7FF}\\u{F900}-\\u{FDCF}\\u{FDF0}-\\u{FFFD}\\u{10000}-\\u{EFFFF}][-:A-Z_a-z\\u{C0}-\\u{D6}\\u{D8}-\\u{F6}\\u{F8}-\\u{2FF}\\u{370}-\\u{37D}\\u{37F}-\\u{1FFF}\\u{200C}-\\u{200D}\\u{2070}-\\u{218F}\\u{2C00}-\\u{2FEF}\\u{3001}-\\u{D7FF}\\u{F900}-\\u{FDCF}\\u{FDF0}-\\u{FFFD}\\u{10000}-\\u{EFFFF}.0-9\\u{B7}\\u{0300}-\\u{036F}\\u{203F}-\\u{2040}]*" } ================================================ FILE: app/src/main/assets/languages/xml/tmLanguage.json ================================================ { "scopeName": "text.xml", "name": "XML", "fileTypes": [ "aiml", "atom", "axml", "bpmn", "config", "cpt", "csl", "csproj", "csproj.user", "dae", "dia", "dita", "ditamap", "dtml", "fodg", "fodp", "fods", "fodt", "fsproj", "fxml", "gir", "glade", "gpx", "graphml", "icls", "iml", "isml", "jmx", "jsp", "kml", "kst", "launch", "menu", "mxml", "nunit", "nuspec", "opml", "owl", "pom", "ppj", "proj", "pt", "pubxml", "pubxml.user", "rdf", "rng", "rss", "sdf", "shproj", "siml", "sld", "storyboard", "StyleCop", "svg", "targets", "tld", "vbox", "vbox-prev", "vbproj", "vbproj.user", "vcproj", "vcproj.filters", "vcxproj", "vcxproj.filters", "wixmsp", "wixmst", "wixobj", "wixout", "wsdl", "wxs", "xaml", "xbl", "xib", "xlf", "xliff", "xml", "xpdl", "xsd", "xul", "ui" ], "firstLineMatch": "(?x)\n# XML declaration\n(?:\n ^ <\\? xml\n\n # VersionInfo\n \\s+ version\n \\s* = \\s*\n (['\"])\n 1 \\. [0-9]+\n \\1\n\n # EncodingDecl\n (?:\n \\s+ encoding\n \\s* = \\s*\n\n # EncName\n (['\"])\n [A-Za-z]\n [-A-Za-z0-9._]*\n \\2\n )?\n\n # SDDecl\n (?:\n \\s+ standalone\n \\s* = \\s*\n (['\"])\n (?:yes|no)\n \\3\n )?\n\n \\s* \\?>\n)\n|\n# Modeline\n(?i:\n # Emacs\n -\\*-(?:\\s*(?=[^:;\\s]+\\s*-\\*-)|(?:.*?[;\\s]|(?<=-\\*-))mode\\s*:\\s*)\n xml\n (?=[\\s;]|(?]?\\d+|m)?|\\sex)(?=:(?=\\s*set?\\s[^\\n:]+:)|:(?!\\s*set?\\s))(?:(?:\\s|\\s*:\\s*)\\w*(?:\\s*=(?:[^\\n\\\\\\s]|\\\\.)*)?)*[\\s:](?:filetype|ft|syntax)\\s*=\n xml\n (?=\\s|:|$)\n)", "patterns": [ { "begin": "(<\\?)\\s*([-_a-zA-Z0-9]+)", "captures": { "1": { "name": "punctuation.definition.tag.xml" }, "2": { "name": "entity.name.tag.xml" } }, "end": "(\\?>)", "name": "meta.tag.preprocessor.xml", "patterns": [ { "match": " ([a-zA-Z-]+)", "name": "entity.other.attribute-name.xml" }, { "include": "#doublequotedString" }, { "include": "#singlequotedString" } ] }, { "begin": "()", "name": "meta.tag.sgml.doctype.xml", "patterns": [ { "include": "#internalSubset" } ] }, { "include": "#comments" }, { "begin": "(<)((?:([-_a-zA-Z0-9]+)(:))?([-_a-zA-Z0-9:]+))(?=(\\s[^>]*)?>)", "beginCaptures": { "1": { "name": "punctuation.definition.tag.xml" }, "2": { "name": "entity.name.tag.xml" }, "3": { "name": "entity.name.tag.namespace.xml" }, "4": { "name": "punctuation.separator.namespace.xml" }, "5": { "name": "entity.name.tag.localname.xml" } }, "end": "(>)()", "endCaptures": { "1": { "name": "punctuation.definition.tag.xml" }, "2": { "name": "punctuation.definition.tag.xml" }, "3": { "name": "entity.name.tag.xml" }, "4": { "name": "entity.name.tag.namespace.xml" }, "5": { "name": "punctuation.separator.namespace.xml" }, "6": { "name": "entity.name.tag.localname.xml" }, "7": { "name": "punctuation.definition.tag.xml" } }, "name": "meta.tag.no-content.xml", "patterns": [ { "include": "#tagStuff" } ] }, { "begin": "()", "name": "meta.tag.xml", "patterns": [ { "include": "#tagStuff" } ] }, { "include": "#entity" }, { "include": "#bare-ampersand" }, { "begin": "<%@", "beginCaptures": { "0": { "name": "punctuation.section.embedded.begin.xml" } }, "end": "%>", "endCaptures": { "0": { "name": "punctuation.section.embedded.end.xml" } }, "name": "source.java-props.embedded.xml", "patterns": [ { "match": "page|include|taglib", "name": "keyword.other.page-props.xml" } ] }, { "begin": "<%[!=]?(?!--)", "beginCaptures": { "0": { "name": "punctuation.section.embedded.begin.xml" } }, "end": "(?!--)%>", "endCaptures": { "0": { "name": "punctuation.section.embedded.end.xml" } }, "name": "source.java.embedded.xml", "patterns": [ { "include": "source.java" } ] }, { "begin": "", "endCaptures": { "0": { "name": "punctuation.definition.string.end.xml" } }, "name": "string.unquoted.cdata.xml" } ], "repository": { "EntityDecl": { "begin": "()", "patterns": [ { "include": "#doublequotedString" }, { "include": "#singlequotedString" } ] }, "bare-ampersand": { "match": "&", "name": "invalid.illegal.bad-ampersand.xml" }, "doublequotedString": { "begin": "\"", "beginCaptures": { "0": { "name": "punctuation.definition.string.begin.xml" } }, "end": "\"", "endCaptures": { "0": { "name": "punctuation.definition.string.end.xml" } }, "name": "string.quoted.double.xml", "patterns": [ { "include": "#entity" }, { "include": "#bare-ampersand" } ] }, "entity": { "captures": { "1": { "name": "punctuation.definition.constant.xml" }, "3": { "name": "punctuation.definition.constant.xml" } }, "match": "(&)([:a-zA-Z_][:a-zA-Z0-9_.-]*|#[0-9]+|#x[0-9a-fA-F]+)(;)", "name": "constant.character.entity.xml" }, "internalSubset": { "begin": "(\\[)", "captures": { "1": { "name": "punctuation.definition.constant.xml" } }, "end": "(\\])", "name": "meta.internalsubset.xml", "patterns": [ { "include": "#EntityDecl" }, { "include": "#parameterEntity" }, { "include": "#comments" } ] }, "parameterEntity": { "captures": { "1": { "name": "punctuation.definition.constant.xml" }, "3": { "name": "punctuation.definition.constant.xml" } }, "match": "(%)([:a-zA-Z_][:a-zA-Z0-9_.-]*)(;)", "name": "constant.character.parameter-entity.xml" }, "singlequotedString": { "begin": "'", "beginCaptures": { "0": { "name": "punctuation.definition.string.begin.xml" } }, "end": "'", "endCaptures": { "0": { "name": "punctuation.definition.string.end.xml" } }, "name": "string.quoted.single.xml", "patterns": [ { "include": "#entity" }, { "include": "#bare-ampersand" } ] }, "tagStuff": { "patterns": [ { "captures": { "1": { "name": "entity.other.attribute-name.namespace.xml" }, "2": { "name": "entity.other.attribute-name.xml" }, "3": { "name": "punctuation.separator.namespace.xml" }, "4": { "name": "entity.other.attribute-name.localname.xml" } }, "match": "(?:^|\\s+)(?:([-\\w.]+)((:)))?([-\\w.:]+)\\s*=" }, { "include": "#doublequotedString" }, { "include": "#singlequotedString" } ] } } } ================================================ FILE: app/src/main/assets/run_server.sh ================================================ #!/system/bin/sh # SPDX-License-Identifier: GPL-3.0-or-later if [ $# -lt 2 ]; then echo "USAGE: ./run_server.sh " exit 1 fi SERVER_NAME= JAR_NAME= JAR_PATH= %ENV_VARS% PORT="path:$1" TOKEN=",token:$2" ARGS="${PORT}${ARGS}${TOKEN}" JAR_PACKAGE_NAME="io.github.muntashirakon.AppManager" JAR_MAIN_CLASS="${JAR_PACKAGE_NAME}.server.ServerRunner" TMP_PATH="/data/local/tmp" EXEC_JAR_PATH=${TMP_PATH}/${JAR_NAME} # Ideally, id -u could be used, but it's not supported on older platforms # neither are commands like awk or sed, we're only left with grep. UID=$(id | grep -oE "uid=[0-9]+" | grep -oE "[0-9]+") GID=$(id | grep -oE "gid=[0-9]+" | grep -oE "[0-9]+") echo "Starting $SERVER_NAME as $UID:$GID..." # Copy am.jar to executable directory cp -f ${JAR_PATH} ${EXEC_JAR_PATH} if [ $? -ne 0 ]; then # Copy failed echo "Error! Could not copy jar file to the executable directory." exit 1 fi # Fix permission chmod 755 ${EXEC_JAR_PATH} chown $UID:$GID ${EXEC_JAR_PATH} # Debug log echo "Jar path: $JAR_PATH" echo "Args: $ARGS" # Save jar path to environment variable export CLASSPATH=${EXEC_JAR_PATH} # Execute local server exec app_process /system/bin --nice-name=${SERVER_NAME} ${JAR_MAIN_CLASS} "$ARGS" $@ & if [ $? -ne 0 ]; then # Start failed echo "Error! Could not start local server." exit 1 else # Start success echo "Local server has started." exit 0 fi ================================================ FILE: app/src/main/assets/suggestions.json ================================================ [{"id":"com.aurora.store","label":"Aurora Store","reason":"Alternative Google Play Store client with privacy in mind.\nhttps:\/\/aurora-oss.vercel.app\/faq\/#aurora-store","source":"f","repo":"https:\/\/gitlab.com\/AuroraOSS\/AuroraStore","_id":"app_stores"},{"id":"com.looker.droidify","label":"Droid-ify","source":"f","repo":"https:\/\/github.com\/Iamlooker\/Droid-ify","_id":"app_stores"},{"id":"com.dimowner.audiorecorder","label":"Audio Recorder","source":"fg","repo":"https:\/\/github.com\/Dimowner\/AudioRecorder","_id":"audio_recorders"},{"id":"com.beemdevelopment.aegis","label":"Aegis","source":"fg","repo":"https:\/\/github.com\/beemdevelopment\/Aegis","_id":"authenticators"},{"id":"com.jens.automation2","label":"Automation","source":"f","repo":"https:\/\/git.server47.de\/jens\/Automation","_id":"automation_apps"},{"id":"com.machiav3lli.backup","label":"Neo Backup","source":"f","repo":"https:\/\/github.com\/NeoApplications\/Neo-Backup","_id":"backup_apps"},{"id":"com.atharok.barcodescanner","label":"Barcode Scanner","source":"fga","repo":"https:\/\/gitlab.com\/Atharok\/BarcodeScanner","_id":"barcode_scanners"},{"id":"org.chromium.chrome","label":"Vanadium","repo":"https:\/\/github.com\/GrapheneOS\/Vanadium","reason":"GrapheneOS only","_id":"browsers"},{"id":"org.cromite.cromite","label":"Cromite","source":"f","repo":"https:\/\/github.com\/uazo\/cromite","_id":"browsers"},{"id":"org.solovyev.android.calculator","label":"Calculator++","source":"fg","repo":"https:\/\/git.bubu1.eu\/Bubu\/android-calculatorpp","_id":"calculators"},{"id":"ws.xsoh.etar","label":"Etar","source":"fg","repo":"https:\/\/github.com\/Etar-Group\/Etar-Calendar","_id":"calendars"},{"id":"app.grapheneos.camera","label":"Camera","repo":"https:\/\/github.com\/GrapheneOS\/Camera","_id":"cameras"},{"id":"net.sourceforge.opencamera","label":"Open Camera","source":"fg","reason":"A more advanced camera app","repo":"https:\/\/sourceforge.net\/projects\/opencamera\/","_id":"cameras"},{"id":"eu.darken.sdmse","label":"SD Maid SE","source":"g","repo":"https:\/\/github.com\/d4rken-org\/sdmaid-se","_id":"cleaners"},{"id":"itkach.aard2","label":"Aard 2","reason":"Simple yet powerful dictionary reader that support multiple dictionaries with offline access.\nSLOB files https:\/\/github.com\/itkach\/slob\/wiki\/Dictionaries","source":"fg","repo":"https:\/\/github.com\/itkach\/aard2-android","_id":"dictionaries"},{"id":"com.foobnix.pro.pdf.reader","label":"Librera","source":"fg","repo":"https:\/\/github.com\/foobnix\/LibreraReader","_id":"ebook_readers"},{"id":"org.koreader.launcher.fdroid","label":"KOReader","source":"f","repo":"https:\/\/github.com\/koreader\/koreader","reason":"Optimized for e-ink displays","_id":"ebook_readers"},{"id":"net.thunderbird.android","label":"Thunderbird","source":"fg","reason":"https:\/\/blog.thunderbird.net\/2023\/07\/k-9-mail-collaborates-with-ostif-and-7asecurity-security-audit\/","repo":"https:\/\/github.com\/thunderbird\/thunderbird-android","_id":"email_clients"},{"id":"me.zhanghai.android.files","label":"Material Files","source":"fg","repo":"https:\/\/github.com\/zhanghai\/MaterialFiles","_id":"file_managers"},{"id":"org.fossify.gallery","label":"Fossify Gallery","source":"fg","repo":"https:\/\/github.com\/FossifyOrg\/Gallery","_id":"gallery"},{"id":"deckers.thibault.aves.libre","label":"Aves Libre","source":"f","repo":"https:\/\/github.com\/deckerst\/aves","_id":"gallery"},{"id":"dev.patrickgold.florisboard","label":"FlorisBoard","source":"f","repo":"https:\/\/github.com\/florisboard\/florisboard","_id":"keyboards"},{"id":"org.smc.inputmethod.indic","label":"Indic Keyboard","source":"fg","reason":"Enhanced keyboard support for Indian languages","repo":"https:\/\/gitlab.com\/indicproject\/Indic-Keyboard","_id":"keyboards"},{"id":"app.organicmaps","label":"Organic Maps","source":"fg","repo":"https:\/\/github.com\/organicmaps\/organicmaps","_id":"maps"},{"id":"org.jitsi.meet","label":"Jitsi Meet","source":"fg","repo":"https:\/\/github.com\/jitsi\/jitsi-meet","_id":"meeting_apps"},{"id":"com.iven.musicplayergo","label":"Music Player GO","source":"fg","repo":"https:\/\/github.com\/enricocid\/Music-Player-GO","_id":"music_apps"},{"id":"com.shadow.blackhole","label":"BlackHole","reason":"Stream & download high quality 320kbps songs, also supports offline local music.\nNo subscription or account login required.\n","source":"f","repo":"https:\/\/github.com\/Sangwan5688\/BlackHole","_id":"music_apps"},{"id":"com.streetwriters.notesnook","label":"Notesnook","reason":"Privacy friendy, encrypted note taking app.\nRoadmap: https:\/\/notesnook.com\/roadmap\/\n","source":"g","repo":"https:\/\/github.com\/streetwriters\/notesnook","_id":"note_taking_apps"},{"id":"io.github.quillpad","label":"Quillpad","source":"fg","repo":"https:\/\/github.com\/quillpad\/quillpad","_id":"note_taking_apps"},{"id":"com.x8bit.bitwarden","label":"Bitwarden","reason":"Regular security audits https:\/\/bitwarden.com\/help\/is-bitwarden-audited\/#third-party-security-audits.\nRecommended to download from their official F-Droid repo https:\/\/mobileapp.bitwarden.com\/fdroid\/.","source":"g","repo":"https:\/\/github.com\/bitwarden\/mobile","_id":"password_managers"},{"id":"de.danoeh.antennapod","label":"AntennaPod","source":"fg","repo":"https:\/\/github.com\/AntennaPod\/AntennaPod","_id":"podcasts"},{"id":"com.nononsenseapps.feeder","label":"Feeder","source":"fg","repo":"https:\/\/gitlab.com\/spacecowboy\/Feeder","_id":"rss_readers"},{"id":"com.oasisfeng.island.fdroid","label":"Insular","source":"f","repo":"https:\/\/gitlab.com\/secure-system\/Insular","_id":"sandboxing_apps"},{"id":"com.bnyro.recorder","label":"Record You","repo":"https:\/\/github.com\/Bnyro\/RecordYou","_id":"screen_recorders"},{"id":"org.localsend.localsend_app","label":"Local Send","source":"fg","repo":"https:\/\/github.com\/localsend\/localsend","reason":"Supports every major operating systems","_id":"sharing_apps"},{"id":"dev.octoshrimpy.quik","label":"QUIK SMS","source":"f","repo":"https:\/\/github.com\/octoshrimpy\/quik","_id":"sms"},{"id":"com.github.libretube","label":"LibreTube","source":"f","repo":"https:\/\/github.com\/libre-tube\/LibreTube","_id":"streaming_apps"},{"id":"com.odysee.floss","label":"Odysee","source":"f","repo":"https:\/\/github.com\/OdyseeTeam\/odysee-android-floss","_id":"streaming_apps"},{"id":"org.schabi.newpipe","label":"NewPipe","source":"f","repo":"https:\/\/github.com\/TeamNewPipe\/NewPipe","_id":"streaming_apps"},{"id":"org.tasks","label":"Tasks","source":"fg","repo":"https:\/\/github.com\/tasks\/tasks","_id":"task_managers"},{"id":"com.bnyro.translate","label":"Translate You","source":"f","repo":"https:\/\/github.com\/Bnyro\/TranslateYou","_id":"translators"},{"id":"com.github.olga_yakovleva.rhvoice.android","label":"RHVoice","source":"f","repo":"https:\/\/github.com\/RHVoice\/RHVoice","reason":"Good voice quality, but most voices are non-free as they prohibit commercial use.","_id":"tts"},{"id":"com.reecedunn.espeak","label":"eSpeak","source":"fg","repo":"https:\/\/github.com\/espeak-ng\/espeak-ng","reason":"Speech quality is not very good and sounds more robotic.","_id":"tts"},{"id":"org.videolan.vlc","label":"VLC","source":"fg","repo":"https:\/\/code.videolan.org\/videolan\/vlc-android","_id":"video_players"},{"id":"net.mullvad.mullvadvpn","label":"Mullvad VPN","reason":"https:\/\/mullvad.net\/en\/why-mullvad-vpn","source":"fg","repo":"https:\/\/github.com\/mullvad\/mullvadvpn-app","_id":"vpn_services"},{"id":"org.breezyweather","label":"Breezy Weather","repo":"https:\/\/github.com\/breezy-weather\/breezy-weather","_id":"weather_apps"}] ================================================ FILE: app/src/main/cpp/AhoCorasick.cpp ================================================ // SPDX-License-Identifier: GPL-3.0-or-later #include #include #include #include #include #include #include #include #include #include "AhoCorasick.h" void AhoCorasick::buildTrie(const std::vector &patterns) { for (int i = 0; i < (int) patterns.size(); ++i) { const std::string &pat = patterns[i]; TrieNode *node = root; for (char c: pat) { if (!node->children.count(c)) node->children[c] = new TrieNode(); node = node->children[c]; } node->output.push_back(i); } } void AhoCorasick::buildFailureLinks() { std::queue q; root->fail = root; for (auto &pair: root->children) { pair.second->fail = root; q.push(pair.second); } while (!q.empty()) { TrieNode *current = q.front(); q.pop(); for (auto &pair: current->children) { char c = pair.first; TrieNode *child = pair.second; TrieNode *f = current->fail; while (f != root && !f->children.count(c)) { f = f->fail; } if (f->children.count(c) && f->children[c] != child) { child->fail = f->children[c]; } else { child->fail = root; } child->output.insert(child->output.end(), child->fail->output.begin(), child->fail->output.end()); q.push(child); } } } std::vector AhoCorasick::search(const std::string &text) const { std::vector matches; TrieNode *node = root; for (char c: text) { while (node != root && !node->children.count(c)) { node = node->fail; } if (node->children.count(c)) { node = node->children.at(c); } matches.insert(matches.end(), node->output.begin(), node->output.end()); } return matches; } void AhoCorasick::freeNodes(TrieNode* node) { if (!node) return; std::stack stack; stack.push(node); while (!stack.empty()) { TrieNode* tmpNode = stack.top(); stack.pop(); // Push children onto stack before deleting the current tmpNode for (auto& pair : tmpNode->children) { stack.push(pair.second); } delete tmpNode; } } ================================================ FILE: app/src/main/cpp/AhoCorasick.h ================================================ // SPDX-License-Identifier: GPL-3.0-or-later #ifndef MUNTASHIRAKON_AHOCORASICK_H #define MUNTASHIRAKON_AHOCORASICK_H #include #include struct TrieNode { std::unordered_map children; TrieNode* fail; std::vector output; TrieNode() : fail(nullptr) {} }; class AhoCorasick { public: AhoCorasick() : root(new TrieNode()) {} ~AhoCorasick() { freeNodes(root); } void buildTrie(const std::vector& patterns); void buildFailureLinks(); std::vector search(const std::string& text) const; private: TrieNode* root; void freeNodes(TrieNode* node); }; #endif //MUNTASHIRAKON_AHOCORASICK_H ================================================ FILE: app/src/main/cpp/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.4.1) set(CMAKE_CXX_STANDARD 17) set(C_FLAGS "-Werror=format -fdata-sections -ffunction-sections -fno-exceptions -fno-rtti -fno-threadsafe-statics") set(LINKER_FLAGS "-Wl,--hash-style=both -Wl,--build-id=none") if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug") message("Builing Release...") set(C_FLAGS "${C_FLAGS} -O2 -fvisibility=hidden -fvisibility-inlines-hidden") set(LINKER_FLAGS "${LINKER_FLAGS} -Wl,-exclude-libs,ALL -Wl,--gc-sections") else() message("Builing Debug...") add_definitions(-DDEBUG) endif () set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${C_FLAGS}") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${C_FLAGS}") set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${LINKER_FLAGS}") set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} ${LINKER_FLAGS}") find_library(log-lib log) add_library(am SHARED AhoCorasick.cpp io_github_muntashirakon_algo_AhoCorasick.cpp io_github_muntashirakon_AppManager_utils_CpuUtils.cpp io_github_muntashirakon_compat_system_OsCompat.cpp) target_link_libraries(am ${log-lib}) if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug") add_custom_command(TARGET am POST_BUILD COMMAND ${CMAKE_STRIP} --remove-section=.comment "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/libam.so") endif () ================================================ FILE: app/src/main/cpp/io_github_muntashirakon_AppManager_utils_CpuUtils.cpp ================================================ // SPDX-License-Identifier: GPL-3.0-or-later #include #include #include #include "io_github_muntashirakon_AppManager_utils_CpuUtils.h" extern "C" JNIEXPORT jlong JNICALL Java_io_github_muntashirakon_AppManager_utils_CpuUtils_getClockTicksPerSecond (JNIEnv *, jclass) { return sysconf(_SC_CLK_TCK); } extern "C" JNIEXPORT jstring JNICALL Java_io_github_muntashirakon_AppManager_utils_CpuUtils_getCpuModel(JNIEnv *env, jclass) { #if defined(__x86_64__) || defined(__i386__) unsigned int eax, ebx, ecx, edx; char cpuModel[48]; // Call CPUID with EAX=0x80000002, 0x80000003, 0x80000004 to get the CPU model for (int i = 0; i < 3; i++) { asm volatile("cpuid" : "=a" (eax), "=b" (ebx), "=c" (ecx), "=d" (edx) : "a" (0x80000002 + i)); memcpy(cpuModel + i * 16, &eax, 4); memcpy(cpuModel + i * 16 + 4, &ebx, 4); memcpy(cpuModel + i * 16 + 8, &ecx, 4); memcpy(cpuModel + i * 16 + 12, &edx, 4); } return env->NewStringUTF(cpuModel); #else return 0; #endif } ================================================ FILE: app/src/main/cpp/io_github_muntashirakon_AppManager_utils_CpuUtils.h ================================================ /* DO NOT EDIT THIS FILE - it is machine generated */ #include /* Header for class io_github_muntashirakon_AppManager_utils_CpuUtils */ #ifndef _Included_io_github_muntashirakon_AppManager_utils_CpuUtils #define _Included_io_github_muntashirakon_AppManager_utils_CpuUtils #ifdef __cplusplus extern "C" { #endif /* * Class: io_github_muntashirakon_AppManager_utils_CpuUtils * Method: getClockTicksPerSecond * Signature: ()J */ JNIEXPORT jlong JNICALL Java_io_github_muntashirakon_AppManager_utils_CpuUtils_getClockTicksPerSecond (JNIEnv *, jclass); /* * Class: io_github_muntashirakon_AppManager_utils_CpuUtils * Method: getCpuModel * Signature: ()java/lang/String; */ JNIEXPORT jstring JNICALL Java_io_github_muntashirakon_AppManager_utils_CpuUtils_getCpuModel (JNIEnv *, jclass); #ifdef __cplusplus } #endif #endif ================================================ FILE: app/src/main/cpp/io_github_muntashirakon_algo_AhoCorasick.cpp ================================================ // SPDX-License-Identifier: GPL-3.0-or-later #include #include #include #include #include #include #include #include #include "AhoCorasick.h" #include "io_github_muntashirakon_algo_AhoCorasick.h" static std::mutex mutex; static std::atomic lastId(0); static std::map instances; extern "C" JNIEXPORT jlong JNICALL Java_io_github_muntashirakon_algo_AhoCorasick_createNative(JNIEnv *env, jobject, jobjectArray patternArray) { jsize len = env->GetArrayLength(patternArray); std::vector patterns(len); for (jsize i = 0; i < len; ++i) { auto jstr = (jstring) env->GetObjectArrayElement(patternArray, i); const char *chars = env->GetStringUTFChars(jstr, nullptr); patterns[i] = chars; env->ReleaseStringUTFChars(jstr, chars); env->DeleteLocalRef(jstr); } auto ac = new AhoCorasick(); ac->buildTrie(patterns); ac->buildFailureLinks(); long long id = ++lastId; { std::lock_guard lock(mutex); instances[id] = ac; } return (jlong) id; } extern "C" JNIEXPORT jintArray JNICALL Java_io_github_muntashirakon_algo_AhoCorasick_searchNative(JNIEnv *env, jobject, jlong instance_id, jstring text) { AhoCorasick *ac = nullptr; { std::lock_guard lock(mutex); if (instances.count(instance_id) == 0) return nullptr; ac = instances[instance_id]; } const char *ctext = env->GetStringUTFChars(text, nullptr); std::string input(ctext); env->ReleaseStringUTFChars(text, ctext); std::vector matches = ac->search(input); jintArray result = env->NewIntArray(matches.size()); if (!matches.empty()) { env->SetIntArrayRegion(result, 0, matches.size(), matches.data()); } return result; } extern "C" JNIEXPORT void JNICALL Java_io_github_muntashirakon_algo_AhoCorasick_destroyNative(JNIEnv *, jobject, jlong instance_id) { std::lock_guard lock(mutex); auto it = instances.find(instance_id); if (it != instances.end()) { delete it->second; instances.erase(it); } } ================================================ FILE: app/src/main/cpp/io_github_muntashirakon_algo_AhoCorasick.h ================================================ /* DO NOT EDIT THIS FILE - it is machine generated */ #include /* Header for class io_github_muntashirakon_algo_AhoCorasick */ #ifndef _Included_io_github_muntashirakon_algo_AhoCorasick #define _Included_io_github_muntashirakon_algo_AhoCorasick #ifdef __cplusplus extern "C" { #endif /* * Class: io_github_muntashirakon_algo_AhoCorasick * Method: createNative * Signature: ([Ljava/lang/String;)J */ extern "C" JNIEXPORT jlong JNICALL Java_io_github_muntashirakon_algo_AhoCorasick_createNative (JNIEnv *, jobject, jobjectArray); /* * Class: io_github_muntashirakon_algo_AhoCorasick * Method: searchNative * Signature: (JLjava/lang/String;)[I */ extern "C" JNIEXPORT jintArray JNICALL Java_io_github_muntashirakon_algo_AhoCorasick_searchNative (JNIEnv *, jobject, jlong, jstring); /* * Class: io_github_muntashirakon_algo_AhoCorasick * Method: searchNative * Signature: (J)V */ extern "C" JNIEXPORT void JNICALL Java_io_github_muntashirakon_algo_AhoCorasick_destroyNative (JNIEnv *, jobject, jlong); #ifdef __cplusplus } #endif #endif ================================================ FILE: app/src/main/cpp/io_github_muntashirakon_compat_system_OsCompat.cpp ================================================ // SPDX-License-Identifier: GPL-3.0-or-later #include #include #include #include #include #include #include #include #include "io_github_muntashirakon_compat_system_OsCompat.h" // Converted from https://github.com/zhanghai/MaterialFiles/blob/faf6c1fe526e0bae3048070a8d1b742ff62c8e6f/app/src/main/jni/syscalls.c // Copyright (c) 2018 Hai Zhang // Checks errno when return value is NULL. #define TEMP_FAILURE_RETRY_N(exp) ({ \ __typeof__(exp) _rc; \ do { \ errno = 0; \ _rc = (exp); \ } while (!_rc && errno == EINTR); \ if (_rc) { \ errno = 0; \ } \ _rc; }) // Always checks errno and ignores return value. #define TEMP_FAILURE_RETRY_V(exp) ({ \ do { \ errno = 0; \ (exp); \ } while (errno == EINTR); }) #define AID_APP_START 10000 // API < 26 does not have the functions #if __ANDROID_API__ < __ANDROID_API_O__ static __thread gid_t getgrentGid = AID_APP_START; static __thread uid_t getpwentUid = AID_APP_START; void setgrent() { getgrentGid = 0; } struct group *getgrent() { while (getgrentGid < AID_APP_START) { struct group *group = getgrgid(getgrentGid); ++getgrentGid; errno = 0; if (group) { return group; } } return NULL; } void endgrent() { setgrent(); } void setpwent() { getpwentUid = 0; } struct passwd *getpwent() { while (getpwentUid < AID_APP_START) { struct passwd *passwd = getpwuid(getpwentUid); ++getpwentUid; errno = 0; if (passwd) { return passwd; } } return NULL; } void endpwent() { setpwent(); } #endif static jclass findClass(JNIEnv *env, const char *name) { jclass localClass = env->FindClass(name); if (!localClass) { abort(); } jclass globalClass = reinterpret_cast(env->NewGlobalRef(localClass)); env->DeleteLocalRef(localClass); if (!globalClass) { abort(); } return globalClass; } static jclass getErrnoExceptionClass(JNIEnv *env) { static jclass errnoExceptionClass = NULL; if (!errnoExceptionClass) { errnoExceptionClass = findClass(env, "android/system/ErrnoException"); } return errnoExceptionClass; } static void throwException(JNIEnv *env, jclass exceptionClass, jmethodID constructor3, jmethodID constructor2, const char *functionName, int error) { jthrowable cause = NULL; if (env->ExceptionCheck()) { cause = env->ExceptionOccurred(); env->ExceptionClear(); } jstring detailMessage = env->NewStringUTF(functionName); if (!detailMessage) { env->ExceptionClear(); } jobject exception; if (cause) { exception = env->NewObject(exceptionClass, constructor3, detailMessage, error, cause); } else { exception = env->NewObject(exceptionClass, constructor2, detailMessage, error); } env->Throw((jthrowable) exception); if (detailMessage) { env->DeleteLocalRef(detailMessage); } } static void throwErrnoException(JNIEnv* env, const char* functionName) { int error = errno; static jmethodID constructor3 = NULL; if (!constructor3) { constructor3 = env->GetMethodID(getErrnoExceptionClass(env), "", "(Ljava/lang/String;ILjava/lang/Throwable;)V"); } static jmethodID constructor2 = NULL; if (!constructor2) { constructor2 = env->GetMethodID(getErrnoExceptionClass(env), "", "(Ljava/lang/String;I)V"); } throwException(env, getErrnoExceptionClass(env), constructor3, constructor2, functionName, error); } static jclass getStringClass(JNIEnv *env) { static jclass stringClass = NULL; if (!stringClass) { stringClass = findClass(env, "java/lang/String"); } return stringClass; } static jclass getStructGroupClass(JNIEnv *env) { static jclass structGroupClass = NULL; if (!structGroupClass) { structGroupClass = findClass(env, "io/github/muntashirakon/compat/system/StructGroup"); } return structGroupClass; } static jclass getStructPasswdClass(JNIEnv *env) { static jclass structPasswdClass = NULL; if (!structPasswdClass) { structPasswdClass = findClass(env, "android/system/StructPasswd"); } return structPasswdClass; } static jclass getStructTimespecClass(JNIEnv *env) { static jclass structTimespecClass = NULL; if (!structTimespecClass) { structTimespecClass = findClass(env, "io/github/muntashirakon/compat/system/StructTimespec"); } return structTimespecClass; } static jclass getOsCompatClass(JNIEnv *env) { static jclass osCompatClass = NULL; if (!osCompatClass) { osCompatClass = findClass(env, "io/github/muntashirakon/compat/system/OsCompat"); } return osCompatClass; } static jobject newStructGroup(JNIEnv *env, const struct group *group) { static jmethodID constructor = NULL; if (!constructor) { constructor = env->GetMethodID(getStructGroupClass(env), "", "(Ljava/lang/String;Ljava/lang/String;I[Ljava/lang/String;)V"); } jstring gr_name = env->NewStringUTF(group->gr_name); jstring gr_passwd = env->NewStringUTF(group->gr_passwd); jobjectArray gr_mem; if (group->gr_mem) { jsize gr_memLength = 0; for (char **gr_memIterator = group->gr_mem; *gr_memIterator; ++gr_memIterator) { ++gr_memLength; } gr_mem = env->NewObjectArray(gr_memLength, getStringClass(env), NULL); if (!gr_mem) { return NULL; } jsize gr_memIndex = 0; for (char **gr_memIterator = group->gr_mem; *gr_memIterator; ++gr_memIterator, ++gr_memIndex) { jstring gr_memElement = env->NewStringUTF(*gr_memIterator); if (!gr_memElement) { return NULL; } env->SetObjectArrayElement(gr_mem, gr_memIndex, gr_memElement); env->DeleteLocalRef(gr_memElement); } } else { gr_mem = NULL; } jobject struct_passwd = env->NewObject(getStructGroupClass(env), constructor, gr_name, gr_passwd, group->gr_gid, gr_mem); if (gr_name) { env->DeleteLocalRef(gr_name); } if (gr_passwd) { env->DeleteLocalRef(gr_passwd); } return struct_passwd; } static jobject newStructPasswd(JNIEnv *env, const struct passwd *passwd) { static jmethodID constructor = NULL; if (!constructor) { constructor = env->GetMethodID(getStructPasswdClass(env), "", "(Ljava/lang/String;IILjava/lang/String;Ljava/lang/String;)V"); } jstring pw_name = env->NewStringUTF(passwd->pw_name); jstring pw_dir = env->NewStringUTF(passwd->pw_dir); jstring pw_shell = env->NewStringUTF(passwd->pw_shell); jobject struct_passwd = env->NewObject(getStructPasswdClass(env), constructor, pw_name, passwd->pw_uid, passwd->pw_gid, pw_dir, pw_shell); if (pw_name) { env->DeleteLocalRef(pw_name); } if (pw_dir) { env->DeleteLocalRef(pw_dir); } if (pw_shell) { env->DeleteLocalRef(pw_shell); } return struct_passwd; } static struct timespec javaStructTimespecToTimespec(JNIEnv *env, jobject obj) { static jfieldID tv_sec = NULL; static jfieldID tv_nsec = NULL; if (!tv_sec) { tv_sec = env->GetFieldID(getStructTimespecClass(env), "tv_sec", "J"); } if (!tv_nsec) { tv_nsec = env->GetFieldID(getStructTimespecClass(env), "tv_nsec", "J"); } struct timespec time; time.tv_sec = (time_t) env->GetLongField(obj, tv_sec); time.tv_nsec = env->GetLongField(obj, tv_nsec); return time; } /** OsCompat **/ JNIEXPORT void JNICALL Java_io_github_muntashirakon_compat_system_OsCompat_setgrent (JNIEnv *env, jclass clazz) { TEMP_FAILURE_RETRY_V(setgrent()); if (errno) { throwErrnoException(env, "setgrent"); } } JNIEXPORT void JNICALL Java_io_github_muntashirakon_compat_system_OsCompat_setpwent (JNIEnv *env, jclass clazz) { TEMP_FAILURE_RETRY_V(setpwent()); if (errno) { throwErrnoException(env, "setpwent"); } } JNIEXPORT jobject JNICALL Java_io_github_muntashirakon_compat_system_OsCompat_getgrent (JNIEnv *env, jclass clazz) { while (true) { struct group *group = TEMP_FAILURE_RETRY_N(getgrent()); if (errno) { throwErrnoException(env, "getgrent"); return NULL; } if (!group) { return NULL; } if (group->gr_name[0] == 'o' && group->gr_name[1] == 'e' && group->gr_name[2] == 'm' && group->gr_name[3] == '_') { continue; } if (group->gr_name[0] == 'u' && (group->gr_name[1] >= '0' && group->gr_name[1] <= '9')) { return NULL; } if (group->gr_name[0] == 'a' && group->gr_name[1] == 'l' && group->gr_name[2] == 'l' && group->gr_name[3] == '_' && group->gr_name[4] == 'a' && (group->gr_name[5] >= '0' && group->gr_name[5] <= '9')) { return NULL; } return newStructGroup(env, group); } } JNIEXPORT jobject JNICALL Java_io_github_muntashirakon_compat_system_OsCompat_getpwent (JNIEnv *env, jclass clazz) { while (true) { struct passwd *passwd = TEMP_FAILURE_RETRY_N(getpwent()); if (errno) { throwErrnoException(env, "getpwent"); return NULL; } if (!passwd) { return NULL; } if (passwd->pw_name[0] == 'o' && passwd->pw_name[1] == 'e' && passwd->pw_name[2] == 'm' && passwd->pw_name[3] == '_') { continue; } if (passwd->pw_name[0] == 'u' && passwd->pw_name[1] >= '0' && passwd->pw_name[1] <= '9') { return NULL; } return newStructPasswd(env, passwd); } } JNIEXPORT void JNICALL Java_io_github_muntashirakon_compat_system_OsCompat_endgrent (JNIEnv *env, jclass clazz) { TEMP_FAILURE_RETRY_V(endgrent()); if (errno) { throwErrnoException(env, "endgrent"); } } JNIEXPORT void JNICALL Java_io_github_muntashirakon_compat_system_OsCompat_endpwent (JNIEnv *env, jclass clazz) { TEMP_FAILURE_RETRY_V(endpwent()); if (errno) { throwErrnoException(env, "endpwent"); } } JNIEXPORT void JNICALL Java_io_github_muntashirakon_compat_system_OsCompat_utimensat (JNIEnv *env, jclass clazz, jint dirfd, jstring pathname, jobject atime, jobject mtime, jint flags) { const char *path = env->GetStringUTFChars(pathname, 0); struct timespec times[2]; times[0] = javaStructTimespecToTimespec(env, atime); times[1] = javaStructTimespecToTimespec(env, mtime); TEMP_FAILURE_RETRY_V(utimensat(dirfd, path, times, flags)); env->ReleaseStringUTFChars(pathname, path); if (errno) { throwErrnoException(env, "utimensat"); } } JNIEXPORT void JNICALL Java_io_github_muntashirakon_compat_system_OsCompat_setNativeConstants (JNIEnv *env, jclass clazz) { jclass osCompatClass = getOsCompatClass(env); jfieldID utime_now = env->GetStaticFieldID(osCompatClass, "UTIME_NOW", "J"); jfieldID utime_omit = env->GetStaticFieldID(osCompatClass, "UTIME_OMIT", "J"); jfieldID at_fdcwd = env->GetStaticFieldID(osCompatClass, "AT_FDCWD", "I"); jfieldID at_symlink_nofollow = env->GetStaticFieldID(osCompatClass, "AT_SYMLINK_NOFOLLOW", "I"); env->SetStaticLongField(osCompatClass, utime_now, UTIME_NOW); env->SetStaticLongField(osCompatClass, utime_omit, UTIME_OMIT); env->SetStaticIntField(osCompatClass, at_fdcwd, AT_FDCWD); env->SetStaticIntField(osCompatClass, at_symlink_nofollow, AT_SYMLINK_NOFOLLOW); } ================================================ FILE: app/src/main/cpp/io_github_muntashirakon_compat_system_OsCompat.h ================================================ /* DO NOT EDIT THIS FILE - it is machine generated */ #include /* Header for class io_github_muntashirakon_compat_system_OsCompat */ #ifndef _Included_io_github_muntashirakon_compat_system_OsCompat #define _Included_io_github_muntashirakon_compat_system_OsCompat #ifdef __cplusplus extern "C" { #endif /* * Class: io_github_muntashirakon_compat_system_OsCompat * Method: setgrent * Signature: ()V */ JNIEXPORT void JNICALL Java_io_github_muntashirakon_compat_system_OsCompat_setgrent (JNIEnv *, jclass); /* * Class: io_github_muntashirakon_compat_system_OsCompat * Method: setpwent * Signature: ()V */ JNIEXPORT void JNICALL Java_io_github_muntashirakon_compat_system_OsCompat_setpwent (JNIEnv *, jclass); /* * Class: io_github_muntashirakon_compat_system_OsCompat * Method: getgrent * Signature: ()Lio/github/muntashirakon/AppManager/compat/StructGroup; */ JNIEXPORT jobject JNICALL Java_io_github_muntashirakon_compat_system_OsCompat_getgrent (JNIEnv *, jclass); /* * Class: io_github_muntashirakon_compat_system_OsCompat * Method: getpwent * Signature: ()Landroid/system/StructPasswd; */ JNIEXPORT jobject JNICALL Java_io_github_muntashirakon_compat_system_OsCompat_getpwent (JNIEnv *, jclass); /* * Class: io_github_muntashirakon_compat_system_OsCompat * Method: endgrent * Signature: ()V */ JNIEXPORT void JNICALL Java_io_github_muntashirakon_compat_system_OsCompat_endgrent (JNIEnv *, jclass); /* * Class: io_github_muntashirakon_compat_system_OsCompat * Method: endpwent * Signature: ()V */ JNIEXPORT void JNICALL Java_io_github_muntashirakon_compat_system_OsCompat_endpwent (JNIEnv *, jclass); /* * Class: io_github_muntashirakon_compat_system_OsCompat * Method: utimensat * Signature: (ILjava/lang/String;Lio/github/muntashirakon/compat/system/StructTimespec;Lio/github/muntashirakon/compat/system/StructTimespec;I)V */ JNIEXPORT void JNICALL Java_io_github_muntashirakon_compat_system_OsCompat_utimensat (JNIEnv *, jclass, jint, jstring, jobject, jobject, jint); /* * Class: io_github_muntashirakon_compat_system_OsCompat * Method: setNativeConstants * Signature: ()V */ JNIEXPORT void JNICALL Java_io_github_muntashirakon_compat_system_OsCompat_setNativeConstants (JNIEnv *, jclass); #ifdef __cplusplus } #endif #endif ================================================ FILE: app/src/main/java/androidx/appcompat/app/PublicTwilightManager.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package androidx.appcompat.app; import android.content.Context; import androidx.annotation.NonNull; public class PublicTwilightManager { public static boolean isNight(@NonNull Context context) { return TwilightManager.getInstance(context).isNight(); } } ================================================ FILE: app/src/main/java/androidx/documentfile/provider/DocumentFileUtils.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package androidx.documentfile.provider; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.net.Uri; import android.provider.DocumentsContract; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.io.File; import java.util.List; import java.util.Objects; import io.github.muntashirakon.io.Paths; public final class DocumentFileUtils { @NonNull public static DocumentFile newTreeDocumentFile(@Nullable DocumentFile parent, @NonNull Context context, @NonNull Uri uri) { return new TreeDocumentFile(parent, context, uri); } public static boolean isSingleDocumentFile(@Nullable DocumentFile documentFile) { return documentFile instanceof SingleDocumentFile; } public static boolean isTreeDocumentFile(@Nullable DocumentFile documentFile) { return documentFile instanceof TreeDocumentFile; } @NonNull public static String resolveAltNameForSaf(@NonNull DocumentFile documentFile) { // For Document Uris, an invalid Uri can return no display name if (DocumentFileUtils.isSingleDocumentFile(documentFile)) { // It's impossible to figure out the correct display name, but since this path is incorrect, // return the full last path segment return documentFile.getUri().getLastPathSegment(); } if (DocumentFileUtils.isTreeDocumentFile(documentFile)) { // The last path segment of the last path segment is the real name return resolveAltNameForTreeUri(documentFile.getUri()); } throw new IllegalArgumentException("Invalid DocumentFile, expected a SAF document."); } public static String resolveAltNameForTreeUri(@NonNull Uri treeUri) { // The last path segment of the last path segment is the real name List segments = treeUri.getPathSegments(); String primaryName = segments.get(1); if (segments.size() == 2) { return primaryName; } String secondaryName = segments.get(3); if (secondaryName.startsWith(primaryName + File.separator)) { secondaryName = Paths.getLastPathSegment(secondaryName.substring(primaryName.length() + 1)); } if (!secondaryName.isEmpty()) { return secondaryName; } throw new IllegalArgumentException("Invalid Uri, expected a tree Uri."); } @Nullable public static ResolveInfo getUriSource(@NonNull Context context, @NonNull Uri uri) { String authority = uri.getAuthority(); if (authority == null) { return null; } PackageManager pm = context.getPackageManager(); Intent intent = new Intent(DocumentsContract.PROVIDER_INTERFACE); List infos = pm.queryIntentContentProviders(intent, 0); for (ResolveInfo info : infos) { if (Objects.equals(authority, info.providerInfo.authority)) { return info; } } return null; } } ================================================ FILE: app/src/main/java/androidx/documentfile/provider/MediaDocumentFile.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package androidx.documentfile.provider; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.database.Cursor; import android.net.Uri; import android.os.Binder; import android.os.Process; import android.provider.MediaStore; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import io.github.muntashirakon.io.Paths; public class MediaDocumentFile extends SingleDocumentFile { private final Context mContext; private final Uri mUri; public MediaDocumentFile(@Nullable DocumentFile parent, Context context, Uri uri) { super(parent, context, uri); mContext = context; mUri = uri; } @Override public boolean isVirtual() { return false; } @Override public boolean canWrite() { boolean writable = super.canWrite(); if (writable) { return true; } if (Binder.getCallingPid() == Process.myPid() || mContext.checkCallingUriPermission(mUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION) == PackageManager.PERMISSION_GRANTED) { // Writing is allowed return true; } // TODO: 15/9/23 Handle actual path in case no write permission is granted // // For media documents, also check if the underlying file is writable as a fallback // String path = getRealPath(mContext, mUri); // return path == null || Paths.get(path).canWrite(); return false; } @Override public boolean exists() { return true; } // @Nullable // private static String getRealPath(@NonNull Context context, @NonNull Uri self) { // final ContentResolver resolver = context.getContentResolver(); // try (Cursor c = resolver.query(self, new String[]{MediaStore.MediaColumns.DATA}, null, null, null)) { // if (c != null && c.moveToFirst() && !c.isNull(0)) { // return c.getString(0); // } else { // return null; // } // } catch (Exception e) { // Log.w(TAG, "Failed query: " + e); // return null; // } // } } ================================================ FILE: app/src/main/java/androidx/documentfile/provider/VirtualDocumentFile.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package androidx.documentfile.provider; import android.net.Uri; import android.os.ParcelFileDescriptor; import android.system.OsConstants; import android.webkit.MimeTypeMap; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.util.Pair; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.channels.FileChannel; import java.util.Objects; import io.github.muntashirakon.io.Paths; import io.github.muntashirakon.io.UidGidPair; import io.github.muntashirakon.io.fs.VirtualFileSystem; // Mother of all virtual documents public class VirtualDocumentFile extends DocumentFile { @Nullable public static Pair parseUri(@NonNull Uri uri) { try { return new Pair<>(Integer.decode(uri.getAuthority()), uri.getPath()); } catch (NumberFormatException e) { return null; } } @NonNull private final VirtualFileSystem mFs; @NonNull private String mFullPath; public VirtualDocumentFile(@Nullable DocumentFile parent, @NonNull VirtualFileSystem fs) { super(parent); mFs = fs; mFullPath = File.separator; } protected VirtualDocumentFile(@NonNull VirtualDocumentFile parent, @NonNull String displayName) { super(Objects.requireNonNull(parent)); if (displayName.contains(File.separator)) { throw new IllegalArgumentException("displayName cannot contain a separator"); } mFs = parent.mFs; mFullPath = Paths.appendPathSegment(parent.mFullPath, displayName); } @Nullable @Override public DocumentFile createFile(@NonNull String mimeType, @NonNull String displayName) { if (displayName.contains(File.separator)) { // displayName cannot contain a separator return null; } // Tack on extension when valid MIME type provided String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); if (extension != null) { displayName += "." + extension; } String newFilePath = Paths.appendPathSegment(mFullPath, displayName); return mFs.createNewFile(newFilePath) ? new VirtualDocumentFile(this, displayName) : null; } @Nullable @Override public DocumentFile createDirectory(@NonNull String displayName) { if (displayName.contains(File.separator)) { // displayName cannot contain a separator return null; } String newFilePath = Paths.appendPathSegment(mFullPath, displayName); return mFs.mkdir(newFilePath) ? new VirtualDocumentFile(this, displayName) : null; } @NonNull public String getFullPath() { return mFullPath; } @NonNull public VirtualFileSystem getFileSystem() { return mFs; } @NonNull @Override public String getName() { if (mFullPath.equals(File.separator)) { return File.separator; } return Paths.getLastPathSegment(mFullPath); } @Nullable @Override public String getType() { if (mFs.isFile(mFullPath)) { String extension = Paths.getPathExtension(getName()); if (extension == null) { return null; } return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); } else if (mFs.isDirectory(mFullPath)) { return "resource/folder"; } return null; } @Override public boolean isVirtual() { return true; } @Override public boolean isFile() { return mFs.isFile(mFullPath); } @Override public boolean isDirectory() { return mFs.isDirectory(mFullPath); } @Override public boolean exists() { return mFs.checkAccess(mFullPath, OsConstants.F_OK); } @Override public boolean canRead() { return mFs.checkAccess(mFullPath, OsConstants.R_OK); } @Override public boolean canWrite() { return mFs.checkAccess(mFullPath, OsConstants.W_OK); } public int getMode() { return mFs.getMode(mFullPath); } public boolean setMode(int mode) { mFs.setMode(mFullPath, mode); return true; } @Nullable public UidGidPair getUidGid() { return mFs.getUidGid(mFullPath); } public boolean setUidGid(@NonNull UidGidPair uidGidPair) { mFs.setUidGid(mFullPath, uidGidPair.uid, uidGidPair.gid); return true; } @Override public boolean delete() { return mFs.delete(mFullPath); } @NonNull @Override public Uri getUri() { return VirtualFileSystem.getUri(mFs.getFsId(), mFullPath); } @NonNull public FileInputStream openInputStream() throws IOException { return mFs.newInputStream(mFullPath); } @NonNull public FileOutputStream openOutputStream(boolean append) throws IOException { return mFs.newOutputStream(mFullPath, append); } public FileChannel openChannel(int mode) throws IOException { return mFs.openChannel(mFullPath, mode); } @NonNull public ParcelFileDescriptor openFileDescriptor(int mode) throws IOException { return mFs.openFileDescriptor(mFullPath, mode); } @Override public long lastModified() { return mFs.lastModified(mFullPath); } public boolean setLastModified(long millis) { return mFs.setLastModified(mFullPath, millis); } public long lastAccess() { return mFs.lastAccess(mFullPath); } public long creationTime() { return mFs.creationTime(mFullPath); } @Override public long length() { return mFs.length(mFullPath); } @Nullable @Override public VirtualDocumentFile findFile(@NonNull String displayName) { displayName = Paths.sanitize(displayName, true); if (displayName == null || displayName.contains(File.separator)) { return null; } VirtualDocumentFile documentFile = new VirtualDocumentFile(this, displayName); if (documentFile.exists()) { return documentFile; } return null; } @NonNull @Override public VirtualDocumentFile[] listFiles() { String[] children = mFs.list(mFullPath); if (children == null) return new VirtualDocumentFile[0]; VirtualDocumentFile[] documentFiles = new VirtualDocumentFile[children.length]; for (int i = 0; i < children.length; ++i) { documentFiles[i] = new VirtualDocumentFile(this, children[i]); } return documentFiles; } @Override public boolean renameTo(@NonNull String displayName) { if (displayName.contains(File.separator)) { // displayName cannot contain a separator return false; } String parent = Paths.removeLastPathSegment(mFullPath); String newFile = Paths.appendPathSegment(parent, displayName); if(mFs.renameTo(mFullPath, newFile)) { mFullPath = newFile; return true; } return false; } } ================================================ FILE: app/src/main/java/aosp/libcore/util/EmptyArray.java ================================================ // SPDX-License-Identifier: Apache-2.0 package aosp.libcore.util; // Copyright 2006 The Android Open Source Project public final class EmptyArray { private EmptyArray() {} public static final boolean[] BOOLEAN = new boolean[0]; public static final byte[] BYTE = new byte[0]; public static final char[] CHAR = new char[0]; public static final double[] DOUBLE = new double[0]; public static final float[] FLOAT = new float[0]; public static final int[] INT = new int[0]; public static final long[] LONG = new long[0]; public static final Class[] CLASS = new Class[0]; public static final Object[] OBJECT = new Object[0]; public static final String[] STRING = new String[0]; public static final Throwable[] THROWABLE = new Throwable[0]; public static final StackTraceElement[] STACK_TRACE_ELEMENT = new StackTraceElement[0]; public static final java.lang.reflect.Type[] TYPE = new java.lang.reflect.Type[0]; @SuppressWarnings("rawtypes") public static final java.lang.reflect.TypeVariable[] TYPE_VARIABLE = new java.lang.reflect.TypeVariable[0]; } ================================================ FILE: app/src/main/java/aosp/libcore/util/HexEncoding.java ================================================ // SPDX-License-Identifier: Apache-2.0 package aosp.libcore.util; /** * Hexadecimal encoding where each byte is represented by two hexadecimal digits. */ // Copyright 2006 The Android Open Source Project public class HexEncoding { private static final char[] LOWER_CASE_DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; private static final char[] UPPER_CASE_DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; /** Hidden constructor to prevent instantiation. */ private HexEncoding() {} /** * Encodes the provided byte as a two-digit hexadecimal String value. */ public static String encodeToString(byte b, boolean upperCase) { char[] digits = upperCase ? UPPER_CASE_DIGITS : LOWER_CASE_DIGITS; char[] buf = new char[2]; // We always want two digits. buf[0] = digits[(b >> 4) & 0xf]; buf[1] = digits[b & 0xf]; return new String(buf, 0, 2); } /** * Encodes the provided data as a sequence of hexadecimal characters. */ public static char[] encode(byte[] data) { return encode(data, 0, data.length, true /* upperCase */); } /** * Encodes the provided data as a sequence of hexadecimal characters. */ public static char[] encode(byte[] data, boolean upperCase) { return encode(data, 0, data.length, upperCase); } /** * Encodes the provided data as a sequence of hexadecimal characters. */ public static char[] encode(byte[] data, int offset, int len) { return encode(data, offset, len, true /* upperCase */); } /** * Encodes the provided data as a sequence of hexadecimal characters. */ private static char[] encode(byte[] data, int offset, int len, boolean upperCase) { char[] digits = upperCase ? UPPER_CASE_DIGITS : LOWER_CASE_DIGITS; char[] result = new char[len * 2]; for (int i = 0; i < len; i++) { byte b = data[offset + i]; int resultIndex = 2 * i; result[resultIndex] = (digits[(b >> 4) & 0x0f]); result[resultIndex + 1] = (digits[b & 0x0f]); } return result; } /** * Encodes the provided data as a sequence of hexadecimal characters. */ public static String encodeToString(byte[] data) { return encodeToString(data, true /* upperCase */); } /** * Encodes the provided data as a sequence of hexadecimal characters. */ public static String encodeToString(byte[] data, boolean upperCase) { return new String(encode(data, upperCase)); } /** * Decodes the provided hexadecimal string into a byte array. Odd-length inputs * are not allowed. * * Throws an {@code IllegalArgumentException} if the input is malformed. */ public static byte[] decode(String encoded) throws IllegalArgumentException { return decode(encoded.toCharArray()); } /** * Decodes the provided hexadecimal string into a byte array. If {@code allowSingleChar} * is {@code true} odd-length inputs are allowed and the first character is interpreted * as the lower bits of the first result byte. * * Throws an {@code IllegalArgumentException} if the input is malformed. */ public static byte[] decode(String encoded, boolean allowSingleChar) throws IllegalArgumentException { return decode(encoded.toCharArray(), allowSingleChar); } /** * Decodes the provided hexadecimal string into a byte array. Odd-length inputs * are not allowed. * * Throws an {@code IllegalArgumentException} if the input is malformed. */ public static byte[] decode(char[] encoded) throws IllegalArgumentException { return decode(encoded, false); } /** * Decodes the provided hexadecimal string into a byte array. If {@code allowSingleChar} * is {@code true} odd-length inputs are allowed and the first character is interpreted * as the lower bits of the first result byte. * * Throws an {@code IllegalArgumentException} if the input is malformed. */ public static byte[] decode(char[] encoded, boolean allowSingleChar) throws IllegalArgumentException { int encodedLength = encoded.length; int resultLengthBytes = (encodedLength + 1) / 2; byte[] result = new byte[resultLengthBytes]; int resultOffset = 0; int i = 0; if (allowSingleChar) { if ((encodedLength % 2) != 0) { // Odd number of digits -- the first digit is the lower 4 bits of the first result // byte. result[resultOffset++] = (byte) toDigit(encoded, i); i++; } } else { if ((encodedLength % 2) != 0) { throw new IllegalArgumentException("Invalid input length: " + encodedLength); } } for (; i < encodedLength; i += 2) { result[resultOffset++] = (byte) ((toDigit(encoded, i) << 4) | toDigit(encoded, i + 1)); } return result; } private static int toDigit(char[] str, int offset) throws IllegalArgumentException { // NOTE: that this isn't really a code point in the traditional sense, since we're // just rejecting surrogate pairs outright. int pseudoCodePoint = str[offset]; if ('0' <= pseudoCodePoint && pseudoCodePoint <= '9') { return pseudoCodePoint - '0'; } else if ('a' <= pseudoCodePoint && pseudoCodePoint <= 'f') { return 10 + (pseudoCodePoint - 'a'); } else if ('A' <= pseudoCodePoint && pseudoCodePoint <= 'F') { return 10 + (pseudoCodePoint - 'A'); } throw new IllegalArgumentException("Illegal char: " + str[offset] + " at offset " + offset); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/AppManager.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager; import android.app.Application; import android.content.Context; import android.os.Build; import android.sun.security.provider.JavaKeyStoreProvider; import androidx.annotation.Keep; import com.topjohnwu.superuser.Shell; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.lsposed.hiddenapibypass.HiddenApiBypass; import java.security.Security; import dalvik.system.ZipPathValidator; import io.github.muntashirakon.AppManager.misc.AMExceptionHandler; import io.github.muntashirakon.AppManager.utils.Utils; import io.github.muntashirakon.AppManager.utils.appearance.AppearanceUtils; public class AppManager extends Application { static { Shell.enableVerboseLogging = BuildConfig.DEBUG; Shell.setDefaultBuilder(Shell.Builder.create() .setFlags(Shell.FLAG_MOUNT_MASTER) .setTimeout(10)); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { // We don't rely on the system to detect a zip slip attack ZipPathValidator.clearCallback(); } } @Keep @Override public void onCreate() { super.onCreate(); Thread.setDefaultUncaughtExceptionHandler(new AMExceptionHandler(this)); AppearanceUtils.init(this); Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME); Security.addProvider(new JavaKeyStoreProvider()); Security.addProvider(new BouncyCastleProvider()); } @Keep @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && !Utils.isRoboUnitTest()) { HiddenApiBypass.addHiddenApiExemptions("L"); } } @Override public void onTrimMemory(int level) { super.onTrimMemory(level); if (level >= TRIM_MEMORY_RUNNING_CRITICAL) { StaticDataset.cleanup(); } } @Override public void onLowMemory() { super.onLowMemory(); StaticDataset.cleanup(); } @Override public void onTerminate() { super.onTerminate(); StaticDataset.cleanup(); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/BaseActivity.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager; import android.Manifest; import android.app.KeyguardManager; import android.content.Intent; import android.os.Build; import android.os.Bundle; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.CallSuper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.biometric.BiometricPrompt; import androidx.biometric.BiometricPrompt.AuthenticationResult; import androidx.core.content.ContextCompat; import androidx.lifecycle.ViewModelProvider; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import io.github.muntashirakon.AppManager.compat.BiometricAuthenticatorsCompat; import io.github.muntashirakon.AppManager.crypto.auth.AuthManager; import io.github.muntashirakon.AppManager.crypto.ks.KeyStoreActivity; import io.github.muntashirakon.AppManager.crypto.ks.KeyStoreManager; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.self.filecache.InternalCacheCleanerService; import io.github.muntashirakon.AppManager.self.life.BuildExpiryChecker; import io.github.muntashirakon.AppManager.settings.Ops; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.settings.SecurityAndOpsViewModel; import io.github.muntashirakon.AppManager.utils.UIUtils; public abstract class BaseActivity extends PerProcessActivity { public static final String TAG = BaseActivity.class.getSimpleName(); public static final HashMap ASKED_PERMISSIONS = new HashMap() {{ // (permission, required) pairs if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { put(Manifest.permission.POST_NOTIFICATIONS, false); } }}; public static final String EXTRA_AUTH = "auth"; @Nullable private AlertDialog mAlertDialog; @Nullable private SecurityAndOpsViewModel mViewModel; private boolean mDisplayLoader = true; private BiometricPrompt mBiometricPrompt; @Nullable private Bundle mSavedInstanceState; private final ActivityResultLauncher mKeyStoreActivity = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { // Need authentication and/or verify mode of operation ensureSecurityAndModeOfOp(); }); private final ActivityResultLauncher mPermissionCheckActivity = registerForActivityResult( new ActivityResultContracts.RequestMultiplePermissions(), permissionStatusMap -> { // Run authentication doAuthenticate(mSavedInstanceState); mSavedInstanceState = null; }); @Override protected final void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (Ops.isAuthenticated()) { Log.d(TAG, "Already authenticated."); onAuthenticated(savedInstanceState); initPermissionChecks(false); return; } if (Boolean.TRUE.equals(BuildExpiryChecker.buildExpired())) { // Build has expired BuildExpiryChecker.getBuildExpiredDialog(this, (dialog, which) -> doAuthenticate(savedInstanceState)).show(); return; } // Init permission checks mSavedInstanceState = savedInstanceState; if (!initPermissionChecks(true)) { mSavedInstanceState = null; // Run authentication doAuthenticate(savedInstanceState); } } protected abstract void onAuthenticated(@Nullable Bundle savedInstanceState); @CallSuper @Override protected void onStart() { super.onStart(); if (mViewModel != null && mViewModel.isAuthenticating() && mAlertDialog != null) { if (mDisplayLoader) { mAlertDialog.show(); } else { mAlertDialog.hide(); } } } @CallSuper @Override protected void onStop() { if (mAlertDialog != null) { mAlertDialog.dismiss(); } super.onStop(); } private void doAuthenticate(@Nullable Bundle savedInstanceState) { mViewModel = new ViewModelProvider(this).get(SecurityAndOpsViewModel.class); mBiometricPrompt = new BiometricPrompt(this, ContextCompat.getMainExecutor(this), new BiometricPrompt.AuthenticationCallback() { @Override public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) { super.onAuthenticationError(errorCode, errString); finishAndRemoveTask(); } @Override public void onAuthenticationSucceeded(@NonNull AuthenticationResult result) { super.onAuthenticationSucceeded(result); handleMigrationAndModeOfOp(); } @Override public void onAuthenticationFailed() { super.onAuthenticationFailed(); } }); mAlertDialog = UIUtils.getProgressDialog(this, getString(R.string.initializing), true); Log.d(TAG, "Waiting to be authenticated."); mViewModel.authenticationStatus().observe(this, status -> { switch (status) { case Ops.STATUS_AUTO_CONNECT_WIRELESS_DEBUGGING: Log.d(TAG, "Try auto-connecting to wireless debugging."); mDisplayLoader = false; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { mViewModel.autoConnectWirelessDebugging(); return; } // fall-through case Ops.STATUS_WIRELESS_DEBUGGING_CHOOSER_REQUIRED: Log.d(TAG, "Display wireless debugging chooser (pair or connect)"); mDisplayLoader = false; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { Ops.connectWirelessDebugging(this, mViewModel); return; } // fall-through case Ops.STATUS_ADB_CONNECT_REQUIRED: Log.d(TAG, "Display connect dialog."); mDisplayLoader = false; Ops.connectAdbInput(this, mViewModel); return; case Ops.STATUS_ADB_PAIRING_REQUIRED: Log.d(TAG, "Display pairing dialog."); mDisplayLoader = false; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { Ops.pairAdbInput(this, mViewModel); return; } // fall-through case Ops.STATUS_FAILURE_ADB_NEED_MORE_PERMS: Ops.displayIncompleteUsbDebuggingMessage(this); case Ops.STATUS_SUCCESS: case Ops.STATUS_FAILURE: Log.d(TAG, "Authentication completed."); mViewModel.setAuthenticating(false); if (mAlertDialog != null) mAlertDialog.dismiss(); Ops.setAuthenticated(this, true); onAuthenticated(savedInstanceState); InternalCacheCleanerService.scheduleAlarm(getApplicationContext()); } }); if (!mViewModel.isAuthenticating()) { mViewModel.setAuthenticating(true); // Check KeyStore if (KeyStoreManager.hasKeyStorePassword()) { // We already have a working keystore password. // Only need authentication and/or verify mode of operation. ensureSecurityAndModeOfOp(); return; } Intent keyStoreIntent = new Intent(this, KeyStoreActivity.class) .putExtra(KeyStoreActivity.EXTRA_KS, true); mKeyStoreActivity.launch(keyStoreIntent); } } private void ensureSecurityAndModeOfOp() { if (!Prefs.Privacy.isScreenLockEnabled()) { // No security enabled handleMigrationAndModeOfOp(); return; } if (getIntent().hasExtra(EXTRA_AUTH)) { Log.i(TAG, "Screen lock-bypass enabled."); // Check for auth String auth = getIntent().getStringExtra(EXTRA_AUTH); if (AuthManager.getKey().equals(auth)) { // Auth successful handleMigrationAndModeOfOp(); return; } // else // Invalid authorization key, fallback to security } Log.i(TAG, "Screen lock enabled."); KeyguardManager keyguardManager = (KeyguardManager) getSystemService(KEYGUARD_SERVICE); if (keyguardManager.isKeyguardSecure()) { // Screen lock enabled BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder() .setTitle(getString(R.string.unlock_app_manager)) .setAllowedAuthenticators(new BiometricAuthenticatorsCompat.Builder().allowEverything(true).build()) .build(); mBiometricPrompt.authenticate(promptInfo); } else { // Screen lock disabled UIUtils.displayLongToast(R.string.screen_lock_not_enabled); finishAndRemoveTask(); } } private void handleMigrationAndModeOfOp() { // Authentication was successful Log.d(TAG, "Authenticated"); // Set mode of operation if (mViewModel != null) { mViewModel.setModeOfOps(); } } private boolean initPermissionChecks(boolean checkAll) { List permissionsToBeAsked = new ArrayList<>(ASKED_PERMISSIONS.size()); for (String permission : ASKED_PERMISSIONS.keySet()) { boolean required = Boolean.TRUE.equals(ASKED_PERMISSIONS.get(permission)); if (!SelfPermissions.checkSelfPermission(permission) && (required || checkAll)) { permissionsToBeAsked.add(permission); } } if (!permissionsToBeAsked.isEmpty()) { // Ask required permissions mPermissionCheckActivity.launch(permissionsToBeAsked.toArray(new String[0])); return true; } return false; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/DummyActivity.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager; import android.app.Activity; import android.os.Bundle; public class DummyActivity extends Activity { @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); finish(); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/PerProcessActivity.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager; import android.annotation.SuppressLint; import android.os.Bundle; import android.view.Menu; import androidx.activity.EdgeToEdge; import androidx.annotation.CallSuper; import androidx.annotation.IdRes; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.view.menu.MenuBuilder; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; public class PerProcessActivity extends AppCompatActivity { @CallSuper @Override protected void onCreate(@Nullable Bundle savedInstanceState) { EdgeToEdge.enable(this); super.onCreate(savedInstanceState); } public boolean getTransparentBackground() { return false; } @CallSuper @SuppressLint("RestrictedApi") @Override public boolean onCreateOptionsMenu(Menu menu) { if (menu instanceof MenuBuilder) { ((MenuBuilder) menu).setOptionalIconsVisible(true); } return super.onCreateOptionsMenu(menu); } protected void clearBackStack() { FragmentManager fragmentManager = getSupportFragmentManager(); if (fragmentManager.getBackStackEntryCount() > 0) { FragmentManager.BackStackEntry entry = fragmentManager.getBackStackEntryAt(0); fragmentManager.popBackStackImmediate(entry.getId(), FragmentManager.POP_BACK_STACK_INCLUSIVE); } } protected void removeCurrentFragment(@IdRes int id) { Fragment fragment = getSupportFragmentManager().findFragmentById(id); if (fragment != null) { getSupportFragmentManager() .beginTransaction() .remove(fragment) .commit(); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/StaticDataset.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager; import android.content.Context; import android.content.res.Resources; import android.util.DisplayMetrics; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import androidx.collection.ArrayMap; import androidx.core.os.ConfigurationCompat; import androidx.core.os.LocaleListCompat; import com.google.gson.Gson; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import io.github.muntashirakon.AppManager.db.utils.AppDb; import io.github.muntashirakon.AppManager.debloat.DebloatObject; import io.github.muntashirakon.AppManager.debloat.SuggestionObject; import io.github.muntashirakon.AppManager.misc.VMRuntime; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.FileUtils; import io.github.muntashirakon.algo.AhoCorasick; public class StaticDataset { @Nullable private static AhoCorasick sAhoCorasickTrackerCache; private static String[] sTrackerNames; private static List sDebloatObjects; public static final String ARMEABI_V7A = "armeabi_v7a"; public static final String ARM64_V8A = "arm64_v8a"; public static final String X86 = "x86"; public static final String X86_64 = "x86_64"; public static Map ALL_ABIS = new HashMap<>(); static { ALL_ABIS.put(ARMEABI_V7A, VMRuntime.ABI_ARMEABI_V7A); ALL_ABIS.put(ARM64_V8A, VMRuntime.ABI_ARM64_V8A); ALL_ABIS.put(X86, VMRuntime.ABI_X86); ALL_ABIS.put(X86_64, VMRuntime.ABI_X86_64); } public static final String LDPI = "ldpi"; public static final String MDPI = "mdpi"; public static final String TVDPI = "tvdpi"; public static final String HDPI = "hdpi"; public static final String XHDPI = "xhdpi"; public static final String XXHDPI = "xxhdpi"; public static final String XXXHDPI = "xxxhdpi"; public static final ArrayMap DENSITY_NAME_TO_DENSITY = new ArrayMap(8) { { put(LDPI, DisplayMetrics.DENSITY_LOW); put(MDPI, DisplayMetrics.DENSITY_MEDIUM); put(TVDPI, DisplayMetrics.DENSITY_TV); put(HDPI, DisplayMetrics.DENSITY_HIGH); put(XHDPI, DisplayMetrics.DENSITY_XHIGH); put(XXHDPI, DisplayMetrics.DENSITY_XXHIGH); put(XXXHDPI, DisplayMetrics.DENSITY_XXXHIGH); } }; public static final int DEVICE_DENSITY; static { DEVICE_DENSITY = Resources.getSystem().getDisplayMetrics().densityDpi; } public static final Map LOCALE_RANKING = new HashMap<>(); static { LocaleListCompat localeList = ConfigurationCompat.getLocales(Resources.getSystem().getConfiguration()); for (int i = 0; i < localeList.size(); i++) { LOCALE_RANKING.put(Objects.requireNonNull(localeList.get(i)).getLanguage(), i); } } public static String[] getTrackerCodeSignatures() { return ContextUtils.getContext().getResources().getStringArray(R.array.tracker_signatures); } public static AhoCorasick getSearchableTrackerSignatures() { if (sAhoCorasickTrackerCache == null) { sAhoCorasickTrackerCache = new AhoCorasick(getTrackerCodeSignatures()); } return sAhoCorasickTrackerCache; } public static void cleanup() { if (sAhoCorasickTrackerCache != null) { sAhoCorasickTrackerCache.close(); sAhoCorasickTrackerCache = null; } } public static String[] getTrackerNames() { if (sTrackerNames == null) { sTrackerNames = ContextUtils.getContext().getResources().getStringArray(R.array.tracker_names); } return sTrackerNames; } @WorkerThread public static List getDebloatObjects() { if (sDebloatObjects == null) { sDebloatObjects = loadDebloatObjects(ContextUtils.getContext(), new Gson()); } return sDebloatObjects; } @WorkerThread public static List getDebloatObjectsWithInstalledInfo(@NonNull Context context) { AppDb appDb = new AppDb(); if (sDebloatObjects == null) { sDebloatObjects = loadDebloatObjects(context, new Gson()); } for (DebloatObject debloatObject : sDebloatObjects) { debloatObject.fillInstallInfo(context, appDb); } return sDebloatObjects; } @NonNull @WorkerThread private static List loadDebloatObjects(@NonNull Context context, @NonNull Gson gson) { HashMap> idSuggestionObjectsMap = loadSuggestions(context, gson); String jsonContent = FileUtils.getContentFromAssets(context, "debloat.json"); try { List debloatObjects = Arrays.asList(gson.fromJson(jsonContent, DebloatObject[].class)); int id = 0; for (DebloatObject debloatObject : debloatObjects) { List suggestionObjects = idSuggestionObjectsMap.get(debloatObject.getSuggestionId()); debloatObject.setSuggestions(suggestionObjects); debloatObject.setId(id++); } return debloatObjects; } catch (Throwable e) { e.printStackTrace(); return Collections.emptyList(); } } @NonNull @WorkerThread private static HashMap> loadSuggestions(@NonNull Context context, @NonNull Gson gson) { String jsonContent = FileUtils.getContentFromAssets(context, "suggestions.json"); HashMap> idSuggestionObjectsMap = new HashMap<>(); try { SuggestionObject[] suggestionObjects = gson.fromJson(jsonContent, SuggestionObject[].class); if (suggestionObjects != null) { for (SuggestionObject suggestionObject : suggestionObjects) { List objects = idSuggestionObjectsMap.get(suggestionObject.suggestionId); if (objects == null) { objects = new ArrayList<>(); idSuggestionObjectsMap.put(suggestionObject.suggestionId, objects); } objects.add(suggestionObject); } } } catch (Throwable th) { th.printStackTrace(); } return idSuggestionObjectsMap; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/accessibility/AccessibilityMultiplexer.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.accessibility; import android.os.Bundle; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; public class AccessibilityMultiplexer { private static final int M_INSTALL = 1; private static final int M_UNINSTALL = 1 << 1; private static final int M_CLEAR_CACHE = 1 << 2; private static final int M_CLEAR_DATA = 1 << 3; private static final int M_FORCE_STOP = 1 << 4; private static final int M_NAVIGATE_TO_STORAGE_AND_CACHE = 1 << 5; private static final int M_LEADING_ACTIVITY_TRACKER = 1 << 6; @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, value = { M_INSTALL, M_UNINSTALL, M_CLEAR_CACHE, M_CLEAR_DATA, M_FORCE_STOP, M_NAVIGATE_TO_STORAGE_AND_CACHE, M_LEADING_ACTIVITY_TRACKER, }) private @interface Flags { } private static final AccessibilityMultiplexer sInstance = new AccessibilityMultiplexer(); public static AccessibilityMultiplexer getInstance() { return sInstance; } @Flags private int mFlags = 0; private final Bundle mArgs = new Bundle(); public boolean isInstallEnabled() { return (mFlags & M_INSTALL) != 0; } public boolean isUninstallEnabled() { return (mFlags & M_UNINSTALL) != 0; } public boolean isClearCacheEnabled() { return (mFlags & M_CLEAR_CACHE) != 0; } public boolean isClearDataEnabled() { return (mFlags & M_CLEAR_DATA) != 0; } public boolean isForceStopEnabled() { return (mFlags & M_FORCE_STOP) != 0; } public boolean isNavigateToStorageAndCache() { return (mFlags & M_NAVIGATE_TO_STORAGE_AND_CACHE) != 0; } public boolean isLeadingActivityTracker() { return (mFlags & M_LEADING_ACTIVITY_TRACKER) != 0; } public void clearFlags() { mFlags = 0; } public void enableInstall(boolean enable) { addOrRemoveFlag(M_INSTALL, enable); } public void enableUninstall(boolean enable) { addOrRemoveFlag(M_UNINSTALL, enable); } public void enableClearCache(boolean enable) { addOrRemoveFlag(M_CLEAR_CACHE, enable); } public void enableClearData(boolean enable) { addOrRemoveFlag(M_CLEAR_DATA, enable); } public void enableForceStop(boolean enable) { addOrRemoveFlag(M_FORCE_STOP, enable); } public void enableNavigateToStorageAndCache(boolean enable) { addOrRemoveFlag(M_NAVIGATE_TO_STORAGE_AND_CACHE, enable); } public void enableLeadingActivityTracker(boolean enable) { addOrRemoveFlag(M_LEADING_ACTIVITY_TRACKER, enable); } @Nullable public String getTitleText() { return mArgs.getString("title"); } public void setTitleText(String title) { mArgs.putString("title", title); } private void addOrRemoveFlag(@Flags int flag, boolean add) { if (add) addFlag(flag); else removeFlag(flag); } private void addFlag(@Flags int flag) { mFlags |= flag; } private void removeFlag(@Flags int flag) { mFlags &= ~flag; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/accessibility/BaseAccessibilityService.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.accessibility; import android.accessibilityservice.AccessibilityService; import android.accessibilityservice.AccessibilityServiceInfo; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.SystemClock; import android.provider.Settings; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.util.List; public abstract class BaseAccessibilityService extends AccessibilityService { private Context mContext; public void init(Context context) { mContext = context.getApplicationContext(); } /** * Check if the accessibility service is enabled */ public static boolean isAccessibilityEnabled(@NonNull Context context) { AccessibilityManager accessibilityManager = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); List accessibilityServices = accessibilityManager.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_ALL_MASK); for (AccessibilityServiceInfo info : accessibilityServices) { ComponentName componentName = ComponentName.unflattenFromString(info.getId()); if (componentName.getPackageName().equals(context.getPackageName())) { return true; } } return false; } public void openAccessibilitySettings() { Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); mContext.startActivity(intent); } /** * Simulate click * * @param nodeInfo nodeInfo */ public void performViewClick(@Nullable AccessibilityNodeInfo nodeInfo) { if (nodeInfo == null) { return; } while (nodeInfo != null) { if (nodeInfo.isClickable()) { nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK); break; } nodeInfo = nodeInfo.getParent(); } } /** * Simulate back/return operation */ public void performBackClick() { SystemClock.sleep(500); performGlobalAction(GLOBAL_ACTION_BACK); } /** * Simulate scroll down */ public void performScrollBackward() { SystemClock.sleep(500); performGlobalAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); } /** * Simulate scroll up */ public void performScrollForward() { SystemClock.sleep(500); performGlobalAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); } /** * Find a view by its text * * @param text text * @return View */ public AccessibilityNodeInfo findViewByText(String text) { return findViewByText(text, false); } /** * Find a view by its text * * @param text text * @param clickable Whether the view can be clicked * @return View */ @Nullable public AccessibilityNodeInfo findViewByText(String text, boolean clickable) { AccessibilityNodeInfo accessibilityNodeInfo = getRootInActiveWindow(); if (accessibilityNodeInfo == null) { return null; } List nodeInfoList = accessibilityNodeInfo.findAccessibilityNodeInfosByText(text); if (nodeInfoList != null) { for (AccessibilityNodeInfo nodeInfo : nodeInfoList) { if (nodeInfo.isClickable() == clickable) { return nodeInfo; } nodeInfo.recycle(); } } return null; } /** * Find a view by its ID * * @param id ID in resource ID format i.e. package:id/id_name * @return View */ @Nullable public AccessibilityNodeInfo findViewById(String id) { AccessibilityNodeInfo accessibilityNodeInfo = getRootInActiveWindow(); if (accessibilityNodeInfo == null) { return null; } List nodeInfoList = accessibilityNodeInfo.findAccessibilityNodeInfosByViewId(id); if (nodeInfoList != null) { for (AccessibilityNodeInfo nodeInfo : nodeInfoList) { return nodeInfo; } } return null; } public void clickTextViewByText(String text) { AccessibilityNodeInfo accessibilityNodeInfo = getRootInActiveWindow(); if (accessibilityNodeInfo == null) { return; } List nodeInfoList = accessibilityNodeInfo.findAccessibilityNodeInfosByText(text); if (nodeInfoList != null && !nodeInfoList.isEmpty()) { for (AccessibilityNodeInfo nodeInfo : nodeInfoList) { performViewClick(nodeInfo); nodeInfo.recycle(); break; } } } public void clickTextViewByID(String id) { AccessibilityNodeInfo accessibilityNodeInfo = getRootInActiveWindow(); if (accessibilityNodeInfo == null) { return; } List nodeInfoList = accessibilityNodeInfo.findAccessibilityNodeInfosByViewId(id); if (nodeInfoList != null) { for (AccessibilityNodeInfo nodeInfo : nodeInfoList) { performViewClick(nodeInfo); nodeInfo.recycle(); break; } } } /** * Set input text * * @param nodeInfo nodeInfo * @param text text */ public void inputText(AccessibilityNodeInfo nodeInfo, String text) { Bundle arguments = new Bundle(); arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text); nodeInfo.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments); } @Nullable protected static AccessibilityNodeInfo findViewByText(@Nullable AccessibilityNodeInfo accessibilityNodeInfo, @NonNull String text, boolean clickable) { if (accessibilityNodeInfo == null) return null; List nodeInfoList = accessibilityNodeInfo.findAccessibilityNodeInfosByText(text); if (nodeInfoList != null) { for (AccessibilityNodeInfo nodeInfo : nodeInfoList) { if (nodeInfo.isClickable() == clickable) { return nodeInfo; } nodeInfo.recycle(); } } return null; } @Nullable protected static AccessibilityNodeInfo findViewByTextRecursive(@Nullable AccessibilityNodeInfo accessibilityNodeInfo, @NonNull String text) { if (accessibilityNodeInfo == null) return null; List nodeInfoList = accessibilityNodeInfo.findAccessibilityNodeInfosByText(text); if (nodeInfoList != null) { for (AccessibilityNodeInfo nodeInfo : nodeInfoList) { return nodeInfo; } } for (int i = 0; i < accessibilityNodeInfo.getChildCount(); ++i) { AccessibilityNodeInfo nodeInfo = findViewByTextRecursive(accessibilityNodeInfo.getChild(i), text); if (nodeInfo != null) { return nodeInfo; } } return null; } @Nullable protected static AccessibilityNodeInfo findViewByClassName(@Nullable AccessibilityNodeInfo nodeInfo, @NonNull CharSequence className) { if (nodeInfo == null) return null; for (int i = 0; i < nodeInfo.getChildCount(); ++i) { AccessibilityNodeInfo child = nodeInfo.getChild(i); if (className.equals(child.getClassName())) { return child; } child.recycle(); } return null; } protected static void waitUntilEnabled(@NonNull AccessibilityNodeInfo nodeInfo, int timesWait) { if (timesWait == 0) timesWait = 10; // Wait 5 seconds while (!nodeInfo.isEnabled() && timesWait > 0) { SystemClock.sleep(500); --timesWait; } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/accessibility/NoRootAccessibilityService.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.accessibility; import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.Resources; import android.os.Build; import android.os.SystemClock; import android.text.TextUtils; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import io.github.muntashirakon.AppManager.accessibility.activity.TrackerWindow; import io.github.muntashirakon.AppManager.utils.ResourceUtil; import io.github.muntashirakon.AppManager.utils.appearance.AppearanceUtils; public class NoRootAccessibilityService extends BaseAccessibilityService { private static final CharSequence SETTING_PACKAGE = "com.android.settings"; private static final CharSequence INSTALLER_PACKAGE = "com.android.packageinstaller"; private final AccessibilityMultiplexer mMultiplexer = AccessibilityMultiplexer.getInstance(); private PackageManager mPm; private int mTries = 1; @Nullable private TrackerWindow mTrackerWindow; @Override public void onCreate() { super.onCreate(); mPm = AppearanceUtils.getSystemContext(this).getPackageManager(); } @Override public void onAccessibilityEvent(AccessibilityEvent event) { if (mMultiplexer.isLeadingActivityTracker()) { if (mTrackerWindow == null) { mTrackerWindow = new TrackerWindow(this); } mTrackerWindow.showOrUpdate(AccessibilityEvent.obtain(event)); } else if (mTrackerWindow != null) { mTrackerWindow.dismiss(); mTrackerWindow = null; } if (event.getEventType() != AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { return; } CharSequence packageName = event.getPackageName(); if (INSTALLER_PACKAGE.equals(packageName)) { automateInstallationUninstallation(event); return; } if (SETTING_PACKAGE.equals(packageName)) { // Clear data/cache, force-stop if (event.getClassName().equals("com.android.settings.applications.InstalledAppDetailsTop")) { AccessibilityNodeInfo node = findViewByText(getString(event, "force_stop"), true); if (mMultiplexer.isForceStopEnabled()) { if (node != null) { if (node.isEnabled()) { mTries = 0; performViewClick(node); } else if (mTries > 0 && navigateToStorageAndCache(event)) { // Hack to enable force-stop when it is disabled due to Android bug performBackClick(); --mTries; } else performBackClick(); node.recycle(); } else performBackClick(); } else if (mMultiplexer.isNavigateToStorageAndCache()) { SystemClock.sleep(1000); navigateToStorageAndCache(event); } } else if (event.getClassName().equals("com.android.settings.SubSettings") || getString(event, "storage_settings").equals(event.getText().toString())) { if (mMultiplexer.isClearDataEnabled()) { performViewClick(findViewByText(getString(event, "clear_user_data_text"), true)); } if (mMultiplexer.isClearCacheEnabled()) { mMultiplexer.enableClearCache(false); AccessibilityNodeInfo node = findViewByText(getString(event, "clear_cache_btn_text"), true); if (node != null) { if (node.isEnabled()) { performViewClick(node); } performBackClick(); performBackClick(); node.recycle(); } } } else if (event.getClassName().equals("androidx.appcompat.app.AlertDialog")) { if (mMultiplexer.isForceStopEnabled() && findViewByText(getString(event, "force_stop_dlg_title")) != null) { mMultiplexer.enableForceStop(false); mTries = 1; // Restore tries performViewClick(findViewByText(getString(event, "dlg_ok"), true)); performBackClick(); } if (mMultiplexer.isClearDataEnabled() && findViewByText(getString(event, "clear_data_dlg_title")) != null) { mMultiplexer.enableClearData(false); performViewClick(findViewByText(getString(event, "dlg_ok"), true)); performBackClick(); performBackClick(); } } } } @Override public void onInterrupt() { } @Override public boolean onUnbind(Intent intent) { if (mTrackerWindow != null) { mTrackerWindow.dismiss(); mTrackerWindow = null; } return super.onUnbind(intent); } private void automateInstallationUninstallation(@NonNull AccessibilityEvent event) { if (event.getClassName().equals("android.app.Dialog")) { if (mMultiplexer.isInstallEnabled()) { // Install performViewClick(findViewByText(getString(event, "install"), true)); // install_text } } else if (event.getClassName().equals("com.android.packageinstaller.UninstallerActivity")) { if (mMultiplexer.isUninstallEnabled()) { // uninstall performViewClick(findViewByText(getString(event, "ok"), true)); // dlg_ok } } } private boolean navigateToStorageAndCache(AccessibilityEvent event) { String storageSettings; try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { storageSettings = getString(event, "storage_settings_for_app"); } else storageSettings = getString(event, "storage_label"); } catch (Resources.NotFoundException e) { // Failed: non-AOSP device return false; } SystemClock.sleep(500); // It may take a few moments to initialise the Recycler/List views AccessibilityNodeInfo storageNode = findViewByTextRecursive(getRootInActiveWindow(), storageSettings); if (storageNode != null) { mMultiplexer.enableNavigateToStorageAndCache(false); // prevent infinite loop performViewClick(storageNode); storageNode.recycle(); return true; } // Failed performBackClick(); return false; } /** * Return the string value associated with a particular resource ID. It will be stripped of any styled text information. * * @param stringRes The desired resource identifier. * @return String The string data associated with the resource, stripped of styled text information. * @throws Resources.NotFoundException Throws NotFoundException if the given ID or package does not exist. */ private String getString(@NonNull AccessibilityEvent event, @NonNull String stringRes) throws Resources.NotFoundException { CharSequence packageName = event.getPackageName(); CharSequence className = event.getClassName(); if (TextUtils.isEmpty(packageName)) { throw new Resources.NotFoundException("Empty package name"); } ResourceUtil resUtil = new ResourceUtil(); if (!TextUtils.isEmpty(className)) { if (!resUtil.loadResources(mPm, packageName.toString(), className.toString()) && !resUtil.loadResources(mPm, packageName.toString()) && !resUtil.loadAndroidResources()) { throw new Resources.NotFoundException("Couldn't load resources for package: " + packageName + ", class: " + className); } } else if (!resUtil.loadResources(mPm, packageName.toString()) && !resUtil.loadAndroidResources()) { throw new Resources.NotFoundException("Couldn't load resources for package: " + packageName); } return resUtil.getString(stringRes); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/accessibility/activity/LeadingActivityTrackerActivity.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.accessibility.activity; import android.content.Intent; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.provider.Settings; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.Nullable; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import io.github.muntashirakon.AppManager.BaseActivity; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.accessibility.AccessibilityMultiplexer; import io.github.muntashirakon.AppManager.accessibility.NoRootAccessibilityService; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.settings.FeatureController; import io.github.muntashirakon.AppManager.utils.ThreadUtils; public class LeadingActivityTrackerActivity extends BaseActivity { private final ActivityResultLauncher mSettingsLauncher = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { // Init again init(); }); private final ActivityResultLauncher mUsageAccessSettingsLauncher = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { // Init again init(); }); @Override protected void onAuthenticated(@Nullable Bundle savedInstanceState) { init(); } @Override public boolean getTransparentBackground() { return true; } private void init() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(this)) { new MaterialAlertDialogBuilder(this) .setTitle(R.string.grant_required_permission) .setMessage(R.string.grant_overlay_permission_message) .setCancelable(false) .setPositiveButton(R.string.ok, (dialog, which) -> { Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION); intent.setData(Uri.parse("package:" + getPackageName())); mSettingsLauncher.launch(intent); }) .setNegativeButton(R.string.go_back, (dialog, which) -> finish()) .show(); return; } if (!SelfPermissions.checkUsageStatsPermission()) { ThreadUtils.postOnMainThread(() -> new MaterialAlertDialogBuilder(this) .setTitle(R.string.grant_usage_access) .setMessage(R.string.grant_usage_acess_message) .setCancelable(false) .setPositiveButton(R.string.go, (dialog, which) -> { mUsageAccessSettingsLauncher.launch(new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS)); }) .setNegativeButton(R.string.go_back, (dialog, which) -> finish()) .show()); return; } if (!NoRootAccessibilityService.isAccessibilityEnabled(this)) { new MaterialAlertDialogBuilder(this) .setTitle(R.string.grant_required_permission) .setMessage(R.string.grant_accessibility_permission_for_tracking_window_contents) .setCancelable(false) .setPositiveButton(R.string.ok, (dialog, which) -> mSettingsLauncher.launch(new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))) .setNegativeButton(R.string.go_back, (dialog, which) -> finish()) .show(); return; } AccessibilityMultiplexer.getInstance().enableLeadingActivityTracker(true); finish(); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/accessibility/activity/TrackerWindow.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.accessibility.activity; import android.annotation.SuppressLint; import android.app.usage.UsageEvents; import android.content.Context; import android.content.Intent; import android.graphics.PixelFormat; import android.graphics.Point; import android.os.Build; import android.os.UserHandleHidden; import android.text.Editable; import android.text.TextUtils; import android.view.Display; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.android.material.button.MaterialButton; import com.google.android.material.card.MaterialCardView; import com.google.android.material.imageview.ShapeableImageView; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.concurrent.Future; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.accessibility.AccessibilityMultiplexer; import io.github.muntashirakon.AppManager.compat.UsageStatsManagerCompat; import io.github.muntashirakon.AppManager.details.AppDetailsActivity; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.AppManager.utils.Utils; import io.github.muntashirakon.AppManager.utils.appearance.AppearanceUtils; import io.github.muntashirakon.widget.TextInputTextView; public class TrackerWindow implements View.OnTouchListener { public final WindowManager mWindowManager; private final WindowManager.LayoutParams mWindowLayoutParams; private final View mView; private final ShapeableImageView mIconView; private final MaterialCardView mContentView; private final TextInputTextView mPackageNameView; private final TextInputTextView mActivityNameView; private final TextInputTextView mClassNameView; private final TextInputTextView mClassHierarchyView; private final MaterialButton mPlayPauseButton; private final Point mWindowSize = new Point(0, 0); private final Point mWindowPosition = new Point(0, 0); private final Point mPressPosition = new Point(0, 0); private final int mMaxWidth; private boolean mPaused = false; private boolean mIconified = false; private boolean mViewAttached = false; @Nullable private Future mClassHierarchyResult; @SuppressLint("ClickableViewAccessibility") public TrackerWindow(@NonNull Context context) { Context themedContext = AppearanceUtils.getThemedContext(context, true); mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); int type = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY : WindowManager.LayoutParams.TYPE_PHONE; Display display = mWindowManager.getDefaultDisplay(); int displayWidth = display.getWidth(); display.getRealSize(mWindowSize); mMaxWidth = (displayWidth / 2) + 300; // FIXME: 5/2/23 Find a better way to represent a display int flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; mWindowLayoutParams = new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, type, flags, PixelFormat.TRANSLUCENT); mWindowLayoutParams.gravity = Gravity.CENTER; mWindowLayoutParams.width = mMaxWidth; mWindowLayoutParams.windowAnimations = android.R.style.Animation_Toast; mView = View.inflate(themedContext, R.layout.window_activity_tracker, null); mIconView = mView.findViewById(R.id.icon); mContentView = mView.findViewById(R.id.content); mPackageNameView = mView.findViewById(R.id.package_name); mActivityNameView = mView.findViewById(R.id.activity_name); mClassNameView = mView.findViewById(R.id.class_name); mClassHierarchyView = mView.findViewById(R.id.class_hierarchy); mPlayPauseButton = mView.findViewById(R.id.action_play_pause); mPackageNameView.setOnLongClickListener(v -> { Editable packageName = mPackageNameView.getText(); if (TextUtils.isEmpty(packageName)) { return false; } copyText("Package name", packageName); return true; }); mActivityNameView.setOnLongClickListener(v -> { Editable activityName = mActivityNameView.getText(); if (TextUtils.isEmpty(activityName)) { return false; } copyText("Activity name", activityName); return true; }); mClassNameView.setOnLongClickListener(v -> { Editable className = mClassNameView.getText(); if (TextUtils.isEmpty(className)) { return false; } copyText("Class name", className); return true; }); mClassHierarchyView.setOnLongClickListener(v -> { Editable hierarchy = mClassHierarchyView.getText(); if (TextUtils.isEmpty(hierarchy)) { return false; } copyText("Class hierarchy", hierarchy); return true; }); mView.findViewById(R.id.info).setOnClickListener(v -> { Editable packageName = mPackageNameView.getText(); if (TextUtils.isEmpty(packageName)) { return; } Intent appInfoIntent = AppDetailsActivity.getIntent(context, packageName.toString(), UserHandleHidden.myUserId(), true); appInfoIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); try { context.startActivity(appInfoIntent); } catch (Throwable th) { UIUtils.displayLongToast("Error: " + th.getMessage()); } }); mView.findViewById(R.id.mini).setOnClickListener(v -> iconify()); mPlayPauseButton.setOnClickListener(v -> { mPaused = !mPaused; mPlayPauseButton.setIconResource(mPaused ? R.drawable.ic_play_arrow : R.drawable.ic_pause); }); mView.findViewById(android.R.id.closeButton).setOnClickListener(v -> dismiss()); mIconView.setVisibility(View.GONE); mIconView.setOnClickListener(v -> expand()); mView.findViewById(R.id.drag).setOnTouchListener(this); mIconView.setOnTouchListener(this); } @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouch(View v, MotionEvent event) { Point point; int action = event.getAction(); if (action == MotionEvent.ACTION_DOWN) { point = new Point((int) event.getRawX(), (int) event.getRawY()); mPressPosition.set(point.x, point.y); mWindowPosition.set(mWindowLayoutParams.x, mWindowLayoutParams.y); return true; } else if (action == MotionEvent.ACTION_MOVE) { point = new Point((int) event.getRawX(), (int) event.getRawY()); int delX = point.x - mPressPosition.x; int delY = point.y - mPressPosition.y; mWindowLayoutParams.x = mWindowPosition.x + delX; mWindowLayoutParams.y = mWindowPosition.y + delY; updateLayout(); return true; } if (v == mIconView && action == MotionEvent.ACTION_UP) { point = new Point((int) event.getRawX(), (int) event.getRawY()); int delX = Math.abs(point.x - mPressPosition.x); int delY = Math.abs(point.y - mPressPosition.y); if (delX < 1 && delY < 1) { v.performClick(); return true; } } return false; } public void showOrUpdate(AccessibilityEvent event) { if (!mViewAttached) { mViewAttached = true; mWindowManager.addView(mView, mWindowLayoutParams); } if (!mPaused) { @Nullable CharSequence packageName = event.getPackageName(); if (packageName != null && BuildConfig.APPLICATION_ID.contentEquals(packageName)) { // On some devices, this window always gets the focus CharSequence className = event.getClassName(); if (className != null && "android.widget.EditText".contentEquals(className)) { // For some reason, only this class is focused if (event.getSource() == null) { // No class hierarchy. This is the intended event return; } } } if (mClassHierarchyResult != null) { mClassHierarchyResult.cancel(true); } mPackageNameView.setText(packageName); mClassNameView.setText(event.getClassName()); mClassHierarchyResult = ThreadUtils.postOnBackgroundThread(() -> { CharSequence classHierarchy = TextUtils.join("\n", getClassHierarchy(event)); String activityName = getActivityName(event); ThreadUtils.postOnMainThread(() -> { mActivityNameView.setText(activityName); mClassHierarchyView.setText(classHierarchy); }); }); } } public void dismiss() { AccessibilityMultiplexer.getInstance().enableLeadingActivityTracker(false); mViewAttached = false; if (mClassHierarchyResult != null) { mClassHierarchyResult.cancel(true); } try { mWindowManager.removeView(mView); } catch (Exception ignore) { } } private void iconify() { mPaused = true; mIconified = true; // Window position may need to be adjusted to display the icon // (0,0) is middle int height = -mWindowSize.y / 2; if (mWindowLayoutParams.y < height) { mWindowPosition.y = height; mWindowLayoutParams.y = height; } mIconView.setVisibility(View.VISIBLE); mContentView.setVisibility(View.GONE); updateLayout(); } private void expand() { mContentView.setVisibility(View.VISIBLE); mIconView.setVisibility(View.GONE); mPaused = false; mIconified = false; // Window position may need to be adjusted to display the drag handle // (0,0) is middle int width = (-mWindowSize.x + mMaxWidth) / 2; if (mWindowLayoutParams.x < width) { mWindowPosition.x = width; mWindowLayoutParams.x = width; } updateLayout(); } private void updateLayout() { mWindowLayoutParams.width = mIconified ? WindowManager.LayoutParams.WRAP_CONTENT : mMaxWidth; mWindowManager.updateViewLayout(mView, mWindowLayoutParams); } private void copyText(CharSequence label, CharSequence content) { Utils.copyToClipboard(mView.getContext(), label, content); } @Nullable public String getActivityName(@NonNull AccessibilityEvent event) { if (event.getPackageName() == null) { return null; } String packageName = event.getPackageName().toString(); UsageEvents.Event usageEvent = new UsageEvents.Event(); long currentTimeMillis = System.currentTimeMillis(); long timeDiff = 5_000; int tries = 0; do { UsageEvents queryEvents = UsageStatsManagerCompat.queryEvents(currentTimeMillis - timeDiff, currentTimeMillis, UserHandleHidden.myUserId()); if (queryEvents == null) { return null; } long lastTime = 0L; String activityName = null; while (queryEvents.hasNextEvent()) { queryEvents.getNextEvent(usageEvent); if (usageEvent.getEventType() == UsageEvents.Event.ACTIVITY_RESUMED && Objects.equals(packageName, usageEvent.getPackageName()) && lastTime < usageEvent.getTimeStamp()) { lastTime = usageEvent.getTimeStamp(); activityName = usageEvent.getClassName(); } } if (activityName != null) { return activityName; } timeDiff *= 60; } while ((++tries) != 3); return null; } @NonNull private static List getClassHierarchy(@NonNull AccessibilityEvent event) { List classHierarchies = new ArrayList<>(); AccessibilityNodeInfo nodeInfo = event.getSource(); if (nodeInfo != null) { classHierarchies.add(nodeInfo.getClassName()); int depth = 0; while (depth < 20) { // Limit depth to avoid running forever AccessibilityNodeInfo tmpNodeInfo = nodeInfo.getParent(); if (tmpNodeInfo != null) { nodeInfo.recycle(); nodeInfo = tmpNodeInfo; classHierarchies.add(nodeInfo.getClassName()); } else { // Max depth reached break; } ++depth; if (ThreadUtils.isInterrupted()) { return Collections.emptyList(); } } try { if (depth == 20) { classHierarchies.add("..."); } } finally { nodeInfo.recycle(); } } Collections.reverse(classHierarchies); if (ThreadUtils.isInterrupted()) { return Collections.emptyList(); } int size = classHierarchies.size(); if (size <= 1) { return classHierarchies; } classHierarchies.set(0, "┬ " + classHierarchies.get(0)); for (int i = 1; i < size; ++i) { StringBuilder sb = new StringBuilder(); for (int j = 1; j < i; ++j) { sb.append(' '); } if (i != (size - 1)) { sb.append("└┬ "); } else sb.append("└─ "); sb.append(classHierarchies.get(i)); classHierarchies.set(i, sb.toString()); if (ThreadUtils.isInterrupted()) { return Collections.emptyList(); } } return classHierarchies; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/adb/AdbConnectionManager.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.adb; import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import java.security.PrivateKey; import java.security.cert.Certificate; import io.github.muntashirakon.AppManager.crypto.ks.KeyPair; import io.github.muntashirakon.AppManager.crypto.ks.KeyStoreManager; import io.github.muntashirakon.AppManager.crypto.ks.KeyStoreUtils; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.adb.AbsAdbConnectionManager; public class AdbConnectionManager extends AbsAdbConnectionManager { public static final String TAG = AdbConnectionManager.class.getSimpleName(); public static final String ADB_KEY_ALIAS = "adb_rsa"; private static AdbConnectionManager sInstance; public static AdbConnectionManager getInstance() throws Exception { if (sInstance == null) { sInstance = new AdbConnectionManager(); } return sInstance; } @NonNull private final KeyPair mKeyPair; private final MutableLiveData mPairingObserver = new MutableLiveData<>(); public AdbConnectionManager() throws Exception { setApi(Build.VERSION.SDK_INT); KeyStoreManager keyStoreManager = KeyStoreManager.getInstance(); KeyPair keyPair = keyStoreManager.getKeyPairNoThrow(ADB_KEY_ALIAS); if (keyPair == null) { String subject = "CN=App Manager"; keyPair = KeyStoreUtils.generateRSAKeyPair(subject, 2048, System.currentTimeMillis() + 86400000); keyStoreManager.addKeyPair(ADB_KEY_ALIAS, keyPair, true); } mKeyPair = keyPair; } public LiveData getPairingObserver() { return mPairingObserver; } @WorkerThread public void pairLiveData(@NonNull String host, int port, @NonNull String pairingCode) throws Exception { try { ThreadUtils.ensureWorkerThread(); pair(host, port, pairingCode); mPairingObserver.postValue(null); } catch (Exception e) { Log.w(TAG, "Pairing failed.", e); mPairingObserver.postValue(e); throw e; } } @NonNull @Override protected PrivateKey getPrivateKey() { return mKeyPair.getPrivateKey(); } @NonNull @Override protected Certificate getCertificate() { return mKeyPair.getCertificate(); } @NonNull @Override protected String getDeviceName() { return "AppManager"; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/adb/AdbPairingService.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.adb; import static io.github.muntashirakon.AppManager.types.ForegroundService.FOREGROUND_SERVICE_TYPE_DATA_SYNC; import static io.github.muntashirakon.AppManager.types.ForegroundService.FOREGROUND_SERVICE_TYPE_SPECIAL_USE; import android.Manifest; import android.app.Notification; import android.app.PendingIntent; import android.app.Service; import android.content.Intent; import android.os.Build; import android.os.Bundle; import android.os.IBinder; import androidx.annotation.MainThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.core.app.NotificationChannelCompat; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import androidx.core.app.PendingIntentCompat; import androidx.core.app.RemoteInput; import androidx.core.app.ServiceCompat; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Observer; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.servermanager.ServerConfig; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.adb.android.AdbMdns; // This works as follows: // 1. Start searching for a pairing port // 2. A port is found, ask to enter a pairing code // 3. Start pairing // 4. Exit with result, or ask to retry @RequiresApi(Build.VERSION_CODES.R) public class AdbPairingService extends Service { public static final String TAG = AdbPairingService.class.getSimpleName(); public static final String CHANNEL_ID = BuildConfig.APPLICATION_ID + ".channel.ADB_PAIRING"; public static final String ACTION_START_SEARCHING = BuildConfig.APPLICATION_ID + ".action.START_SEARCHING"; public static final String ACTION_STOP_SEARCHING = BuildConfig.APPLICATION_ID + ".action.STOP_SEARCHING"; public static final String ACTION_START_PAIRING = BuildConfig.APPLICATION_ID + ".action.ENTER_CODE"; public static final String EXTRA_PORT = "port"; public static final String INPUT_CODE = "code"; private NotificationCompat.Builder mNotificationBuilder; private boolean mStartedSearching = false; private AdbMdns mAdbMdnsPairing; private final MutableLiveData mAdbPairingPort = new MutableLiveData<>(); private final Observer mAdbPairingPortObserver = port -> { Log.i(TAG, "Found port %d", port); inputPairingCode(port); }; @Override public void onCreate() { super.onCreate(); NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); NotificationChannelCompat notificationChannel = new NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_HIGH) .setName("ADB Pairing") .setSound(null, null) .setShowBadge(false) .build(); notificationManager.createNotificationChannel(notificationChannel); mNotificationBuilder = new NotificationCompat.Builder(this, CHANNEL_ID) .setDefaults(Notification.DEFAULT_ALL) .setLocalOnly(!Prefs.Misc.sendNotificationsToConnectedDevices()) .setContentTitle(getString(R.string.wireless_debugging)) .setSubText(getText(R.string.wireless_debugging)) .setSmallIcon(R.drawable.ic_default_notification) .setPriority(NotificationCompat.PRIORITY_HIGH); } @Override public int onStartCommand(Intent intent, int flags, int startId) { if (intent == null || intent.getAction() == null) { // Invalid intent return START_NOT_STICKY; } switch (intent.getAction()) { case ACTION_START_SEARCHING: startSearching(); return START_REDELIVER_INTENT; case ACTION_START_PAIRING: int port = intent.getIntExtra(EXTRA_PORT, -1); Bundle remoteInputs = RemoteInput.getResultsFromIntent(intent); if (port != -1 && remoteInputs != null) { String code = remoteInputs.getCharSequence(INPUT_CODE, "").toString().trim(); startPairing(port, code); } else { // Wrong inputs, continue searching startSearching(); } return START_REDELIVER_INTENT; case ACTION_STOP_SEARCHING: ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE); stopSelf(); default: return START_NOT_STICKY; } } @Nullable @Override public IBinder onBind(Intent intent) { return null; } @Override public void onDestroy() { super.onDestroy(); if (mStartedSearching) { // Still looking for a port, hence the pairing wasn't successful // Fail intentionally to avoid looping forever Log.i(TAG, "Stop searching for an active port..."); ThreadUtils.postOnBackgroundThread(() -> { try { AdbConnectionManager.getInstance().pairLiveData(ServerConfig.getAdbHost(this), -1, ""); } catch (Exception ignore) { } }); stopSearching(); } } @MainThread private void startSearching() { if (mStartedSearching) { return; } mStartedSearching = true; if (mAdbMdnsPairing == null) { mAdbMdnsPairing = new AdbMdns(getApplication(), AdbMdns.SERVICE_TYPE_TLS_PAIRING, (hostAddress, port) -> { if (port != -1) { mAdbPairingPort.postValue(port); } }); } mAdbPairingPort.observeForever(mAdbPairingPortObserver); PendingIntent stopPendingIntent = getStopIntent(); NotificationCompat.Action stopAction = new NotificationCompat.Action.Builder(null, getString(R.string.adb_pairing_stop_searching), stopPendingIntent).build(); mNotificationBuilder.setContentText(getText(R.string.adb_pairing_searching_for_port)) .clearActions() .addAction(stopAction); ServiceCompat.startForeground(this, 1, mNotificationBuilder.build(), FOREGROUND_SERVICE_TYPE_DATA_SYNC | FOREGROUND_SERVICE_TYPE_SPECIAL_USE); mAdbMdnsPairing.start(); } @MainThread private void inputPairingCode(int port) { Intent inputIntent = new Intent(this, getClass()) .setAction(ACTION_START_PAIRING) .putExtra(EXTRA_PORT, port); PendingIntent inputPendingIntent = PendingIntentCompat.getForegroundService(this, 2, inputIntent, PendingIntent.FLAG_UPDATE_CURRENT, true); RemoteInput pairingCodeInput = new RemoteInput.Builder(INPUT_CODE) .setLabel(getString(R.string.adb_pairing_pairing_code)) .build(); NotificationCompat.Action inputAction = new NotificationCompat.Action.Builder(null, getString(R.string.adb_pairing_input_pairing_code), inputPendingIntent) .addRemoteInput(pairingCodeInput) .build(); mNotificationBuilder.setContentText(getString(R.string.adb_pairing_found_pairing_service_with_port, port)) .clearActions() .addAction(inputAction); if (SelfPermissions.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)) { NotificationManagerCompat.from(this).notify(1, mNotificationBuilder.build()); } } @MainThread private void startPairing(int port, String code) { mNotificationBuilder.setContentText(getString(R.string.adb_pairing_pairing_in_progress)) .clearActions(); ServiceCompat.startForeground(this, 1, mNotificationBuilder.build(), FOREGROUND_SERVICE_TYPE_DATA_SYNC | FOREGROUND_SERVICE_TYPE_SPECIAL_USE); ThreadUtils.postOnBackgroundThread(() -> { boolean isSuccess; try { AdbConnectionManager.getInstance().pairLiveData(ServerConfig.getAdbHost(this), port, code); isSuccess = true; } catch (Exception e) { Log.w(TAG, "Pairing failed.", e); isSuccess = false; } ThreadUtils.postOnMainThread(this::stopSearching); if (isSuccess) { mNotificationBuilder.setContentText(getString(R.string.paired_successfully)).clearActions(); stopSelf(); } else { PendingIntent deleteIntent = getStopIntent(); Intent retryIntent = new Intent(this, getClass()).setAction(ACTION_START_SEARCHING); PendingIntent retryPendingIntent = PendingIntentCompat.getForegroundService(this, 3, retryIntent, 0, false); NotificationCompat.Action retryAction = new NotificationCompat.Action.Builder(null, getString(R.string.adb_pairing_retry_pairing), retryPendingIntent).build(); mNotificationBuilder.setContentText(getString(R.string.failed)) .clearActions() .setDeleteIntent(deleteIntent) .addAction(retryAction); } if (SelfPermissions.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)) { NotificationManagerCompat.from(this).notify(1, mNotificationBuilder.build()); } }); } @MainThread private void stopSearching() { if (!mStartedSearching) { return; } mStartedSearching = false; mAdbMdnsPairing.stop(); mAdbPairingPort.removeObserver(mAdbPairingPortObserver); } @NonNull private PendingIntent getStopIntent() { Intent stopIntent = new Intent(this, getClass()).setAction(ACTION_STOP_SEARCHING); return PendingIntentCompat.getForegroundService(this, 1, stopIntent, 0, false); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/adb/AdbUtils.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.adb; import android.Manifest; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.net.Uri; import android.os.Build; import android.os.SystemClock; import android.os.SystemProperties; import android.provider.Settings; import android.provider.SettingsHidden; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.annotation.WorkerThread; import androidx.core.util.Pair; import java.io.IOException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import io.github.muntashirakon.AppManager.runner.Runner; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.servermanager.ServerConfig; import io.github.muntashirakon.adb.android.AdbMdns; public class AdbUtils { @WorkerThread @NonNull public static Pair getLatestAdbDaemon(@NonNull Context context, long timeout, @NonNull TimeUnit unit) throws InterruptedException, IOException { if (!isAdbdRunning()) { throw new IOException("ADB daemon not running."); } AtomicInteger atomicPort = new AtomicInteger(-1); AtomicReference atomicHostAddress = new AtomicReference<>(null); CountDownLatch resolveHostAndPort = new CountDownLatch(1); AdbMdns adbMdnsTcp = new AdbMdns(context, AdbMdns.SERVICE_TYPE_ADB, (hostAddress, port) -> { if (hostAddress != null) { atomicHostAddress.set(hostAddress.getHostAddress()); atomicPort.set(port); } resolveHostAndPort.countDown(); }); adbMdnsTcp.start(); AdbMdns adbMdnsTls; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { adbMdnsTls = new AdbMdns(context, AdbMdns.SERVICE_TYPE_TLS_CONNECT, (hostAddress, port) -> { if (hostAddress != null) { atomicHostAddress.set(hostAddress.getHostAddress()); atomicPort.set(port); } resolveHostAndPort.countDown(); }); adbMdnsTls.start(); } else adbMdnsTls = null; try { if (!resolveHostAndPort.await(timeout, unit)) { throw new InterruptedException("Timed out while trying to find a valid host address and port"); } } finally { adbMdnsTcp.stop(); if (adbMdnsTls != null) { adbMdnsTls.stop(); } } String host = atomicHostAddress.get(); int port = atomicPort.get(); if (host == null || port == -1) { throw new IOException("Could not find any valid host address or port"); } return new Pair<>(host, port); } @RequiresApi(Build.VERSION_CODES.R) public static boolean enableWirelessDebugging(@NonNull Context context) { ContentResolver resolver = context.getContentResolver(); boolean wirelessDebuggingEnabled = Settings.Global.getInt(resolver, SettingsHidden.Global.ADB_WIFI_ENABLED, 0) != 0; if (wirelessDebuggingEnabled && isAdbdRunning()) { return true; } if (!SelfPermissions.checkSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS)) { // No permission return false; } try { if (Settings.Global.getInt(resolver, Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) == 0) { ContentValues contentValues = new ContentValues(2); contentValues.put("name", Settings.Global.DEVELOPMENT_SETTINGS_ENABLED); contentValues.put("value", 1); resolver.insert(Uri.parse("content://settings/global"), contentValues); } if (!wirelessDebuggingEnabled) { ContentValues contentValues = new ContentValues(2); contentValues.put("name", SettingsHidden.Global.ADB_WIFI_ENABLED); contentValues.put("value", 1); resolver.insert(Uri.parse("content://settings/global"), contentValues); } // Try at most 3 times to figure out if something has altered for (int i = 0; i < 5; ++i) { if (isAdbdRunning()) { return true; } SystemClock.sleep(500); } } catch (Throwable th) { th.printStackTrace(); } return false; } public static boolean isAdbdRunning() { // Default is set to “running” to avoid other issues return "running".equals(SystemProperties.get("init.svc.adbd", "running")); } public static int getAdbPortOrDefault() { return SystemProperties.getInt("service.adb.tcp.port", ServerConfig.DEFAULT_ADB_PORT); } public static boolean startAdb(int port) { return Runner.runCommand(new String[]{"setprop", "service.adb.tcp.port", String.valueOf(port)}).isSuccessful() && Runner.runCommand(new String[]{"setprop", "ctl.restart", "adbd"}).isSuccessful(); } public static boolean stopAdb() { return Runner.runCommand(new String[]{"setprop", "service.adb.tcp.port", "-1"}).isSuccessful() && Runner.runCommand(new String[]{"setprop", "ctl.restart", "adbd"}).isSuccessful(); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/ApkFile.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk; import static io.github.muntashirakon.AppManager.apk.ApkUtils.getDensityFromName; import static io.github.muntashirakon.AppManager.apk.ApkUtils.getManifestAttributes; import static io.github.muntashirakon.AppManager.apk.ApkUtils.getManifestFromApk; import static io.github.muntashirakon.AppManager.utils.UIUtils.getSmallerText; import android.content.ContentResolver; import android.content.Context; import android.content.pm.ApplicationInfo; import android.net.Uri; import android.os.Build; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextUtils; import android.text.format.Formatter; import android.text.style.ForegroundColorSpan; import android.util.DisplayMetrics; import android.util.SparseIntArray; import androidx.annotation.AnyThread; import androidx.annotation.GuardedBy; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import androidx.collection.SparseArrayCompat; import com.google.android.material.color.MaterialColors; import org.json.JSONException; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.concurrent.ThreadLocalRandom; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.StaticDataset; import io.github.muntashirakon.AppManager.apk.signing.SigSchemes; import io.github.muntashirakon.AppManager.apk.signing.Signer; import io.github.muntashirakon.AppManager.apk.splitapk.ApksMetadata; import io.github.muntashirakon.AppManager.fm.FmProvider; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.misc.VMRuntime; import io.github.muntashirakon.AppManager.self.filecache.FileCache; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.utils.ArrayUtils; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.FileUtils; import io.github.muntashirakon.AppManager.utils.LangUtils; import io.github.muntashirakon.io.IoUtils; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; import io.github.muntashirakon.unapkm.api.UnApkm; import io.github.muntashirakon.util.LocalizedString; public final class ApkFile implements AutoCloseable { public static final String TAG = "ApkFile"; private static final String ATTR_IS_FEATURE_SPLIT = "android:isFeatureSplit"; private static final String ATTR_IS_SPLIT_REQUIRED = "android:isSplitRequired"; private static final String ATTR_ISOLATED_SPLIT = "android:isolatedSplits"; private static final String ATTR_CONFIG_FOR_SPLIT = "configForSplit"; private static final String ATTR_SPLIT = "split"; private static final String ATTR_PACKAGE = "package"; private static final String CONFIG_PREFIX = "config."; private static final String UN_APKM_PKG = "io.github.muntashirakon.unapkm"; // There's hardly any chance of using multiple instances of ApkFile but still kept for convenience private static final SparseArrayCompat sApkFiles = new SparseArrayCompat<>(3); private static final SparseIntArray sInstanceCount = new SparseIntArray(3); @AnyThread @Nullable static ApkFile getInstance(int sparseArrayKey) { synchronized (sApkFiles) { ApkFile apkFile = sApkFiles.get(sparseArrayKey); if (apkFile == null) { return null; } synchronized (sInstanceCount) { // Increment the number of active instances sInstanceCount.put(sparseArrayKey, sInstanceCount.get(sparseArrayKey) + 1); } return apkFile; } } @AnyThread static int createInstance(Uri apkUri, @Nullable String mimeType) throws ApkFileException { synchronized (sApkFiles) { int key = getUniqueKey(); ApkFile apkFile = new ApkFile(apkUri, mimeType, key); sApkFiles.put(key, apkFile); return key; } } @AnyThread static int createInstance(ApplicationInfo info) throws ApkFileException { synchronized (sApkFiles) { int key = getUniqueKey(); ApkFile apkFile = new ApkFile(info, key); sApkFiles.put(key, apkFile); return key; } } @GuardedBy("sApkFiles") private static int getUniqueKey() { int key; do { key = ThreadLocalRandom.current().nextInt(); } while (sApkFiles.containsKey(key)); return key; } @IntDef(value = { APK_BASE, APK_SPLIT_FEATURE, APK_SPLIT_ABI, APK_SPLIT_DENSITY, APK_SPLIT_LOCALE, APK_SPLIT_UNKNOWN, APK_SPLIT, }) @Retention(RetentionPolicy.SOURCE) public @interface ApkType { } public static final int APK_BASE = 0; public static final int APK_SPLIT_FEATURE = 1; public static final int APK_SPLIT_ABI = 2; public static final int APK_SPLIT_DENSITY = 3; public static final int APK_SPLIT_LOCALE = 4; public static final int APK_SPLIT_UNKNOWN = 5; /** * Generic split type. For internal uses only, never returned by {@link Entry#type}. */ public static final int APK_SPLIT = 6; public static List SUPPORTED_EXTENSIONS = new ArrayList<>(); public static List SUPPORTED_MIMES = new ArrayList<>(); static { SUPPORTED_EXTENSIONS.add("apk"); SUPPORTED_EXTENSIONS.add("apkm"); SUPPORTED_EXTENSIONS.add("apks"); SUPPORTED_EXTENSIONS.add("xapk"); SUPPORTED_MIMES.add("application/x-apks"); SUPPORTED_MIMES.add("application/vnd.android.package-archive"); SUPPORTED_MIMES.add("application/vnd.apkm"); SUPPORTED_MIMES.add("application/xapk-package-archive"); } private final int mSparseArrayKey; @NonNull private final List mEntries = new ArrayList<>(); private Entry mBaseEntry; @Nullable private File mIdsigFile; @Nullable private ApksMetadata mApksMetadata; @NonNull private final String mPackageName; @NonNull private final List mObbFiles = new ArrayList<>(); private final FileCache mFileCache = new FileCache(); @NonNull private final File mCacheFilePath; @Nullable private ParcelFileDescriptor mFd; @Nullable private ZipFile mZipFile; private boolean mClosed; private ApkFile(@NonNull Uri apkUri, @Nullable String mimeType, int sparseArrayKey) throws ApkFileException { mSparseArrayKey = sparseArrayKey; Context context = ContextUtils.getContext(); Path apkSource = Paths.get(apkUri); @NonNull String extension; // Check type if (mimeType == null) mimeType = apkSource.getType(); if (!SUPPORTED_MIMES.contains(mimeType)) { Log.e(TAG, "Invalid mime: %s", mimeType); // Check extension if (!SUPPORTED_EXTENSIONS.contains(apkSource.getExtension())) { throw new ApkFileException("Invalid package extension."); } extension = Objects.requireNonNull(apkSource.getExtension()); } else { switch (mimeType) { case "application/x-apks": extension = "apks"; break; case "application/xapk-package-archive": extension = "xapk"; break; case "application/vnd.apkm": extension = "apkm"; break; default: extension = "apk"; break; } } if (extension.equals("apkm")) { try { if (FileUtils.isZip(apkSource)) { // DRM-free APKM file, mark it as APKS // FIXME(#227): Give it a special name and verify integrity extension = "apks"; } } catch (IOException | SecurityException e) { throw new ApkFileException(e); } } // Cache the file or use file descriptor for non-APKM files if (extension.equals("apkm")) { // Convert to APKS try { mCacheFilePath = mFileCache.createCachedFile("apks"); try (ParcelFileDescriptor inputFD = FileUtils.getFdFromUri(context, apkUri, "r"); OutputStream outputStream = new FileOutputStream(mCacheFilePath)) { UnApkm unApkm = new UnApkm(context, UN_APKM_PKG); unApkm.decryptFile(inputFD, outputStream); } } catch (IOException | RemoteException e) { throw new ApkFileException(e); } } else { // Open file descriptor if necessary File cacheFilePath = null; if (ContentResolver.SCHEME_FILE.equals(apkUri.getScheme())) { // File scheme may not require an FD cacheFilePath = new File(apkUri.getPath()); } if (!FmProvider.AUTHORITY.equals(apkUri.getAuthority())) { // Content scheme has a third-party authority try { mFd = FileUtils.getFdFromUri(context, apkUri, "r"); cacheFilePath = FileUtils.getFileFromFd(mFd); } catch (FileNotFoundException e) { throw new ApkFileException(e); } catch (SecurityException e) { Log.e(TAG, e); } } if (cacheFilePath == null || !FileUtils.canReadUnprivileged(cacheFilePath)) { // Cache manually try { mCacheFilePath = mFileCache.getCachedFile(apkSource); } catch (IOException | SecurityException e) { throw new ApkFileException("Could not cache the input file.", e); } } else mCacheFilePath = cacheFilePath; } String packageName = null; // Check for splits if (extension.equals("apk")) { // Get manifest attributes ByteBuffer manifest = getManifestFromApk(mCacheFilePath); HashMap manifestAttrs = getManifestAttributes(manifest); if (!manifestAttrs.containsKey(ATTR_PACKAGE)) { throw new IllegalArgumentException("Manifest doesn't contain any package name."); } packageName = manifestAttrs.get(ATTR_PACKAGE); mBaseEntry = new Entry(mCacheFilePath, manifest, manifestAttrs); mEntries.add(mBaseEntry); } else { try { mZipFile = new ZipFile(mCacheFilePath); } catch (IOException e) { throw new ApkFileException(e); } Enumeration zipEntries = mZipFile.entries(); while (zipEntries.hasMoreElements()) { ZipEntry zipEntry = zipEntries.nextElement(); if (zipEntry.isDirectory()) continue; String fileName = FileUtils.getFilenameFromZipEntry(zipEntry); if (fileName.endsWith(".apk")) { // APK is more likely to match try (InputStream zipInputStream = mZipFile.getInputStream(zipEntry)) { // Get manifest attributes ByteBuffer manifest = getManifestFromApk(zipInputStream); HashMap manifestAttrs = getManifestAttributes(manifest); if (manifestAttrs.containsKey("split")) { // TODO: check for duplicates Entry entry = new Entry(fileName, zipEntry, APK_SPLIT, manifest, manifestAttrs); mEntries.add(entry); } else { if (mBaseEntry != null) { throw new RuntimeException("Duplicate base apk found."); } mBaseEntry = new Entry(fileName, zipEntry, APK_BASE, manifest, manifestAttrs); mEntries.add(mBaseEntry); if (manifestAttrs.containsKey(ATTR_PACKAGE)) { packageName = manifestAttrs.get(ATTR_PACKAGE); } else throw new RuntimeException("Package name not found."); } } catch (IOException e) { throw new ApkFileException(e); } } else if (fileName.equals(ApksMetadata.META_FILE)) { try { String jsonString = IoUtils.getInputStreamContent(mZipFile.getInputStream(zipEntry)); mApksMetadata = new ApksMetadata(); mApksMetadata.readMetadata(jsonString); } catch (IOException | JSONException e) { mApksMetadata = null; Log.w(TAG, "The contents of info.json in the bundle is invalid", e); } } else if (fileName.endsWith(".obb")) { mObbFiles.add(zipEntry); } else if (fileName.endsWith(".idsig")) { try { mIdsigFile = mFileCache.getCachedFile(mZipFile.getInputStream(zipEntry), ".idsig"); } catch (IOException e) { throw new ApkFileException(e); } } } if (mBaseEntry == null) throw new ApkFileException("No base apk found."); // Sort the entries based on type and rank Collections.sort(mEntries, (o1, o2) -> { Integer int1 = o1.type; int int2 = o2.type; int typeCmp; if ((typeCmp = int1.compareTo(int2)) != 0) return typeCmp; int1 = o1.rank; int2 = o2.rank; return int1.compareTo(int2); }); } if (packageName == null) throw new ApkFileException("Package name not found."); mPackageName = packageName; } private ApkFile(@NonNull ApplicationInfo info, int sparseArrayKey) throws ApkFileException { mSparseArrayKey = sparseArrayKey; mPackageName = info.packageName; mCacheFilePath = new File(info.publicSourceDir); File sourceDir = mCacheFilePath.getParentFile(); if (sourceDir == null || "/data/app".equals(sourceDir.getAbsolutePath())) { // Old file structure (storing APK files at /data/app) mEntries.add(mBaseEntry = new Entry(mCacheFilePath, getManifestFromApk(mCacheFilePath), null)); } else { File[] apks = sourceDir.listFiles((dir, name) -> name.endsWith(".apk")); if (apks == null) { // Directory might be inaccessible Log.w(TAG, "No apk files found in %s. Using default.", sourceDir); List allApks = new ArrayList<>(); allApks.add(mCacheFilePath); String[] splits = info.splitPublicSourceDirs; if (splits != null) { for (String split : splits) { if (split != null) { allApks.add(new File(split)); } } } apks = allApks.toArray(new File[0]); } String fileName; for (File apk : apks) { fileName = Paths.getLastPathSegment(apk.getAbsolutePath()); // Get manifest attributes ByteBuffer manifest = getManifestFromApk(apk); HashMap manifestAttrs = getManifestAttributes(manifest); if (manifestAttrs.containsKey("split")) { Entry entry = new Entry(fileName, apk, APK_SPLIT, manifest, manifestAttrs); mEntries.add(entry); } else { // Could be a base entry, check package name if (!manifestAttrs.containsKey(ATTR_PACKAGE)) { throw new IllegalArgumentException("Manifest doesn't contain any package name."); } String newPackageName = manifestAttrs.get(ATTR_PACKAGE); if (mPackageName.equals(newPackageName)) { if (mBaseEntry != null) { throw new RuntimeException("Duplicate base apk found."); } mBaseEntry = new Entry(fileName, apk, APK_BASE, manifest, manifestAttrs); mEntries.add(mBaseEntry); } // else continue; } } if (mBaseEntry == null) throw new ApkFileException("No base apk found."); // Sort the entries based on type Collections.sort(mEntries, (o1, o2) -> { Integer int1 = o1.type; int int2 = o2.type; int typeCmp; if ((typeCmp = int1.compareTo(int2)) != 0) return typeCmp; int1 = o1.rank; int2 = o2.rank; return int1.compareTo(int2); }); } } public Entry getBaseEntry() { return mBaseEntry; } @NonNull public List getEntries() { return mEntries; } @Nullable public File getIdsigFile() { if (mIdsigFile != null) { return mIdsigFile; } return null; } @Nullable public ApksMetadata getApksMetadata() { return mApksMetadata; } @NonNull public String getPackageName() { return mPackageName; } public boolean isSplit() { return mEntries.size() > 1; } public boolean hasObb() { return !mObbFiles.isEmpty(); } @WorkerThread public void extractObb(Path writableObbDir) throws IOException { if (!hasObb() || mZipFile == null) return; for (ZipEntry obbEntry : mObbFiles) { String fileName = FileUtils.getFilenameFromZipEntry(obbEntry); Path obbDir = writableObbDir.findOrCreateFile(fileName, null); // Extract obb file to the destination directory try (InputStream zipInputStream = mZipFile.getInputStream(obbEntry); OutputStream outputStream = obbDir.openOutputStream()) { IoUtils.copy(zipInputStream, outputStream); } } } public boolean isClosed() { return mClosed; } @Override public void close() { synchronized (sInstanceCount) { if (sInstanceCount.get(mSparseArrayKey) > 1) { // This isn't the only instance, do not close yet sInstanceCount.put(mSparseArrayKey, sInstanceCount.get(mSparseArrayKey) - 1); return; } // Only this instance remained sInstanceCount.delete(mSparseArrayKey); } mClosed = true; sApkFiles.remove(mSparseArrayKey); for (Entry entry : mEntries) { entry.close(); } IoUtils.closeQuietly(mZipFile); IoUtils.closeQuietly(mFd); IoUtils.closeQuietly(mFileCache); FileUtils.deleteSilently(mIdsigFile); // Ensure that entries are not accessible if accidentally accessed mEntries.clear(); mBaseEntry = null; mObbFiles.clear(); } @Override protected void finalize() { if (!mClosed) { close(); } } public class Entry implements AutoCloseable, LocalizedString { /** * Unique identifier capable of persisting across new instances. This is usually the file path (relative or * absolute). */ public final String id; /** * Name of the file, for split apk, name of the split instead */ @NonNull public final String name; /** * Type of the APK (base or split). One of {@link #APK_BASE}, {@link #APK_SPLIT_FEATURE}, * {@link #APK_SPLIT_ABI}, {@link #APK_SPLIT_DENSITY}, {@link #APK_SPLIT_LOCALE}, {@link #APK_SPLIT_UNKNOWN}. */ @ApkType public final int type; /** * The entire manifest file as {@link ByteBuffer}. */ @NonNull public final ByteBuffer manifest; @Nullable private String mSplitSuffix; @Nullable private String mForFeature = null; @Nullable private File mCachedFile; @Nullable private ZipEntry mZipEntry; @Nullable private File mSource; @Nullable private File mSignedFile; @Nullable private File mIdsigFile; private final boolean mRequired; private final boolean mIsolated; /** * Rank for a certain {@link #type} to create a priority list. This is applicable for * {@link #APK_SPLIT_ABI}, {@link #APK_SPLIT_DENSITY} and {@link #APK_SPLIT_LOCALE}. * Smallest rank number denotes highest rank. */ public int rank = Integer.MAX_VALUE; Entry(@NonNull File source, @NonNull ByteBuffer manifest, @Nullable HashMap manifestAttrs) { this("base-apk", "Base.apk", APK_BASE, manifest, manifestAttrs); mSource = Objects.requireNonNull(source); } Entry(@NonNull String name, @NonNull ZipEntry zipEntry, @ApkType int type, @NonNull ByteBuffer manifest, @Nullable HashMap manifestAttrs) { this(Objects.requireNonNull(zipEntry).getName(), name, type, manifest, manifestAttrs); mZipEntry = Objects.requireNonNull(zipEntry); } Entry(@NonNull String name, @NonNull File source, @ApkType int type, @NonNull ByteBuffer manifest, @Nullable HashMap manifestAttrs) { this(Objects.requireNonNull(source).getAbsolutePath(), name, type, manifest, manifestAttrs); mSource = source; } private Entry(@NonNull String id, @NonNull String name, @ApkType int type, @NonNull ByteBuffer manifest, @Nullable HashMap manifestAttrs) { Objects.requireNonNull(name); Objects.requireNonNull(manifest); this.id = id; this.manifest = manifest; if (type == APK_BASE) { this.name = name; mRequired = true; mIsolated = false; this.type = APK_BASE; } else if (type == APK_SPLIT) { Objects.requireNonNull(manifestAttrs); String splitName = manifestAttrs.get(ATTR_SPLIT); if (splitName == null) throw new RuntimeException("Split name is empty."); this.name = splitName; // Check if required if (manifestAttrs.containsKey(ATTR_IS_SPLIT_REQUIRED)) { String value = manifestAttrs.get(ATTR_IS_SPLIT_REQUIRED); mRequired = value != null && Boolean.parseBoolean(value); } else mRequired = false; // Check if isolated if (manifestAttrs.containsKey(ATTR_ISOLATED_SPLIT)) { String value = manifestAttrs.get(ATTR_ISOLATED_SPLIT); mIsolated = value != null && Boolean.parseBoolean(value); } else mIsolated = false; // Infer types if (manifestAttrs.containsKey(ATTR_IS_FEATURE_SPLIT)) { this.type = APK_SPLIT_FEATURE; } else { if (manifestAttrs.containsKey(ATTR_CONFIG_FOR_SPLIT)) { mForFeature = manifestAttrs.get(ATTR_CONFIG_FOR_SPLIT); if (TextUtils.isEmpty(mForFeature)) mForFeature = null; } int configPartIndex = this.name.lastIndexOf(CONFIG_PREFIX); if (configPartIndex == -1 || (configPartIndex != 0 && this.name.charAt(configPartIndex - 1) != '.')) { this.type = APK_SPLIT_UNKNOWN; return; } mSplitSuffix = this.name.substring(configPartIndex + (CONFIG_PREFIX.length())); if (StaticDataset.ALL_ABIS.containsKey(mSplitSuffix)) { // This split is an ABI this.type = APK_SPLIT_ABI; String abi = StaticDataset.ALL_ABIS.get(mSplitSuffix); int index = ArrayUtils.indexOf(Build.SUPPORTED_ABIS, Objects.requireNonNull(abi)); if (index != -1) { this.rank = index; if (mForFeature == null) { // Increment rank for base APK this.rank -= 1000; } } } else if (StaticDataset.DENSITY_NAME_TO_DENSITY.containsKey(mSplitSuffix)) { // This split is for Screen Density this.type = APK_SPLIT_DENSITY; this.rank = Math.abs(StaticDataset.DEVICE_DENSITY - getDensityFromName(mSplitSuffix)); if (mForFeature == null) { // Increment rank for base APK this.rank -= 1000; } } else if (LangUtils.isValidLocale(mSplitSuffix)) { // This split is for Locale this.type = APK_SPLIT_LOCALE; Integer rank = StaticDataset.LOCALE_RANKING.get(mSplitSuffix); if (rank != null) { this.rank = rank; if (mForFeature == null) { // Increment rank for base APK this.rank -= 1000; } } } else this.type = APK_SPLIT_UNKNOWN; } } else { this.name = name; this.type = APK_SPLIT_UNKNOWN; mRequired = mIsolated = false; } } /** * Get filename of the entry. This does not necessarily exist as a real file. */ @NonNull public String getFileName() { if (Paths.exists(mCachedFile)) return mCachedFile.getName(); if (mZipEntry != null) return FileUtils.getFilenameFromZipEntry(mZipEntry); if (Paths.exists(mSource)) return mSource.getName(); else throw new RuntimeException("Neither zipEntry nor source is defined."); } /** * Get size of the entry. */ public long getFileSize() { if (Paths.exists(mCachedFile)) return mCachedFile.length(); if (mZipEntry != null) return mZipEntry.getSize(); if (Paths.exists(mSource)) return mSource.length(); else throw new RuntimeException("Neither zipEntry nor source is defined."); } /** * Get size of the entry or {@code -1} if unavailable */ @WorkerThread public long getFileSize(boolean signed) { try { return (signed ? getSignedFile() : getRealCachedFile()).length(); } catch (IOException e) { return -1; } } @WorkerThread public File getFile(boolean signed) throws IOException { return signed ? getSignedFile() : getRealCachedFile(); } @WorkerThread public InputStream getInputStream(boolean signed) throws IOException { return signed ? getSignedInputStream() : getRealInputStream(); } /** * Get signed APK file. * * @throws IOException If the APK cannot be signed or cached. */ private File getSignedFile() throws IOException { File realFile = getRealCachedFile(); if (Paths.exists(mSignedFile)) return mSignedFile; mSignedFile = mFileCache.createCachedFile("apk"); SigSchemes sigSchemes = Prefs.Signing.getSigSchemes(); boolean zipAlign = Prefs.Signing.zipAlign(); try { Signer signer = Signer.getInstance(sigSchemes); if (signer.isV4SchemeEnabled()) { mIdsigFile = mFileCache.createCachedFile("idsig"); signer.setIdsigFile(mIdsigFile); } if (signer.sign(realFile, mSignedFile, -1, zipAlign) && Signer.verify(sigSchemes, mSignedFile, mIdsigFile)) { return mSignedFile; } throw new IOException("Failed to sign " + realFile); } catch (IOException e) { throw e; } catch (Exception e) { throw new IOException(e); } } /** * Same as {@link #getSignedFile()} except that it returns an {@link InputStream}. * * @throws IOException If the APK cannot be signed or cached. */ private InputStream getSignedInputStream() throws IOException { return new FileInputStream(getSignedFile()); } /** * Get the APK file source if it has a physical location. * * @return Absolute path to the APK file. */ @Nullable public String getApkSource() { return mSource == null ? null : mSource.getAbsolutePath(); } /** * Close this entry i.e. delete the cached files. Called automatically if {@link ApkFile#close()} is called. */ @Override public void close() { FileUtils.deleteSilently(mCachedFile); FileUtils.deleteSilently(mIdsigFile); FileUtils.deleteSilently(mSignedFile); if (mSource != null && !mSource.getAbsolutePath().startsWith("/proc/self") && !mSource.getAbsolutePath().startsWith("/data/app")) { FileUtils.deleteSilently(mSource); } } /** * Get input stream of the entry. It does not sign the APK based on user preferences. It also does not cache * the APK file, but tries to reuse existing cache file. * * @throws IOException If I/O error occurs. */ @NonNull private InputStream getRealInputStream() throws IOException { if (Paths.exists(mCachedFile)) return new FileInputStream(mCachedFile); if (mZipEntry != null) return Objects.requireNonNull(mZipFile).getInputStream(mZipEntry); if (Paths.exists(mSource)) return new FileInputStream(mSource); else throw new IOException("Neither zipEntry nor source is defined."); } /** * Get a readable file of the entry, cached if necessary. It does not sign the APK based on user preferences. * * @throws IOException If an I/O error occurs while caching the APK. */ @WorkerThread private File getRealCachedFile() throws IOException { if (mSource != null && mSource.canRead() && !mSource.getAbsolutePath().startsWith("/proc/self")) { return mSource; } if (mCachedFile != null) { if (mCachedFile.canRead()) { return mCachedFile; } else FileUtils.deleteSilently(mCachedFile); } try (InputStream is = getRealInputStream()) { mCachedFile = mFileCache.getCachedFile(is, "apk"); return Objects.requireNonNull(mCachedFile); } } /** * Whether the entry is a required entry i.e. it must be installed along with the base APK. */ public boolean isRequired() { return mRequired; } /** * Whether the entry is an isolated entry. */ public boolean isIsolated() { return mIsolated; } /** * Get ABI if the split is an ABI split. * * @return One of {@link VMRuntime#ABI_ARMEABI_V7A}, {@link VMRuntime#ABI_ARM64_V8A}, {@link VMRuntime#ABI_X86}, * {@link VMRuntime#ABI_X86_64}. * @throws RuntimeException If split is not an ABI split. * @throws NullPointerException If the ABI is not valid. */ @NonNull public String getAbi() { if (type == APK_SPLIT_ABI) { return Objects.requireNonNull(StaticDataset.ALL_ABIS.get(mSplitSuffix)); } throw new RuntimeException("Attempt to fetch ABI for invalid apk"); } /** * Get density if the split is a density split. * * @return One of {@link DisplayMetrics#DENSITY_LOW}, {@link DisplayMetrics#DENSITY_MEDIUM}, * {@link DisplayMetrics#DENSITY_TV}, {@link DisplayMetrics#DENSITY_HIGH}, {@link DisplayMetrics#DENSITY_XHIGH}, * {@link DisplayMetrics#DENSITY_XXHIGH}, {@link DisplayMetrics#DENSITY_XXXHIGH}. * @throws RuntimeException If split is not a density split, or the density is not valid. */ public int getDensity() { if (type == APK_SPLIT_DENSITY) { return getDensityFromName(mSplitSuffix); } throw new RuntimeException("Attempt to fetch Density for invalid apk"); } /** * Get locale if the split is a locale split. Each locale can belong to multiple regions. * * @throws RuntimeException If the split is not a locale split. * @throws NullPointerException If the locale is not valid. */ @NonNull public Locale getLocale() { if (type == APK_SPLIT_LOCALE) { return new Locale.Builder().setLanguageTag(Objects.requireNonNull(mSplitSuffix)).build(); } throw new RuntimeException("Attempt to fetch Locale for invalid apk"); } @Nullable public String getFeature() { if (type == APK_SPLIT_FEATURE) { return name; } return mForFeature; } public boolean isForFeature() { return mForFeature != null; } /** * Whether the split supported by this platform */ public boolean supported() { if (type == APK_SPLIT_ABI) { // Not all ABIs are supported by all platforms. // This can be deduced by checking the rank of the ABI. return rank != Integer.MAX_VALUE; } return true; } @Override @NonNull public CharSequence toLocalizedString(@NonNull Context context) { CharSequence localizedString = toShortLocalizedString(context); SpannableStringBuilder builder = new SpannableStringBuilder() .append(context.getString(R.string.size)).append(LangUtils.getSeparatorString()) .append(Formatter.formatFileSize(context, getFileSize())); if (isRequired()) { builder.append(", ").append(context.getString(R.string.required)); } if (isIsolated()) { builder.append(", ").append(context.getString(R.string.isolated)); } if (!supported()) { builder.append(", "); int start = builder.length(); builder.append(context.getString(R.string.unsupported_split_apk)); builder.setSpan(new ForegroundColorSpan(MaterialColors.getColor(context, androidx.appcompat.R.attr.colorError, "null")), start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } return new SpannableStringBuilder(localizedString).append("\n").append(getSmallerText(builder)); } public CharSequence toShortLocalizedString(Context context) { switch (type) { case ApkFile.APK_BASE: return context.getString(R.string.base_apk); case ApkFile.APK_SPLIT_DENSITY: if (mForFeature != null) { return context.getString(R.string.density_split_for_feature, mSplitSuffix, getDensity(), mForFeature); } else { return context.getString(R.string.density_split_for_base_apk, mSplitSuffix, getDensity()); } case ApkFile.APK_SPLIT_ABI: if (mForFeature != null) { return context.getString(R.string.abi_split_for_feature, getAbi(), mForFeature); } else { return context.getString(R.string.abi_split_for_base_apk, getAbi()); } case ApkFile.APK_SPLIT_LOCALE: if (mForFeature != null) { return context.getString(R.string.locale_split_for_feature, getLocale().getDisplayLanguage(), mForFeature); } else { return context.getString(R.string.locale_split_for_base_apk, getLocale().getDisplayLanguage()); } case ApkFile.APK_SPLIT_FEATURE: return context.getString(R.string.split_feature_name, name); case ApkFile.APK_SPLIT_UNKNOWN: case ApkFile.APK_SPLIT: if (mForFeature != null) { return context.getString(R.string.unknown_split_for_feature, name, mForFeature); } else { return context.getString(R.string.unknown_split_for_base_apk, name); } default: throw new RuntimeException("Invalid split type."); } } @Override public boolean equals(Object o) { if (this == o) return true; if (o instanceof String) return name.equals(o); if (!(o instanceof Entry)) return false; Entry entry = (Entry) o; return name.equals(entry.name); } @Override public int hashCode() { return Objects.hash(name); } } public static class ApkFileException extends Throwable { public ApkFileException(@Nullable String message) { super(message); } public ApkFileException(@Nullable String message, Throwable throwable) { super(message, throwable); } public ApkFileException(Throwable throwable) { super(throwable); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/ApkSource.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk; import android.content.pm.ApplicationInfo; import android.net.Uri; import android.os.Parcelable; import androidx.annotation.AnyThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.json.JSONException; import io.github.muntashirakon.AppManager.history.IJsonSerializer; import io.github.muntashirakon.AppManager.history.JsonDeserializer; import io.github.muntashirakon.AppManager.utils.JSONUtils; public abstract class ApkSource implements Parcelable, IJsonSerializer { @NonNull public static ApkSource getApkSource(@NonNull Uri uri, @Nullable String mimeType) { return new UriApkSource(uri, mimeType); } @NonNull public static ApkSource getCachedApkSource(@NonNull Uri uri, @Nullable String mimeType) { return new CachedApkSource(uri, mimeType); } @NonNull public static ApkSource getApkSource(@NonNull ApplicationInfo applicationInfo) { return new ApplicationInfoApkSource(applicationInfo); } @AnyThread @NonNull public abstract ApkFile resolve() throws ApkFile.ApkFileException; @AnyThread @NonNull public abstract ApkSource toCachedSource(); public static final JsonDeserializer.Creator DESERIALIZER = jsonObject -> { String tag = JSONUtils.getString(jsonObject, "tag"); if (ApplicationInfoApkSource.TAG.equals(tag)) { return ApplicationInfoApkSource.DESERIALIZER.deserialize(jsonObject); } else if (CachedApkSource.TAG.equals(tag)) { return CachedApkSource.DESERIALIZER.deserialize(jsonObject); } else if (UriApkSource.TAG.equals(tag)) { return UriApkSource.DESERIALIZER.deserialize(jsonObject); } else throw new JSONException("Invalid tag: " + tag); }; } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/ApkUtils.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk; import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.MATCH_UNINSTALLED_PACKAGES; import android.annotation.UserIdInt; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Build; import android.os.RemoteException; import android.os.UserHandleHidden; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import androidx.core.content.pm.PackageInfoCompat; import com.android.apksig.apk.ApkFormatException; import com.android.apksig.internal.zip.CentralDirectoryRecord; import com.android.apksig.internal.zip.LocalFileRecord; import com.android.apksig.internal.zip.ZipUtils; import com.android.apksig.util.DataSource; import com.android.apksig.util.DataSources; import com.android.apksig.zip.ZipFormatException; import com.reandroid.arsc.chunk.xml.ResXmlAttribute; import com.reandroid.arsc.chunk.xml.ResXmlDocument; import com.reandroid.arsc.chunk.xml.ResXmlElement; import com.reandroid.arsc.io.BlockReader; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import io.github.muntashirakon.AppManager.StaticDataset; import io.github.muntashirakon.AppManager.apk.parser.AndroidBinXmlDecoder; import io.github.muntashirakon.AppManager.apk.splitapk.SplitApkExporter; import io.github.muntashirakon.AppManager.backup.BackupItems; import io.github.muntashirakon.AppManager.compat.PackageManagerCompat; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.misc.OsEnvironment; import io.github.muntashirakon.AppManager.self.filecache.FileCache; import io.github.muntashirakon.AppManager.utils.AppPref; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.DateUtils; import io.github.muntashirakon.AppManager.utils.FileUtils; import io.github.muntashirakon.io.IoUtils; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; public final class ApkUtils { public static final String TAG = ApkUtils.class.getSimpleName(); public static final String EXT_APK = ".apk"; public static final String EXT_APKS = ".apks"; private static final Object sLock = new Object(); private static final String MANIFEST_FILE = "AndroidManifest.xml"; @WorkerThread @NonNull public static Path getSharableApkFile(@NonNull Context ctx, @NonNull PackageInfo packageInfo) throws IOException { synchronized (sLock) { PackageManager pm = ctx.getPackageManager(); ApplicationInfo info = packageInfo.applicationInfo; String outputName = Paths.sanitizeFilename(getFormattedApkFilename(ctx, packageInfo, pm), "_", Paths.SANITIZE_FLAG_FAT_ILLEGAL_CHARS | Paths.SANITIZE_FLAG_UNIX_RESERVED); if (outputName == null) outputName = info.packageName; Path tmpPublicSource; if (isSplitApk(info) || hasObbFiles(info.packageName, UserHandleHidden.getUserId(info.uid))) { // Split apk tmpPublicSource = Paths.get(new File(FileUtils.getExternalCachePath(ContextUtils.getContext()), outputName + EXT_APKS)); SplitApkExporter.saveApks(packageInfo, tmpPublicSource); } else { // Regular apk tmpPublicSource = Paths.get(new File(FileUtils.getExternalCachePath(ContextUtils.getContext()), outputName + EXT_APK)); IoUtils.copy(Paths.get(info.publicSourceDir), tmpPublicSource); } return tmpPublicSource; } } /** * Backup the given apk (both root and no-root). This is similar to apk sharing feature except * that these are saved at /sdcard/AppManager/apks */ @WorkerThread public static void backupApk(@NonNull Context ctx, @NonNull String packageName, @UserIdInt int userId) throws IOException, PackageManager.NameNotFoundException, RemoteException { Path backupPath = BackupItems.getApkBackupDirectory(); // Fetch package info PackageManager pm = ctx.getPackageManager(); PackageInfo packageInfo = PackageManagerCompat.getPackageInfo(packageName, MATCH_UNINSTALLED_PACKAGES | PackageManager.GET_SHARED_LIBRARY_FILES | PackageManagerCompat.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES, userId); ApplicationInfo info = packageInfo.applicationInfo; String outputName = Paths.sanitizeFilename(getFormattedApkFilename(ctx, packageInfo, pm), "_", Paths.SANITIZE_FLAG_FAT_ILLEGAL_CHARS | Paths.SANITIZE_FLAG_UNIX_RESERVED); if (outputName == null) outputName = packageName; Path apkFile; if (isSplitApk(info) || hasObbFiles(packageName, userId)) { // Split apk apkFile = backupPath.createNewFile(outputName + EXT_APKS, null); SplitApkExporter.saveApks(packageInfo, apkFile); } else { // Regular apk apkFile = backupPath.createNewFile(outputName + EXT_APK, null); IoUtils.copy(Paths.get(info.publicSourceDir), apkFile); } } @NonNull private static String getFormattedApkFilename(@NonNull Context context, @NonNull PackageInfo packageInfo, @NonNull PackageManager pm) { // TODO: 15/3/22 Optimize this String apkName = AppPref.getString(AppPref.PrefKey.PREF_SAVED_APK_FORMAT_STR) .replaceAll("%label%", packageInfo.applicationInfo.loadLabel(pm).toString()) .replaceAll("%package_name%", packageInfo.packageName) .replaceAll("%version%", packageInfo.versionName) .replaceAll("%version_code%", String.valueOf(PackageInfoCompat.getLongVersionCode(packageInfo))) .replaceAll("%target_sdk%", String.valueOf(packageInfo.applicationInfo.targetSdkVersion)) .replaceAll("%datetime%", DateUtils.formatDateTime(context, System.currentTimeMillis())); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { return apkName.replaceAll("%min_sdk%", String.valueOf(packageInfo.applicationInfo.minSdkVersion)); } return apkName; } public static boolean isSplitApk(@NonNull ApplicationInfo info) { return info.splitPublicSourceDirs != null && info.splitPublicSourceDirs.length > 0; } @NonNull public static ByteBuffer getManifestFromApk(File apkFile) throws ApkFile.ApkFileException { try (RandomAccessFile in = new RandomAccessFile(apkFile, "r")) { DataSource apk = DataSources.asDataSource(in); com.android.apksig.apk.ApkUtils.ZipSections apkSections; try { apkSections = com.android.apksig.apk.ApkUtils.findZipSections(apk); } catch (ZipFormatException e) { throw new ApkFile.ApkFileException("Malformed APK: not a ZIP archive", e); } List cdRecords; try { cdRecords = ZipUtils.parseZipCentralDirectory(apk, apkSections); } catch (ApkFormatException e) { throw new ApkFile.ApkFileException(e.getMessage(), e); } try { return getAndroidManifestFromApk( cdRecords, apk.slice(0, apkSections.getZipCentralDirectoryOffset())); } catch (ApkFormatException e) { throw new ApkFile.ApkFileException(e.getMessage(), e); } catch (ZipFormatException e) { throw new ApkFile.ApkFileException("Failed to read " + MANIFEST_FILE, e); } } catch (IOException e) { throw new ApkFile.ApkFileException(e.getMessage(), e); } } @NonNull public static ByteBuffer getManifestFromApk(InputStream apkInputStream) throws ApkFile.ApkFileException { try (ZipInputStream zipInputStream = new ZipInputStream(new BufferedInputStream(apkInputStream))) { ZipEntry zipEntry; while ((zipEntry = zipInputStream.getNextEntry()) != null) { if (!zipEntry.getName().equals(MANIFEST_FILE)) { continue; } ByteArrayOutputStream buffer = new ByteArrayOutputStream(); byte[] buf = new byte[IoUtils.DEFAULT_BUFFER_SIZE]; int n; while (-1 != (n = zipInputStream.read(buf))) { buffer.write(buf, 0, n); } return ByteBuffer.wrap(buffer.toByteArray()); } } catch (IOException e) { Log.w(TAG, "Could not fetch AndroidManifest.xml from APK stream, trying an alternative...", e); } // This could be due to a Zip error, try caching the APK File cachedApk; try { cachedApk = FileCache.getGlobalFileCache().getCachedFile(apkInputStream, "apk"); } catch (IOException e) { throw new ApkFile.ApkFileException("Could not cache the APK file", e); } ByteBuffer byteBuffer; try { byteBuffer = getManifestFromApk(cachedApk); } finally { FileCache.getGlobalFileCache().delete(cachedApk); } return byteBuffer; } @NonNull public static HashMap getManifestAttributes(@NonNull ByteBuffer manifestBytes) throws ApkFile.ApkFileException { try (BlockReader reader = new BlockReader(manifestBytes.array())) { HashMap manifestAttrs = new HashMap<>(); ResXmlDocument xmlBlock = new ResXmlDocument(); try { xmlBlock.readBytes(reader); } catch (IOException e) { throw new ApkFile.ApkFileException(e); } xmlBlock.setPackageBlock(AndroidBinXmlDecoder.getFrameworkPackageBlock()); ResXmlElement resManifestElement = xmlBlock.getDocumentElement(); // manifest if (!"manifest".equals(resManifestElement.getName())) { throw new ApkFile.ApkFileException("No manifest found."); } Iterator attrIt = resManifestElement.getAttributes(); ResXmlAttribute attr; String attrName; while (attrIt.hasNext()) { attr = attrIt.next(); attrName = attr.getName(); if (TextUtils.isEmpty(attrName)) { continue; } manifestAttrs.put(attrName, attr.getValueAsString()); } // application ResXmlElement resApplicationElement = null; Iterator resXmlElementIt = resManifestElement.getElements("application"); if (resXmlElementIt.hasNext()) { resApplicationElement = resXmlElementIt.next(); } if (resXmlElementIt.hasNext()) { throw new ApkFile.ApkFileException("\"manifest\" has duplicate \"application\" tags."); } if (resApplicationElement == null) { Log.w(TAG, "No application tag found while parsing APK."); return manifestAttrs; } attrIt = resApplicationElement.getAttributes(); while (attrIt.hasNext()) { attr = attrIt.next(); attrName = attr.getName(); if (TextUtils.isEmpty(attrName)) { continue; } if (manifestAttrs.containsKey(attrName)) { Log.w(TAG, "Ignoring invalid attribute in the application tag: " + attrName); continue; } manifestAttrs.put(attrName, attr.getValueAsString()); } return manifestAttrs; } } public static boolean hasObbFiles(@NonNull String packageName, @UserIdInt int userId) { try { return getObbDir(packageName, userId).listFiles().length > 0; } catch (FileNotFoundException e) { e.printStackTrace(); return false; } } @NonNull public static Path getObbDir(@NonNull String packageName, @UserIdInt int userId) throws FileNotFoundException { // Get writable OBB directory Path obbDir = getWritableExternalDirectory(userId) .findFile("Android") .findFile("obb") .findFile(packageName); return Paths.get(obbDir.getUri()); } @NonNull public static Path getOrCreateObbDir(@NonNull String packageName, @UserIdInt int userId) throws IOException { // Get writable OBB directory Path obbDir = getWritableExternalDirectory(userId) .findOrCreateDirectory("Android") .findOrCreateDirectory("obb") .findOrCreateDirectory(packageName); return Paths.get(obbDir.getUri()); } @NonNull public static Path getWritableExternalDirectory(@UserIdInt int userId) throws FileNotFoundException { // Get the first writable external storage directory OsEnvironment.UserEnvironment userEnvironment = OsEnvironment.getUserEnvironment(userId); Path[] extDirs = userEnvironment.getExternalDirs(); Path writableExtDir = null; for (Path extDir : extDirs) { if (extDir.canWrite() || Objects.requireNonNull(extDir.getFilePath()).startsWith("/storage/emulated")) { writableExtDir = extDir; break; } } if (writableExtDir == null) { throw new FileNotFoundException("Couldn't find any writable Obb dir"); } return writableExtDir; } public static int getDensityFromName(@Nullable String densityName) { Integer density = StaticDataset.DENSITY_NAME_TO_DENSITY.get(densityName); if (density == null) { throw new IllegalArgumentException("Unknown density " + densityName); } return density; } @NonNull private static ByteBuffer getAndroidManifestFromApk( @NonNull List cdRecords, @NonNull DataSource lhfSection) throws IOException, ApkFormatException, ZipFormatException { CentralDirectoryRecord androidManifestCdRecord = findCdRecord(cdRecords, MANIFEST_FILE); if (androidManifestCdRecord == null) { throw new ApkFormatException("Missing " + MANIFEST_FILE); } return ByteBuffer.wrap(LocalFileRecord.getUncompressedData( lhfSection, androidManifestCdRecord, lhfSection.size())); } @Nullable private static CentralDirectoryRecord findCdRecord( @NonNull List cdRecords, @NonNull String name) { for (CentralDirectoryRecord cdRecord : cdRecords) { if (name.equals(cdRecord.getName())) { return cdRecord; } } return null; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/ApplicationInfoApkSource.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Parcel; import androidx.annotation.NonNull; import androidx.core.os.ParcelCompat; import org.json.JSONException; import org.json.JSONObject; import java.io.File; import java.util.Objects; import io.github.muntashirakon.AppManager.history.JsonDeserializer; import io.github.muntashirakon.AppManager.utils.ContextUtils; public class ApplicationInfoApkSource extends ApkSource { public static final String TAG = ApplicationInfoApkSource.class.getSimpleName(); @NonNull private final ApplicationInfo mApplicationInfo; private int mApkFileKey; ApplicationInfoApkSource(@NonNull ApplicationInfo applicationInfo) { mApplicationInfo = Objects.requireNonNull(applicationInfo); } @NonNull @Override public ApkFile resolve() throws ApkFile.ApkFileException { ApkFile apkFile = ApkFile.getInstance(mApkFileKey); if (apkFile != null && !apkFile.isClosed()) { // Usable past instance return apkFile; } mApkFileKey = ApkFile.createInstance(mApplicationInfo); return Objects.requireNonNull(ApkFile.getInstance(mApkFileKey)); } @NonNull @Override public ApkSource toCachedSource() { return new CachedApkSource(Uri.fromFile(new File(mApplicationInfo.publicSourceDir)), "application/vnd.android.package-archive"); } protected ApplicationInfoApkSource(@NonNull Parcel in) { mApplicationInfo = Objects.requireNonNull(ParcelCompat.readParcelable(in, ApplicationInfo.class.getClassLoader(), ApplicationInfo.class)); mApkFileKey = in.readInt(); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeParcelable(mApplicationInfo, flags); dest.writeInt(mApkFileKey); } protected ApplicationInfoApkSource(@NonNull JSONObject jsonObject) throws JSONException { PackageManager pm = ContextUtils.getContext().getPackageManager(); String file = jsonObject.getString("file"); PackageInfo packageInfo = Objects.requireNonNull(pm.getPackageArchiveInfo(file, 0)); mApplicationInfo = Objects.requireNonNull(packageInfo.applicationInfo); mApplicationInfo.publicSourceDir = mApplicationInfo.sourceDir = file; mApkFileKey = jsonObject.getInt("apk_file_key"); } @NonNull @Override public JSONObject serializeToJson() throws JSONException { JSONObject jsonObject = new JSONObject(); jsonObject.put("tag", TAG); jsonObject.put("file", mApplicationInfo.publicSourceDir); jsonObject.put("apk_file_key", mApkFileKey); return jsonObject; } public static final JsonDeserializer.Creator DESERIALIZER = ApplicationInfoApkSource::new; public static final Creator CREATOR = new Creator() { @Override public ApplicationInfoApkSource createFromParcel(Parcel source) { return new ApplicationInfoApkSource(source); } @Override public ApplicationInfoApkSource[] newArray(int size) { return new ApplicationInfoApkSource[size]; } }; } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/CachedApkSource.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk; import android.content.ContentResolver; import android.net.Uri; import android.os.Parcel; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.os.ParcelCompat; import org.json.JSONException; import org.json.JSONObject; import java.io.File; import java.io.IOException; import java.util.Objects; import io.github.muntashirakon.AppManager.fm.FmProvider; import io.github.muntashirakon.AppManager.history.JsonDeserializer; import io.github.muntashirakon.AppManager.self.filecache.FileCache; import io.github.muntashirakon.AppManager.utils.FileUtils; import io.github.muntashirakon.AppManager.utils.JSONUtils; import io.github.muntashirakon.io.Paths; public class CachedApkSource extends ApkSource { public static final String TAG = CachedApkSource.class.getSimpleName(); @NonNull private final Uri mUri; @Nullable private final String mMimeType; private int mApkFileKey; @Nullable private File mCachedFile; CachedApkSource(@NonNull Uri uri, @Nullable String mimeType) { mUri = Objects.requireNonNull(uri); mMimeType = mimeType; } @NonNull @Override public ApkFile resolve() throws ApkFile.ApkFileException { ApkFile apkFile = ApkFile.getInstance(mApkFileKey); if (apkFile != null && !apkFile.isClosed()) { // Usable past instance return apkFile; } // May need to cache the APK if it's not from our own content provider if (mCachedFile != null && mCachedFile.exists()) { mApkFileKey = ApkFile.createInstance(Uri.fromFile(mCachedFile), mMimeType); } else if (ContentResolver.SCHEME_FILE.equals(mUri.getScheme())) { mApkFileKey = ApkFile.createInstance(mUri, mMimeType); } else if (ContentResolver.SCHEME_CONTENT.equals(mUri.getScheme()) && FmProvider.AUTHORITY.equals(mUri.getAuthority())) { mApkFileKey = ApkFile.createInstance(mUri, mMimeType); } else { // Need caching try { mCachedFile = FileCache.getGlobalFileCache().getCachedFile(Paths.get(mUri)); mApkFileKey = ApkFile.createInstance(Uri.fromFile(mCachedFile), mMimeType); } catch (IOException | SecurityException e) { throw new ApkFile.ApkFileException(e); } } return Objects.requireNonNull(ApkFile.getInstance(mApkFileKey)); } @NonNull @Override public ApkSource toCachedSource() { Uri uri; if (mCachedFile != null && mCachedFile.exists()) { uri = Uri.fromFile(mCachedFile); } else uri = mUri; return new CachedApkSource(uri, mMimeType); } public void cleanup() { FileUtils.deleteSilently(mCachedFile); mCachedFile = null; } protected CachedApkSource(@NonNull Parcel in) { mUri = Objects.requireNonNull(ParcelCompat.readParcelable(in, Uri.class.getClassLoader(), Uri.class)); mMimeType = in.readString(); mApkFileKey = in.readInt(); String file = in.readString(); if (file != null) { mCachedFile = new File(file); } } @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeParcelable(mUri, flags); dest.writeString(mMimeType); dest.writeInt(mApkFileKey); String file = mCachedFile != null ? mCachedFile.getAbsolutePath() : null; dest.writeString(file); } protected CachedApkSource(@NonNull JSONObject jsonObject) throws JSONException { mUri = Uri.parse(jsonObject.getString("uri")); mMimeType = jsonObject.getString("mime_type"); mApkFileKey = jsonObject.getInt("apk_file_key"); String cachedFile = JSONUtils.optString(jsonObject, "cached_file", null); mCachedFile = cachedFile != null ? new File(cachedFile) : null; } @NonNull @Override public JSONObject serializeToJson() throws JSONException { JSONObject jsonObject = new JSONObject(); jsonObject.put("tag", TAG); jsonObject.put("uri", mUri.toString()); jsonObject.put("mime_type", mMimeType); jsonObject.put("apk_file_key", mApkFileKey); jsonObject.put("cached_file", mCachedFile != null ? mCachedFile.getAbsolutePath() : null); return jsonObject; } public static final JsonDeserializer.Creator DESERIALIZER = CachedApkSource::new; public static final Creator CREATOR = new Creator() { @Override public CachedApkSource createFromParcel(Parcel source) { return new CachedApkSource(source); } @Override public CachedApkSource[] newArray(int size) { return new CachedApkSource[size]; } }; } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/UriApkSource.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk; import android.net.Uri; import android.os.Parcel; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.os.ParcelCompat; import org.json.JSONException; import org.json.JSONObject; import java.util.Objects; import io.github.muntashirakon.AppManager.history.JsonDeserializer; public class UriApkSource extends ApkSource { public static final String TAG = UriApkSource.class.getSimpleName(); @NonNull private final Uri mUri; @Nullable private final String mMimeType; private int mApkFileKey; public UriApkSource(@NonNull Uri uri, @Nullable String mimeType) { mUri = Objects.requireNonNull(uri); mMimeType = mimeType; } @NonNull @Override public ApkFile resolve() throws ApkFile.ApkFileException { ApkFile apkFile = ApkFile.getInstance(mApkFileKey); if (apkFile != null && !apkFile.isClosed()) { // Usable past instance return apkFile; } mApkFileKey = ApkFile.createInstance(mUri, mMimeType); return Objects.requireNonNull(ApkFile.getInstance(mApkFileKey)); } @NonNull @Override public ApkSource toCachedSource() { return new CachedApkSource(mUri, mMimeType); } protected UriApkSource(@NonNull Parcel in) { mUri = Objects.requireNonNull(ParcelCompat.readParcelable(in, Uri.class.getClassLoader(), Uri.class)); mMimeType = in.readString(); mApkFileKey = in.readInt(); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeParcelable(mUri, flags); dest.writeString(mMimeType); dest.writeInt(mApkFileKey); } protected UriApkSource(@NonNull JSONObject jsonObject) throws JSONException { mUri = Uri.parse(jsonObject.getString("uri")); mMimeType = jsonObject.getString("mime_type"); mApkFileKey = jsonObject.getInt("apk_file_key"); } @NonNull @Override public JSONObject serializeToJson() throws JSONException { JSONObject jsonObject = new JSONObject(); jsonObject.put("tag", TAG); jsonObject.put("uri", mUri.toString()); jsonObject.put("mime_type", mMimeType); jsonObject.put("apk_file_key", mApkFileKey); return jsonObject; } public static final JsonDeserializer.Creator DESERIALIZER = UriApkSource::new; public static final Creator CREATOR = new Creator() { @Override public UriApkSource createFromParcel(Parcel source) { return new UriApkSource(source); } @Override public UriApkSource[] newArray(int size) { return new UriApkSource[size]; } }; } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/behavior/FreezeUnfreeze.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk.behavior; import android.annotation.UserIdInt; import android.app.Notification; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.os.UserHandleHidden; import android.text.SpannableStringBuilder; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.PendingIntentCompat; import androidx.core.content.ContextCompat; import androidx.fragment.app.FragmentActivity; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.compat.PackageManagerCompat; import io.github.muntashirakon.AppManager.utils.ArrayUtils; import io.github.muntashirakon.AppManager.utils.FreezeUtils; import io.github.muntashirakon.AppManager.utils.NotificationUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.dialog.SearchableSingleChoiceDialogBuilder; public final class FreezeUnfreeze { @IntDef(flag = true, value = { FLAG_ON_UNFREEZE_OPEN_APP, FLAG_ON_OPEN_APP_NO_TASK, FLAG_FREEZE_ON_PHONE_LOCKED, }) @Retention(RetentionPolicy.SOURCE) public @interface FreezeFlags { } public static final int FLAG_ON_UNFREEZE_OPEN_APP = 1 << 0; public static final int FLAG_ON_OPEN_APP_NO_TASK = 1 << 1; public static final int FLAG_FREEZE_ON_PHONE_LOCKED = 1 << 2; public static final int PRIVATE_FLAG_FREEZE_FORCE = 1 << 0; private static final String EXTRA_PACKAGE_NAME = "pkg"; private static final String EXTRA_USER_ID = "user"; private static final String EXTRA_FLAGS = "flags"; private static final String EXTRA_FORCE_FREEZE = "force"; @NonNull public static Intent getShortcutIntent(@NonNull Context context, @NonNull FreezeUnfreezeShortcutInfo shortcutInfo) { Intent intent = new Intent(context, FreezeUnfreezeActivity.class); intent.putExtra(EXTRA_PACKAGE_NAME, shortcutInfo.packageName); intent.putExtra(EXTRA_USER_ID, shortcutInfo.userId); intent.putExtra(EXTRA_FLAGS, shortcutInfo.flags); intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_NEW_TASK); return intent; } @NonNull public static Intent getShortcutIntent(@NonNull Context context, @NonNull String packageName, @UserIdInt int userId, int flags) { Intent intent = new Intent(context, FreezeUnfreezeActivity.class); intent.putExtra(EXTRA_PACKAGE_NAME, packageName); intent.putExtra(EXTRA_USER_ID, userId); intent.putExtra(EXTRA_FLAGS, flags); intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_NEW_TASK); return intent; } @Nullable public static FreezeUnfreezeShortcutInfo getShortcutInfo(@NonNull Intent intent) { String packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME); if (packageName == null) { return null; } int userId = intent.getIntExtra(EXTRA_USER_ID, UserHandleHidden.myUserId()); int flags = intent.getIntExtra(EXTRA_FLAGS, 0); boolean force = intent.getBooleanExtra(EXTRA_FORCE_FREEZE, false); FreezeUnfreezeShortcutInfo shortcutInfo = new FreezeUnfreezeShortcutInfo(packageName, userId, flags); if (force) { shortcutInfo.addPrivateFlags(PRIVATE_FLAG_FREEZE_FORCE); } return shortcutInfo; } private static final Integer[] FREEZING_METHODS = new Integer[]{ FreezeUtils.FREEZE_SUSPEND, FreezeUtils.FREEZE_ADV_SUSPEND, FreezeUtils.FREEZE_DISABLE, FreezeUtils.FREEZE_HIDE }; private static final Integer[] FREEZING_METHOD_TITLES = new Integer[]{ R.string.suspend_app, R.string.advanced_suspend_app, R.string.disable, R.string.hide_app }; private static final Integer[] FREEZING_METHOD_DESCRIPTIONS = new Integer[]{ R.string.suspend_app_description, R.string.advanced_suspend_app_description, R.string.disable_app_description, R.string.hide_app_description }; @NonNull public static SearchableSingleChoiceDialogBuilder getFreezeDialog( @NonNull Context context, @FreezeUtils.FreezeMethod int selectedType) { CharSequence[] itemDescription = new CharSequence[FREEZING_METHODS.length]; for (int i = 0; i < FREEZING_METHODS.length; ++i) { itemDescription[i] = new SpannableStringBuilder() .append(context.getString(FREEZING_METHOD_TITLES[i])) .append("\n") .append(UIUtils.getSmallerText(context.getString(FREEZING_METHOD_DESCRIPTIONS[i]))); } return new SearchableSingleChoiceDialogBuilder<>(context, FREEZING_METHODS, itemDescription) .setSelectionIndex(ArrayUtils.indexOf(FREEZING_METHODS, selectedType)); } static void launchApp(@NonNull FragmentActivity activity, @NonNull FreezeUnfreezeShortcutInfo shortcutInfo) { Intent launchIntent = PackageManagerCompat.getLaunchIntentForPackage(shortcutInfo.packageName, shortcutInfo.userId); if (launchIntent == null) { // No launch intent found return; } // launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_TASK_ON_HOME | Intent.FLAG_ACTIVITY_NEW_DOCUMENT); if ((shortcutInfo.flags & FLAG_ON_OPEN_APP_NO_TASK) != 0) { launchIntent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); } try { activity.startActivity(launchIntent); Intent intent = getShortcutIntent(activity, shortcutInfo); intent.putExtra(EXTRA_FORCE_FREEZE, true); int requestCode = shortcutInfo.hashCode(); PendingIntent pendingIntent = PendingIntentCompat.getActivity(activity, requestCode, intent, PendingIntent.FLAG_ONE_SHOT, false); // There's a small chance that the notification by shortcutInfo.hasCode() already exists, in that case, // find the next one. This will cause trouble with dismissing the notification, but this is a viable // trade-off. String notificationTag = String.valueOf(requestCode); NotificationUtils.displayFreezeUnfreezeNotification(activity, notificationTag, builder -> builder .setDefaults(Notification.DEFAULT_ALL) .setWhen(System.currentTimeMillis()) .setSmallIcon(R.drawable.ic_default_notification) .setTicker(activity.getText(R.string.freeze)) .setContentTitle(shortcutInfo.getName()) .setContentText(activity.getString(R.string.tap_to_freeze_app)) .setContentIntent(pendingIntent) .build()); if ((shortcutInfo.flags & FLAG_FREEZE_ON_PHONE_LOCKED) != 0) { Intent service = new Intent(intent) .setClassName(activity, FreezeUnfreezeService.class.getName()); ContextCompat.startForegroundService(activity, service); } } catch (Throwable th) { UIUtils.displayLongToast(th.getLocalizedMessage()); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/behavior/FreezeUnfreezeActivity.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk.behavior; import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.MATCH_DISABLED_COMPONENTS; import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.MATCH_UNINSTALLED_PACKAGES; import static io.github.muntashirakon.AppManager.utils.UIUtils.getBitmapFromDrawable; import static io.github.muntashirakon.AppManager.utils.UIUtils.getDimmedBitmap; import android.app.Application; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.os.Bundle; import android.os.RemoteException; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.pm.ShortcutInfoCompat; import androidx.core.content.pm.ShortcutManagerCompat; import androidx.core.graphics.drawable.IconCompat; import androidx.core.util.Pair; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModelProvider; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.util.Collections; import java.util.LinkedList; import java.util.Optional; import java.util.Queue; import io.github.muntashirakon.AppManager.BaseActivity; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.compat.PackageManagerCompat; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.utils.FreezeUtils; import io.github.muntashirakon.AppManager.utils.NotificationUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; public class FreezeUnfreezeActivity extends BaseActivity { private FreezeUnfreezeViewModel mViewModel; @Override protected void onAuthenticated(@Nullable Bundle savedInstanceState) { mViewModel = new ViewModelProvider(this).get(FreezeUnfreezeViewModel.class); if (!SelfPermissions.canFreezeUnfreezePackages()) { UIUtils.displayShortToast(R.string.only_works_in_root_or_adb_mode); finish(); return; } FreezeUnfreezeShortcutInfo i = FreezeUnfreeze.getShortcutInfo(getIntent()); if (i != null) { hideNotification(i); mViewModel.addToPendingShortcuts(i); mViewModel.checkNextFrozen(); } else { finish(); return; } mViewModel.mIsFrozenLiveData.observe(this, shortcutInfoBooleanPair -> { if (shortcutInfoBooleanPair == null) { // End of queue reached finish(); return; } FreezeUnfreezeShortcutInfo shortcutInfo = shortcutInfoBooleanPair.first; Intent intent = FreezeUnfreeze.getShortcutIntent(this, shortcutInfo); // Set action for shortcut intent.setAction(Intent.ACTION_CREATE_SHORTCUT); ShortcutInfoCompat shortcutInfoCompat = new ShortcutInfoCompat.Builder(this, shortcutInfo.getId()) .setShortLabel(shortcutInfo.getName()) .setLongLabel(shortcutInfo.getName()) .setIcon(IconCompat.createWithBitmap(shortcutInfo.getIcon())) .setIntent(intent) .build(); ShortcutManagerCompat.updateShortcuts(this, Collections.singletonList(shortcutInfoCompat)); // Launch app if requested if (!shortcutInfoBooleanPair.second && (shortcutInfo.flags & FreezeUnfreeze.FLAG_ON_UNFREEZE_OPEN_APP) != 0) { FreezeUnfreeze.launchApp(this, shortcutInfo); } mViewModel.checkNextFrozen(); }); mViewModel.mOpenAppOrFreeze.observe(this, shortcutInfo -> new MaterialAlertDialogBuilder(this) .setTitle(R.string.freeze_unfreeze) .setMessage(R.string.choose_what_to_do) .setPositiveButton(R.string.open, (dialog, which) -> FreezeUnfreeze.launchApp(this, shortcutInfo)) .setNegativeButton(R.string.freeze, (dialog, which) -> mViewModel.freezeFinal(shortcutInfo)) .setOnDismissListener(v -> mViewModel.checkNextFrozen()) .show()); } @Override public boolean getTransparentBackground() { return true; } @Override protected void onNewIntent(@NonNull Intent intent) { super.onNewIntent(intent); if (!SelfPermissions.canFreezeUnfreezePackages()) { UIUtils.displayShortToast(R.string.only_works_in_root_or_adb_mode); finish(); return; } FreezeUnfreezeShortcutInfo shortcutInfo = FreezeUnfreeze.getShortcutInfo(getIntent()); if (mViewModel != null && shortcutInfo != null) { hideNotification(shortcutInfo); mViewModel.addToPendingShortcuts(shortcutInfo); } } private void hideNotification(@Nullable FreezeUnfreezeShortcutInfo shortcutInfo) { if (shortcutInfo == null) return; String notificationTag = String.valueOf(shortcutInfo.hashCode()); NotificationUtils.getFreezeUnfreezeNotificationManager(this).cancel(notificationTag, 1); } public static class FreezeUnfreezeViewModel extends AndroidViewModel { private final MutableLiveData> mIsFrozenLiveData = new MutableLiveData<>(); private final MutableLiveData mOpenAppOrFreeze = new MutableLiveData<>(); private final Queue mPendingShortcuts = new LinkedList<>(); public FreezeUnfreezeViewModel(@NonNull Application application) { super(application); } public void addToPendingShortcuts(@NonNull FreezeUnfreezeShortcutInfo shortcutInfo) { synchronized (mPendingShortcuts) { mPendingShortcuts.add(shortcutInfo); } } public void checkNextFrozen() { ThreadUtils.postOnBackgroundThread(() -> { FreezeUnfreezeShortcutInfo shortcutInfo; synchronized (mPendingShortcuts) { shortcutInfo = mPendingShortcuts.poll(); } if (shortcutInfo == null) { mIsFrozenLiveData.postValue(null); return; } boolean forceFreeze = (shortcutInfo.getPrivateFlags() & FreezeUnfreeze.PRIVATE_FLAG_FREEZE_FORCE) != 0; try { ApplicationInfo applicationInfo = PackageManagerCompat.getApplicationInfo(shortcutInfo.packageName, MATCH_UNINSTALLED_PACKAGES | MATCH_DISABLED_COMPONENTS | PackageManagerCompat.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES, shortcutInfo.userId); Bitmap icon = getBitmapFromDrawable(applicationInfo.loadIcon(getApplication().getPackageManager())); shortcutInfo.setName(applicationInfo.loadLabel(getApplication().getPackageManager())); boolean isFrozen = !forceFreeze && FreezeUtils.isFrozen(applicationInfo); if (isFrozen) { FreezeUtils.unfreeze(shortcutInfo.packageName, shortcutInfo.userId); shortcutInfo.setIcon(icon); } else { shortcutInfo.setIcon(getDimmedBitmap(icon)); if (!forceFreeze && (shortcutInfo.flags & FreezeUnfreeze.FLAG_ON_UNFREEZE_OPEN_APP) != 0) { // Ask whether to open or freeze the app mOpenAppOrFreeze.postValue(shortcutInfo); return; } int freezeType = Optional.ofNullable(FreezeUtils.loadFreezeMethod(shortcutInfo.packageName)) .orElse(Prefs.Blocking.getDefaultFreezingMethod()); FreezeUtils.freeze(shortcutInfo.packageName, shortcutInfo.userId, freezeType); } mIsFrozenLiveData.postValue(new Pair<>(shortcutInfo, !isFrozen)); } catch (RemoteException | PackageManager.NameNotFoundException e) { e.printStackTrace(); } }); } public void freezeFinal(FreezeUnfreezeShortcutInfo shortcutInfo) { ThreadUtils.postOnBackgroundThread(() -> { try { int freezeType = Optional.ofNullable(FreezeUtils.loadFreezeMethod(shortcutInfo.packageName)) .orElse(Prefs.Blocking.getDefaultFreezingMethod()); FreezeUtils.freeze(shortcutInfo.packageName, shortcutInfo.userId, freezeType); mIsFrozenLiveData.postValue(new Pair<>(shortcutInfo, true)); } catch (RemoteException e) { e.printStackTrace(); } }); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/behavior/FreezeUnfreezeService.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk.behavior; import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.MATCH_DISABLED_COMPONENTS; import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.MATCH_UNINSTALLED_PACKAGES; import static io.github.muntashirakon.AppManager.utils.UIUtils.getBitmapFromDrawable; import static io.github.muntashirakon.AppManager.utils.UIUtils.getDimmedBitmap; import android.app.PendingIntent; import android.app.Service; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.os.IBinder; import android.os.PowerManager; import android.os.RemoteException; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import androidx.core.app.PendingIntentCompat; import androidx.core.app.ServiceCompat; import androidx.core.content.ContextCompat; import androidx.core.content.pm.ShortcutInfoCompat; import androidx.core.content.pm.ShortcutManagerCompat; import androidx.core.graphics.drawable.IconCompat; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.concurrent.Future; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.DummyActivity; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.compat.PackageManagerCompat; import io.github.muntashirakon.AppManager.misc.ScreenLockChecker; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.types.ForegroundService; import io.github.muntashirakon.AppManager.utils.CpuUtils; import io.github.muntashirakon.AppManager.utils.FreezeUtils; import io.github.muntashirakon.AppManager.utils.NotificationUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; public class FreezeUnfreezeService extends Service { public static final String TAG = FreezeUnfreezeService.class.getSimpleName(); public static final String CHANNEL_ID = BuildConfig.APPLICATION_ID + ".channel.FREEZE_UNFREEZE_MONITOR"; private static final String STOP_ACTION = BuildConfig.APPLICATION_ID + ".action.STOP_FREEZE_UNFREEZE_MONITOR"; private final Map mPackagesToShortcut = new HashMap<>(); private final Map mPackagesToNotificationTag = new HashMap<>(); private ScreenLockChecker mScreenLockChecker; private final BroadcastReceiver mScreenLockedReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { try { if (mCheckLockResult != null) { mCheckLockResult.cancel(true); } mCheckLockResult = ThreadUtils.postOnBackgroundThread(() -> { if (mScreenLockChecker == null) { mScreenLockChecker = new ScreenLockChecker(FreezeUnfreezeService.this, () -> freezeAllPackages()); } mScreenLockChecker.checkLock(); }); } catch (Throwable th) { th.printStackTrace(); } } }; private boolean mIsWorking; @Nullable private Future mCheckLockResult; private PowerManager.WakeLock mWakeLock; @Override public void onCreate() { super.onCreate(); mWakeLock = CpuUtils.getPartialWakeLock("freeze_unfreeze"); } @Override public int onStartCommand(@Nullable Intent intent, int flags, int startId) { if (intent != null && STOP_ACTION.equals(intent.getAction())) { stopSelf(); return START_NOT_STICKY; } onHandleIntent(intent); if (mIsWorking) { return START_NOT_STICKY; } mIsWorking = true; NotificationUtils.getNewNotificationManager(this, CHANNEL_ID, "Freeze/unfreeze Monitor", NotificationManagerCompat.IMPORTANCE_LOW); Intent stopIntent = new Intent(this, FreezeUnfreezeService.class).setAction(STOP_ACTION); PendingIntent pendingIntent = PendingIntentCompat.getService(this, 0, stopIntent, PendingIntent.FLAG_ONE_SHOT, false); NotificationCompat.Action stopServiceAction = new NotificationCompat.Action.Builder(null, getString(R.string.action_stop_service), pendingIntent) .setAuthenticationRequired(true) .build(); NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID) .setLocalOnly(true) .setOngoing(true) .setContentTitle(null) .setContentText(getString(R.string.waiting_for_the_phone_to_be_locked)) .setSmallIcon(R.drawable.ic_default_notification) .setSubText(getText(R.string.freeze_unfreeze)) .setPriority(NotificationCompat.PRIORITY_LOW) .addAction(stopServiceAction); ForegroundService.start(this, NotificationUtils.nextNotificationId(null), builder.build(), ForegroundService.FOREGROUND_SERVICE_TYPE_DATA_SYNC | ForegroundService.FOREGROUND_SERVICE_TYPE_SPECIAL_USE); IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_ON); filter.addAction(Intent.ACTION_SCREEN_OFF); filter.addAction(Intent.ACTION_USER_PRESENT); ContextCompat.registerReceiver(this, mScreenLockedReceiver, filter, ContextCompat.RECEIVER_EXPORTED); return START_NOT_STICKY; } @Override public void onTaskRemoved(Intent rootIntent) { // https://issuetracker.google.com/issues/36967794 Intent intent = new Intent(this, DummyActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); } @Override public void onDestroy() { unregisterReceiver(mScreenLockedReceiver); ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE); if (mCheckLockResult != null) { mCheckLockResult.cancel(true); } CpuUtils.releaseWakeLock(mWakeLock); super.onDestroy(); } @Nullable @Override public IBinder onBind(Intent intent) { return null; } private void onHandleIntent(@Nullable Intent intent) { if (intent == null) return; FreezeUnfreezeShortcutInfo shortcutInfo = FreezeUnfreeze.getShortcutInfo(intent); if (shortcutInfo == null) return; mPackagesToShortcut.put(shortcutInfo.packageName, shortcutInfo); String notificationTag = String.valueOf(shortcutInfo.hashCode()); mPackagesToNotificationTag.put(shortcutInfo.packageName, notificationTag); } @WorkerThread private void freezeAllPackages() { for (String packageName : mPackagesToShortcut.keySet()) { FreezeUnfreezeShortcutInfo shortcutInfo = mPackagesToShortcut.get(packageName); String notificationTag = mPackagesToNotificationTag.get(packageName); if (shortcutInfo != null) { try { ApplicationInfo applicationInfo = PackageManagerCompat.getApplicationInfo(shortcutInfo.packageName, MATCH_UNINSTALLED_PACKAGES | MATCH_DISABLED_COMPONENTS | PackageManagerCompat.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES, shortcutInfo.userId); Bitmap icon = getBitmapFromDrawable(applicationInfo.loadIcon(getApplication().getPackageManager())); shortcutInfo.setName(applicationInfo.loadLabel(getApplication().getPackageManager())); int freezeType = Optional.ofNullable(FreezeUtils.loadFreezeMethod(shortcutInfo.packageName)) .orElse(Prefs.Blocking.getDefaultFreezingMethod()); FreezeUtils.freeze(shortcutInfo.packageName, shortcutInfo.userId, freezeType); shortcutInfo.setIcon(getDimmedBitmap(icon)); updateShortcuts(shortcutInfo); } catch (RemoteException | PackageManager.NameNotFoundException e) { e.printStackTrace(); } } if (notificationTag != null) { NotificationUtils.getFreezeUnfreezeNotificationManager(this).cancel(notificationTag, 1); } } stopSelf(); } private void updateShortcuts(@NonNull FreezeUnfreezeShortcutInfo shortcutInfo) { Intent intent = FreezeUnfreeze.getShortcutIntent(this, shortcutInfo); // Set action for shortcut intent.setAction(Intent.ACTION_CREATE_SHORTCUT); ShortcutInfoCompat shortcutInfoCompat = new ShortcutInfoCompat.Builder(this, shortcutInfo.getId()) .setShortLabel(shortcutInfo.getName()) .setLongLabel(shortcutInfo.getName()) .setIcon(IconCompat.createWithBitmap(shortcutInfo.getIcon())) .setIntent(intent) .build(); ShortcutManagerCompat.updateShortcuts(this, Collections.singletonList(shortcutInfoCompat)); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/behavior/FreezeUnfreezeShortcutInfo.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk.behavior; import android.annotation.UserIdInt; import android.content.Context; import android.content.Intent; import android.os.Parcel; import androidx.annotation.NonNull; import java.util.Objects; import io.github.muntashirakon.AppManager.shortcut.ShortcutInfo; public class FreezeUnfreezeShortcutInfo extends ShortcutInfo { @NonNull public final String packageName; @UserIdInt public final int userId; @FreezeUnfreeze.FreezeFlags public final int flags; private int mPrivateFlags; public FreezeUnfreezeShortcutInfo(@NonNull String packageName, int userId, int flags) { setId("freeze:u=" + userId + ",p=" + packageName); this.packageName = packageName; this.userId = userId; this.flags = flags; } protected FreezeUnfreezeShortcutInfo(Parcel in) { super(in); packageName = in.readString(); userId = in.readInt(); flags = in.readInt(); mPrivateFlags = in.readInt(); } public int getPrivateFlags() { return mPrivateFlags; } public void setPrivateFlags(int privateFlags) { mPrivateFlags = privateFlags; } public void addPrivateFlags(int privateFlags) { mPrivateFlags |= privateFlags; } public void removePrivateFlags(int privateFlags) { mPrivateFlags &= ~privateFlags; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeString(packageName); dest.writeInt(userId); dest.writeInt(flags); dest.writeInt(mPrivateFlags); } @Override public int hashCode() { return Objects.hash(packageName, userId); } @Override public Intent toShortcutIntent(@NonNull Context context) { return FreezeUnfreeze.getShortcutIntent(context, this); } public static final Creator CREATOR = new Creator() { @Override public FreezeUnfreezeShortcutInfo createFromParcel(Parcel source) { return new FreezeUnfreezeShortcutInfo(source); } @Override public FreezeUnfreezeShortcutInfo[] newArray(int size) { return new FreezeUnfreezeShortcutInfo[size]; } }; } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/dexopt/DexOptDialog.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk.dexopt; import android.app.Dialog; import android.content.Intent; import android.os.Build; import android.os.Bundle; import android.text.Editable; import android.text.TextUtils; import android.view.View; import android.widget.AutoCompleteTextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.fragment.app.DialogFragment; import com.google.android.material.checkbox.MaterialCheckBox; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.util.ArrayList; import java.util.List; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.batchops.BatchOpsManager; import io.github.muntashirakon.AppManager.batchops.BatchOpsService; import io.github.muntashirakon.AppManager.batchops.BatchQueueItem; import io.github.muntashirakon.AppManager.batchops.struct.BatchDexOptOptions; import io.github.muntashirakon.AppManager.settings.Ops; import io.github.muntashirakon.AppManager.users.Users; import io.github.muntashirakon.adapters.AnyFilterArrayAdapter; public class DexOptDialog extends DialogFragment { public static final String TAG = DexOptDialog.class.getSimpleName(); private static final String ARG_PACKAGES = "pkg"; @NonNull public static DexOptDialog getInstance(@Nullable String[] packages) { DexOptDialog dialog = new DexOptDialog(); Bundle args = new Bundle(); args.putStringArray(ARG_PACKAGES, packages); dialog.setArguments(args); return dialog; } private static final List COMPILER_FILTERS = new ArrayList() {{ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { add("verify-none"); // = assume-verified add("verify-at-runtime"); // = extract add("verify-profile"); // = verify add("interpret-only"); // = quicken add("time"); // = space add("balanced"); // speed } else { add("assume-verified"); add("extract"); add("verify"); add("quicken"); } add("space"); add("space-profile"); add("speed"); add("speed-profile"); add("everything"); add("everything-profile"); }}; private final DexOptOptions mOptions = DexOptOptions.getDefault(); @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { mOptions.packages = requireArguments().getStringArray(ARG_PACKAGES); int uid = Users.getSelfOrRemoteUid(); boolean isRootOrSystem = uid == Ops.SYSTEM_UID || uid == Ops.ROOT_UID; // Inflate view View view = View.inflate(requireContext(), R.layout.dialog_dexopt, null); AutoCompleteTextView compilerFilterSelectionView = view.findViewById(R.id.compiler_filter); MaterialCheckBox compileLayoutsCheck = view.findViewById(R.id.compile_layouts); MaterialCheckBox clearProfileDataCheck = view.findViewById(R.id.clear_profile_data); MaterialCheckBox checkProfilesCheck = view.findViewById(R.id.check_profiles); MaterialCheckBox forceCompilationCheck = view.findViewById(R.id.force_compilation); MaterialCheckBox forceDexOptCheck = view.findViewById(R.id.force_dexopt); compilerFilterSelectionView.setText(mOptions.compilerFiler); checkProfilesCheck.setChecked(mOptions.checkProfiles); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // Compile layout options was introduced in Android 10 and removed in Android 12 compileLayoutsCheck.setVisibility(View.GONE); } if (!isRootOrSystem) { // clearProfileData and forceDexOpt can only be run as root/system clearProfileDataCheck.setVisibility(View.GONE); forceDexOptCheck.setVisibility(View.GONE); } // Set listeners compilerFilterSelectionView.setAdapter(new AnyFilterArrayAdapter<>(requireContext(), io.github.muntashirakon.ui.R.layout.auto_complete_dropdown_item, COMPILER_FILTERS)); compileLayoutsCheck.setOnCheckedChangeListener((buttonView, isChecked) -> mOptions.compileLayouts = isChecked); clearProfileDataCheck.setOnCheckedChangeListener((buttonView, isChecked) -> mOptions.clearProfileData = isChecked); checkProfilesCheck.setOnCheckedChangeListener((buttonView, isChecked) -> mOptions.checkProfiles = isChecked); forceCompilationCheck.setOnCheckedChangeListener((buttonView, isChecked) -> mOptions.forceCompilation = isChecked); forceDexOptCheck.setOnCheckedChangeListener((buttonView, isChecked) -> mOptions.forceDexOpt = isChecked); if (isRootOrSystem) { forceDexOptCheck.setChecked(true); } return new MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.title_perform_runtime_optimization_to_apps) .setView(view) .setPositiveButton(R.string.action_run, (dialog, which) -> { Editable compilerFilterRaw = compilerFilterSelectionView.getText(); if (TextUtils.isEmpty(compilerFilterRaw)) { return; } String compilerFiler = compilerFilterRaw.toString().trim(); if (!COMPILER_FILTERS.contains(compilerFiler)) { // Invalid compiler filter return; } mOptions.compilerFiler = compilerFiler; launchOp(); }) .setNegativeButton(R.string.cancel, null) .setNeutralButton(R.string.reset_to_default, (dialog, which) -> { mOptions.compilerFiler = DexOptOptions.getDefaultCompilerFilterForInstallation(); mOptions.forceCompilation = true; mOptions.clearProfileData = true; launchOp(); }) .create(); } private void launchOp() { BatchDexOptOptions options = new BatchDexOptOptions(mOptions); BatchQueueItem queueItem = BatchQueueItem.getBatchOpQueue( BatchOpsManager.OP_DEXOPT, null, null, options); Intent intent = BatchOpsService.getServiceIntent(requireContext(), queueItem); ContextCompat.startForegroundService(requireContext(), intent); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/dexopt/DexOptOptions.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk.dexopt; import android.os.Parcel; import android.os.Parcelable; import android.os.SystemProperties; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.json.JSONException; import org.json.JSONObject; import io.github.muntashirakon.AppManager.history.IJsonSerializer; import io.github.muntashirakon.AppManager.history.JsonDeserializer; import io.github.muntashirakon.AppManager.utils.JSONUtils; public class DexOptOptions implements Parcelable, IJsonSerializer { @NonNull public static DexOptOptions getDefault() { DexOptOptions options = new DexOptOptions(); options.compilerFiler = getDefaultCompilerFilterForInstallation(); options.checkProfiles = SystemProperties.getBoolean("dalvik.vm.usejitprofiles", false); options.bootComplete = true; return options; } @Nullable public String[] packages; @Nullable public String compilerFiler; public boolean compileLayouts; public boolean clearProfileData; public boolean checkProfiles; public boolean bootComplete; public boolean forceCompilation; public boolean forceDexOpt; private DexOptOptions() { } protected DexOptOptions(@NonNull Parcel in) { packages = in.createStringArray(); compilerFiler = in.readString(); compileLayouts = in.readByte() != 0; clearProfileData = in.readByte() != 0; checkProfiles = in.readByte() != 0; bootComplete = in.readByte() != 0; forceCompilation = in.readByte() != 0; forceDexOpt = in.readByte() != 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeStringArray(packages); dest.writeString(compilerFiler); dest.writeByte((byte) (compileLayouts ? 1 : 0)); dest.writeByte((byte) (clearProfileData ? 1 : 0)); dest.writeByte((byte) (checkProfiles ? 1 : 0)); dest.writeByte((byte) (bootComplete ? 1 : 0)); dest.writeByte((byte) (forceCompilation ? 1 : 0)); dest.writeByte((byte) (forceDexOpt ? 1 : 0)); } protected DexOptOptions(@NonNull JSONObject jsonObject) throws JSONException { packages = JSONUtils.getArray(String.class, jsonObject.optJSONArray("packages")); compilerFiler = jsonObject.getString("compiler_filter"); compileLayouts = jsonObject.getBoolean("compile_layouts"); clearProfileData = jsonObject.getBoolean("clear_profile_data"); checkProfiles = jsonObject.getBoolean("check_profiles"); bootComplete = jsonObject.getBoolean("boot_complete"); forceCompilation = jsonObject.getBoolean("force_compilation"); forceDexOpt = jsonObject.getBoolean("force_dex_opt"); } public static final JsonDeserializer.Creator DESERIALIZER = DexOptOptions::new; @NonNull @Override public JSONObject serializeToJson() throws JSONException { JSONObject jsonObject = new JSONObject(); jsonObject.put("packages", JSONUtils.getJSONArray(packages)); jsonObject.put("compiler_filer", compilerFiler); jsonObject.put("compile_layouts", compileLayouts); jsonObject.put("clear_profile_data", clearProfileData); jsonObject.put("check_profiles", checkProfiles); jsonObject.put("boot_complete", bootComplete); jsonObject.put("force_compilation", forceCompilation); jsonObject.put("force_dex_opt", forceDexOpt); return jsonObject; } @Override public int describeContents() { return 0; } public static final Creator CREATOR = new Creator() { @NonNull @Override public DexOptOptions createFromParcel(@NonNull Parcel in) { return new DexOptOptions(in); } @NonNull @Override public DexOptOptions[] newArray(int size) { return new DexOptOptions[size]; } }; @NonNull static String getDefaultCompilerFilterForInstallation() { String profile = SystemProperties.get("pm.dexopt.install"); if (TextUtils.isEmpty(profile)) { return "speed"; } return profile; } @NonNull static String getDefaultCompilerFilter() { String profile = SystemProperties.get("dalvik.vm.dex2oat-filter"); if (TextUtils.isEmpty(profile)) { return "speed"; } return profile; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/dexopt/DexOptimizer.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk.dexopt; import android.content.pm.IPackageManager; import android.os.Build; import android.os.RemoteException; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import java.util.Objects; import io.github.muntashirakon.AppManager.self.SelfPermissions; @RequiresApi(Build.VERSION_CODES.N) public class DexOptimizer { @NonNull private final IPackageManager mPm; private final String mPackageName; @Nullable private Exception mLastError; public DexOptimizer(@NonNull IPackageManager pm, @NonNull String packageName) { mPm = pm; mPackageName = packageName; } @Nullable public Exception getLastError() { try { return mLastError; } finally { mLastError = null; } } @SuppressWarnings("deprecation") public boolean performDexOptMode(boolean checkProfiles, @NonNull String targetCompilerFilter, boolean force, boolean bootComplete, @Nullable String splitName) { try { // Allowed for root/system/shell and installer app if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { return mPm.performDexOptMode(mPackageName, checkProfiles, targetCompilerFilter, force, bootComplete, splitName); } else { return mPm.performDexOptMode(mPackageName, checkProfiles, targetCompilerFilter, force); } } catch (RemoteException | SecurityException e) { mLastError = e; } return false; } public boolean clearApplicationProfileData() { try { // Allowed for only root/system mPm.clearApplicationProfileData(mPackageName); return true; } catch (RemoteException | SecurityException e) { mLastError = e; } return false; } @SuppressWarnings("deprecation") @RequiresApi(Build.VERSION_CODES.Q) public boolean compileLayouts() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // Removed again return false; } try { return mPm.compileLayouts(mPackageName); } catch (RemoteException | SecurityException e) { mLastError = e; } return false; } public boolean forceDexOpt() { try { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE && SelfPermissions.isSystemOrRoot()) { // Allowed for only root/system try { mPm.forceDexOpt(mPackageName); return true; } catch (IllegalArgumentException e) { if (Objects.equals(e.getMessage(), "reason -1 invalid")) { // AOSP bug: https://github.com/MuntashirAkon/AppManager/issues/1131 return forceDexOptUnprivileged(); } // Other valid error throw e; } } return forceDexOptUnprivileged(); } catch (RemoteException | SecurityException | IllegalArgumentException e) { mLastError = e; } catch (IllegalStateException e) { String message = e.getMessage(); if (message != null && message.startsWith("Failed to dexopt: 0")) { // Skipped. This could be due to many reasons: // 1. Package is android and does not need optimization // 2. Package does not have code return true; } mLastError = e; } return false; } private boolean forceDexOptUnprivileged() { // forceDexOpt only applies certain set of configurations with performDexOptMode. So, it's possible to // do the same using performDexOptMode in unprivileged mode // https://android.googlesource.com/platform/frameworks/base/+/eb4af72f526c8351ad22322a635507a54c9ad1b8/services/core/java/com/android/server/pm/DexOptHelper.java#495 return performDexOptMode(false, DexOptOptions.getDefaultCompilerFilter(), true, true, null); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/ApkQueueItem.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk.installer; import static io.github.muntashirakon.AppManager.apk.installer.SupportedAppStores.isAppStoreSupported; import android.content.ContentResolver; import android.content.Intent; import android.content.pm.PackageInstaller; import android.net.Uri; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.os.ParcelCompat; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.List; import java.util.Objects; import io.github.muntashirakon.AppManager.apk.ApkSource; import io.github.muntashirakon.AppManager.history.IJsonSerializer; import io.github.muntashirakon.AppManager.history.JsonDeserializer; import io.github.muntashirakon.AppManager.intercept.IntentCompat; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.ExUtils; import io.github.muntashirakon.AppManager.utils.JSONUtils; public class ApkQueueItem implements Parcelable, IJsonSerializer { @NonNull static List fromIntent(@NonNull Intent intent, @Nullable String originatingPackage) { List apkQueueItems = new ArrayList<>(); List uris = IntentCompat.getDataUris(intent); if (uris == null) { return apkQueueItems; } ContentResolver cr = ContextUtils.getContext().getContentResolver(); String mimeType = intent.getType(); Uri originatingUri = IntentCompat.getParcelableExtra(intent, Intent.EXTRA_ORIGINATING_URI, Uri.class); int takeFlags = intent.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); for (Uri uri : uris) { ApkQueueItem item; if ("package".equals(uri.getScheme())) { item = new ApkQueueItem(uri.getSchemeSpecificPart(), true); } else { // file, content item = new ApkQueueItem(ApkSource.getCachedApkSource(uri, mimeType)); item.mOriginatingUri = originatingUri; item.mOriginatingPackage = originatingPackage; if (takeFlags > 0) { ExUtils.exceptionAsIgnored(() -> cr.takePersistableUriPermission(uri, takeFlags)); } } apkQueueItems.add(item); } return apkQueueItems; } @NonNull public static ApkQueueItem fromApkSource(@NonNull ApkSource apkSource) { return new ApkQueueItem(apkSource.toCachedSource()); } @Nullable private String mPackageName; @Nullable private String mAppLabel; private final boolean mInstallExisting; @Nullable private String mOriginatingPackage; @Nullable private Uri mOriginatingUri; @Nullable private ApkSource mApkSource; @Nullable private InstallerOptions mInstallerOptions; @Nullable private ArrayList mSelectedSplits; private ApkQueueItem(@NonNull String packageName, boolean installExisting) { mPackageName = Objects.requireNonNull(packageName); mInstallExisting = installExisting; assert installExisting; } private ApkQueueItem(@NonNull ApkSource apkSource) { mApkSource = Objects.requireNonNull(apkSource); mInstallExisting = false; } protected ApkQueueItem(@NonNull Parcel in) { mPackageName = in.readString(); mAppLabel = in.readString(); mInstallExisting = in.readByte() != 0; mOriginatingPackage = in.readString(); mOriginatingUri = ParcelCompat.readParcelable(in, Uri.class.getClassLoader(), Uri.class); mApkSource = ParcelCompat.readParcelable(in, ApkSource.class.getClassLoader(), ApkSource.class); mInstallerOptions = ParcelCompat.readParcelable(in, InstallerOptions.class.getClassLoader(), InstallerOptions.class); mSelectedSplits = new ArrayList<>(); in.readStringList(mSelectedSplits); } @Nullable public String getPackageName() { return mPackageName; } public void setPackageName(@Nullable String packageName) { mPackageName = packageName; } public boolean isInstallExisting() { return mInstallExisting; } @Nullable public ApkSource getApkSource() { return mApkSource; } public void setApkSource(@Nullable ApkSource apkSource) { mApkSource = apkSource; } @Nullable public InstallerOptions getInstallerOptions() { return mInstallerOptions; } public void setInstallerOptions(@Nullable InstallerOptions installerOptions) { if (installerOptions != null) { installerOptions.setOriginatingPackage(mOriginatingPackage); installerOptions.setOriginatingUri(mOriginatingUri); // Set package source to PACKAGE_SOURCE_STORE if it's supported if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && mOriginatingPackage != null && isAppStoreSupported(mOriginatingPackage)) { installerOptions.setPackageSource(PackageInstaller.PACKAGE_SOURCE_STORE); } } mInstallerOptions = installerOptions; } public void setSelectedSplits(@NonNull ArrayList selectedSplits) { mSelectedSplits = selectedSplits; } @Nullable public ArrayList getSelectedSplits() { return mSelectedSplits; } @Nullable public String getAppLabel() { return mAppLabel; } public void setAppLabel(@Nullable String appLabel) { mAppLabel = appLabel; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeString(mPackageName); dest.writeString(mAppLabel); dest.writeByte((byte) (mInstallExisting ? 1 : 0)); dest.writeString(mOriginatingPackage); dest.writeParcelable(mOriginatingUri, flags); dest.writeParcelable(mApkSource, flags); dest.writeParcelable(mInstallerOptions, flags); dest.writeStringList(mSelectedSplits); } protected ApkQueueItem(@NonNull JSONObject jsonObject) throws JSONException { mPackageName = JSONUtils.optString(jsonObject, "package_name", null); mAppLabel = JSONUtils.optString(jsonObject, "app_label", null); mInstallExisting = jsonObject.optBoolean("install_existing", false); mOriginatingPackage = JSONUtils.optString(jsonObject, "originating_package", null); String originatingUri = JSONUtils.optString(jsonObject, "originating_uri", null); mOriginatingUri = originatingUri != null ? Uri.parse(originatingUri) : null; JSONObject apkSource = jsonObject.optJSONObject("apk_source"); mApkSource = apkSource != null ? ApkSource.DESERIALIZER.deserialize(apkSource) : null; JSONObject installerOptions = jsonObject.optJSONObject("installer_options"); mInstallerOptions = installerOptions != null ? InstallerOptions.DESERIALIZER.deserialize(installerOptions) : null; mSelectedSplits = JSONUtils.getArray(jsonObject.optJSONArray("selected_splits")); } @NonNull @Override public JSONObject serializeToJson() throws JSONException { JSONObject jsonObject = new JSONObject(); jsonObject.put("package_name", mPackageName); jsonObject.put("app_label", mAppLabel); jsonObject.put("install_existing", mInstallExisting); jsonObject.put("originating_package", mOriginatingPackage); jsonObject.put("originating_uri", mOriginatingUri != null ? mOriginatingUri.toString() : null); jsonObject.put("apk_source", mApkSource != null ? mApkSource.serializeToJson() : null); jsonObject.put("installer_options", mInstallerOptions != null ? mInstallerOptions.serializeToJson() : null); jsonObject.put("selected_splits", JSONUtils.getJSONArray(mSelectedSplits)); return jsonObject; } public static final JsonDeserializer.Creator DESERIALIZER = ApkQueueItem::new; public static final Creator CREATOR = new Creator() { @Override @NonNull public ApkQueueItem createFromParcel(@NonNull Parcel in) { return new ApkQueueItem(in); } @Override @NonNull public ApkQueueItem[] newArray(int size) { return new ApkQueueItem[size]; } }; } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/InstallerDialogFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk.installer; import android.app.Dialog; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.DialogFragment; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.dialog.DialogTitleBuilder; public class InstallerDialogFragment extends DialogFragment { public static final String TAG = InstallerDialogFragment.class.getSimpleName(); public interface FragmentStartedCallback { void onStart(@NonNull InstallerDialogFragment fragment, @NonNull AlertDialog dialog); } private FragmentStartedCallback mFragmentStartedCallback; private View mDialogView; private DialogTitleBuilder mTitleBuilder; public void setFragmentStartedCallback(FragmentStartedCallback fragmentStartedCallback) { mFragmentStartedCallback = fragmentStartedCallback; } @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { mDialogView = View.inflate(requireContext(), R.layout.dialog_installer, null); mTitleBuilder = new DialogTitleBuilder(requireContext()); View titleView = mTitleBuilder.build(); return new MaterialAlertDialogBuilder(requireContext()) .setCustomTitle(titleView) .setView(mDialogView) .setPositiveButton(" ", null) .setNegativeButton(" ", null) .setNeutralButton(" ", null) .setCancelable(false) .create(); } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return mDialogView; } @Override public void onStart() { super.onStart(); if (mFragmentStartedCallback != null) { mFragmentStartedCallback.onStart(this, (AlertDialog) requireDialog()); } } public View getDialogView() { return mDialogView; } public DialogTitleBuilder getTitleBuilder() { return mTitleBuilder; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/InstallerDialogHelper.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk.installer; import android.content.Context; import android.graphics.drawable.Drawable; import android.view.View; import android.widget.Button; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.LinearLayoutCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentContainerView; import com.google.android.material.textview.MaterialTextView; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.dialog.DialogTitleBuilder; public final class InstallerDialogHelper { public interface OnClickButtonsListener { void triggerInstall(); void triggerCancel(); } private final Context mContext; private final InstallerDialogFragment mFragment; private final DialogTitleBuilder mTitleBuilder; private final FragmentContainerView mFragmentContainer; private final MaterialTextView mMessage; private final LinearLayoutCompat mLayout; private final int mFragmentId = R.id.fragment_container_view_tag; private final AlertDialog mDialog; private final Button mPositiveBtn; private final Button mNegativeBtn; private final Button mNeutralBtn; public InstallerDialogHelper(@NonNull InstallerDialogFragment fragment, AlertDialog dialog) { mContext = fragment.requireContext(); mFragment = fragment; mDialog = dialog; View view = mFragment.getDialogView(); mTitleBuilder = mFragment.getTitleBuilder(); mFragmentContainer = view.findViewById(mFragmentId); mMessage = view.findViewById(R.id.message); mLayout = view.findViewById(R.id.layout); mPositiveBtn = mDialog.getButton(AlertDialog.BUTTON_POSITIVE); mNegativeBtn = mDialog.getButton(AlertDialog.BUTTON_NEGATIVE); mNeutralBtn = mDialog.getButton(AlertDialog.BUTTON_NEUTRAL); } public void initProgress(View.OnClickListener cancelListener) { // Title section mTitleBuilder.setTitle(R.string._undefined) .setStartIcon(R.drawable.ic_get_app) .setSubtitle(null) .setEndIcon(null, null); // Buttons mPositiveBtn.setVisibility(View.GONE); mNeutralBtn.setVisibility(View.GONE); mNegativeBtn.setVisibility(View.VISIBLE); mNegativeBtn.setText(R.string.cancel); mNegativeBtn.setOnClickListener(cancelListener); // Body View v = View.inflate(mContext, R.layout.dialog_progress2, null); TextView tv = v.findViewById(android.R.id.text1); tv.setText(R.string.staging_apk_files); mLayout.setVisibility(View.VISIBLE); mLayout.removeAllViews(); mLayout.addView(v); mMessage.setVisibility(View.GONE); mFragmentContainer.setVisibility(View.GONE); } public void showParseFailedDialog(View.OnClickListener closeListener) { // Title section mTitleBuilder.setTitle(R.string._undefined) .setStartIcon(R.drawable.ic_get_app) .setSubtitle(null) .setEndIcon(null, null); // Buttons mPositiveBtn.setVisibility(View.GONE); mNeutralBtn.setVisibility(View.GONE); mNegativeBtn.setVisibility(View.VISIBLE); mNegativeBtn.setText(R.string.close); mNegativeBtn.setOnClickListener(closeListener); // Body mLayout.setVisibility(View.GONE); mMessage.setVisibility(View.VISIBLE); mMessage.setText(R.string.failed_to_fetch_package_info); mFragmentContainer.setVisibility(View.GONE); } public void onParseSuccess(CharSequence title, CharSequence subtitle, Drawable icon, @Nullable View.OnClickListener optionsClickListener) { mTitleBuilder.setTitle(title) .setStartIcon(icon) .setSubtitle(subtitle); if (optionsClickListener != null) { mTitleBuilder.setEndIcon(R.drawable.ic_settings, optionsClickListener) .setEndIconContentDescription(R.string.installer_options); } else mTitleBuilder.setEndIcon(null, null); } public void showWhatsNewDialog(@StringRes int installButtonRes, Fragment fragment, @NonNull OnClickButtonsListener onClickButtonsListener, @NonNull View.OnClickListener appInfoButtonListener) { // Buttons mNeutralBtn.setVisibility(View.VISIBLE); mNeutralBtn.setText(R.string.app_info); mNeutralBtn.setOnClickListener(appInfoButtonListener); mPositiveBtn.setVisibility(View.VISIBLE); mPositiveBtn.setText(installButtonRes); mPositiveBtn.setOnClickListener(v -> onClickButtonsListener.triggerInstall()); mNegativeBtn.setVisibility(View.VISIBLE); mNegativeBtn.setText(R.string.cancel); mNegativeBtn.setOnClickListener(v -> onClickButtonsListener.triggerCancel()); // Body mLayout.setVisibility(View.GONE); mMessage.setVisibility(View.GONE); mFragmentContainer.setVisibility(View.VISIBLE); mFragment.getChildFragmentManager().beginTransaction().replace(mFragmentId, fragment).commit(); } public void showInstallConfirmationDialog(@StringRes int installButtonRes, @NonNull OnClickButtonsListener onClickButtonsListener, @NonNull View.OnClickListener appInfoButtonListener) { // Buttons mNeutralBtn.setVisibility(View.VISIBLE); mNeutralBtn.setText(R.string.app_info); mNeutralBtn.setOnClickListener(appInfoButtonListener); mPositiveBtn.setVisibility(View.VISIBLE); mPositiveBtn.setText(installButtonRes); mPositiveBtn.setOnClickListener(v -> onClickButtonsListener.triggerInstall()); mNegativeBtn.setVisibility(View.VISIBLE); mNegativeBtn.setText(R.string.cancel); mNegativeBtn.setOnClickListener(v -> onClickButtonsListener.triggerCancel()); // Body mLayout.setVisibility(View.GONE); mMessage.setVisibility(View.VISIBLE); mMessage.setText(R.string.install_app_message); mFragmentContainer.setVisibility(View.GONE); } public void showApkChooserDialog(@StringRes int installButtonRes, Fragment fragment, @NonNull OnClickButtonsListener onClickButtonsListener, @NonNull View.OnClickListener appInfoButtonListener) { // Buttons mNeutralBtn.setVisibility(View.VISIBLE); mNeutralBtn.setText(R.string.app_info); mNeutralBtn.setOnClickListener(appInfoButtonListener); mPositiveBtn.setVisibility(View.VISIBLE); mPositiveBtn.setText(installButtonRes); mPositiveBtn.setOnClickListener(v -> onClickButtonsListener.triggerInstall()); mNegativeBtn.setVisibility(View.VISIBLE); mNegativeBtn.setText(R.string.cancel); mNegativeBtn.setOnClickListener(v -> onClickButtonsListener.triggerCancel()); // Body mLayout.setVisibility(View.GONE); mMessage.setVisibility(View.GONE); mFragmentContainer.setVisibility(View.VISIBLE); mFragment.getChildFragmentManager().beginTransaction().replace(mFragmentId, fragment).commit(); } public void showDowngradeReinstallWarning(CharSequence msg, @NonNull OnClickButtonsListener onClickButtonsListener, @NonNull View.OnClickListener appInfoButtonListener) { // Buttons mNeutralBtn.setVisibility(View.VISIBLE); mNeutralBtn.setText(R.string.app_info); mNeutralBtn.setOnClickListener(appInfoButtonListener); mPositiveBtn.setVisibility(View.VISIBLE); mPositiveBtn.setText(R.string.yes); mPositiveBtn.setOnClickListener(v -> onClickButtonsListener.triggerInstall()); mNegativeBtn.setVisibility(View.VISIBLE); mNegativeBtn.setText(R.string.cancel); mNegativeBtn.setOnClickListener(v -> onClickButtonsListener.triggerCancel()); // Body mLayout.setVisibility(View.GONE); mMessage.setVisibility(View.VISIBLE); mMessage.setText(msg); mFragmentContainer.setVisibility(View.GONE); } public void showSignatureMismatchReinstallWarning(CharSequence msg, @NonNull OnClickButtonsListener onClickButtonsListener, @NonNull View.OnClickListener installOnlyButtonListener, boolean isSystem) { // Buttons mNeutralBtn.setVisibility(View.VISIBLE); mNeutralBtn.setText(R.string.only_install); mNeutralBtn.setOnClickListener(installOnlyButtonListener); mPositiveBtn.setVisibility(isSystem ? View.GONE : View.VISIBLE); mPositiveBtn.setText(R.string.yes); mPositiveBtn.setOnClickListener(v -> onClickButtonsListener.triggerInstall()); mNegativeBtn.setVisibility(View.VISIBLE); mNegativeBtn.setText(R.string.cancel); mNegativeBtn.setOnClickListener(v -> onClickButtonsListener.triggerCancel()); // Body mLayout.setVisibility(View.GONE); mMessage.setVisibility(View.VISIBLE); mMessage.setText(msg); mFragmentContainer.setVisibility(View.GONE); } public void showInstallProgressDialog(@Nullable View.OnClickListener backgroundButtonListener) { // Disable installer options mTitleBuilder.setEndIcon(null, null); // Buttons mNeutralBtn.setVisibility(View.GONE); if (backgroundButtonListener != null) { mPositiveBtn.setVisibility(View.VISIBLE); mPositiveBtn.setText(R.string.background); mPositiveBtn.setOnClickListener(backgroundButtonListener); } else { mPositiveBtn.setVisibility(View.GONE); } mNegativeBtn.setVisibility(View.GONE); // Body mLayout.setVisibility(View.VISIBLE); View v = View.inflate(mContext, R.layout.dialog_progress2, null); TextView tv = v.findViewById(android.R.id.text1); tv.setText(R.string.install_in_progress); mLayout.removeAllViews(); mLayout.addView(v); mMessage.setVisibility(View.GONE); mFragmentContainer.setVisibility(View.GONE); } public void showInstallFinishedDialog(CharSequence msg, @StringRes int cancelOrNextRes, @NonNull View.OnClickListener cancelClickListener, @Nullable View.OnClickListener openButtonClickListener, @Nullable View.OnClickListener appInfoButtonClickListener) { // Buttons if (appInfoButtonClickListener != null) { mNeutralBtn.setVisibility(View.VISIBLE); mNeutralBtn.setText(R.string.app_info); mNeutralBtn.setOnClickListener(appInfoButtonClickListener); } else mNeutralBtn.setVisibility(View.GONE); if (openButtonClickListener != null) { mPositiveBtn.setVisibility(View.VISIBLE); mPositiveBtn.setText(R.string.open); mPositiveBtn.setOnClickListener(openButtonClickListener); } else mPositiveBtn.setVisibility(View.GONE); mNegativeBtn.setVisibility(View.VISIBLE); mNegativeBtn.setText(cancelOrNextRes); mNegativeBtn.setOnClickListener(cancelClickListener); // Body mLayout.setVisibility(View.GONE); mMessage.setVisibility(View.VISIBLE); mMessage.setText(msg); mFragmentContainer.setVisibility(View.GONE); } public void dismiss() { mDialog.dismiss(); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/InstallerOptions.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk.installer; import android.annotation.UserIdInt; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import android.os.UserHandleHidden; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.os.ParcelCompat; import org.json.JSONException; import org.json.JSONObject; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.history.IJsonSerializer; import io.github.muntashirakon.AppManager.history.JsonDeserializer; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.utils.JSONUtils; public class InstallerOptions implements Parcelable, IJsonSerializer { @NonNull public static InstallerOptions getDefault() { return new InstallerOptions(); } @UserIdInt private int mUserId; private int mInstallLocation; @Nullable private String mInstallerName; @Nullable private String mOriginatingPackage; @Nullable private Uri mOriginatingUri; private boolean mSetOriginatingPackage; private int mPackageSource; private int mInstallScenario; private boolean mRequestUpdateOwnership; private boolean mDisableApkVerification; private boolean mSignApkFiles; private boolean mForceDexOpt; private boolean mBlockTrackers; private InstallerOptions() { mUserId = UserHandleHidden.myUserId(); mInstallLocation = Prefs.Installer.getInstallLocation(); mInstallerName = Prefs.Installer.getInstallerPackageName(); mOriginatingPackage = null; mOriginatingUri = null; mSetOriginatingPackage = Prefs.Installer.isSetOriginatingPackage(); mPackageSource = Prefs.Installer.getPackageSource(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // If the user is always installing apps in the background, we expect that the user does // want to install an app quite fast. mInstallScenario = Prefs.Installer.installInBackground() ? PackageManager.INSTALL_SCENARIO_BULK : PackageManager.INSTALL_SCENARIO_FAST; } mRequestUpdateOwnership = Prefs.Installer.requestUpdateOwnership(); mDisableApkVerification = Prefs.Installer.isDisableApkVerification(); mSignApkFiles = Prefs.Installer.canSignApk(); mForceDexOpt = Prefs.Installer.forceDexOpt(); mBlockTrackers = Prefs.Installer.blockTrackers(); } protected InstallerOptions(@NonNull Parcel in) { mUserId = in.readInt(); mInstallLocation = in.readInt(); mInstallerName = in.readString(); mOriginatingPackage = in.readString(); mOriginatingUri = ParcelCompat.readParcelable(in, Uri.class.getClassLoader(), Uri.class); mSetOriginatingPackage = ParcelCompat.readBoolean(in); mPackageSource = in.readInt(); mInstallScenario = in.readInt(); mRequestUpdateOwnership = ParcelCompat.readBoolean(in); mDisableApkVerification = ParcelCompat.readBoolean(in); mSignApkFiles = ParcelCompat.readBoolean(in); mForceDexOpt = ParcelCompat.readBoolean(in); mBlockTrackers = ParcelCompat.readBoolean(in); } public void copy(@NonNull InstallerOptions options) { mUserId = options.mUserId; mInstallLocation = options.mInstallLocation; mInstallerName = options.mInstallerName; mOriginatingPackage = options.mOriginatingPackage; mOriginatingUri = options.mOriginatingUri; mSetOriginatingPackage = options.mSetOriginatingPackage; mPackageSource = options.mPackageSource; mInstallScenario = options.mInstallScenario; mRequestUpdateOwnership = options.mRequestUpdateOwnership; mDisableApkVerification = options.mDisableApkVerification; mSignApkFiles = options.mSignApkFiles; mForceDexOpt = options.mForceDexOpt; mBlockTrackers = options.mBlockTrackers; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeInt(mUserId); dest.writeInt(mInstallLocation); dest.writeString(mInstallerName); dest.writeString(mOriginatingPackage); dest.writeParcelable(mOriginatingUri, flags); ParcelCompat.writeBoolean(dest, mSetOriginatingPackage); dest.writeInt(mPackageSource); dest.writeInt(mInstallScenario); ParcelCompat.writeBoolean(dest, mRequestUpdateOwnership); ParcelCompat.writeBoolean(dest, mDisableApkVerification); ParcelCompat.writeBoolean(dest, mSignApkFiles); ParcelCompat.writeBoolean(dest, mForceDexOpt); ParcelCompat.writeBoolean(dest, mBlockTrackers); } protected InstallerOptions(@NonNull JSONObject jsonObject) throws JSONException { mUserId = jsonObject.getInt("user_id"); mInstallLocation = jsonObject.getInt("install_location"); mInstallerName = JSONUtils.optString(jsonObject, "installer_name", null); mOriginatingPackage = JSONUtils.optString(jsonObject, "originating_package"); String originatingUri = JSONUtils.optString(jsonObject, "originating_uri", null); mOriginatingUri = originatingUri != null ? Uri.parse(originatingUri) : null; mSetOriginatingPackage = jsonObject.optBoolean("set_originating_package", Prefs.Installer.isSetOriginatingPackage()); mPackageSource = jsonObject.getInt("package_source"); mInstallScenario = jsonObject.getInt("install_scenario"); mRequestUpdateOwnership = jsonObject.getBoolean("request_update_ownership"); mDisableApkVerification = jsonObject.getBoolean("disable_apk_verification"); mSignApkFiles = jsonObject.getBoolean("sign_apk_files"); mForceDexOpt = jsonObject.getBoolean("force_dex_opt"); mBlockTrackers = jsonObject.getBoolean("block_trackers"); } @NonNull @Override public JSONObject serializeToJson() throws JSONException { JSONObject jsonObject = new JSONObject(); jsonObject.put("user_id", mUserId); jsonObject.put("install_location", mInstallLocation); jsonObject.put("installer_name", mInstallerName); jsonObject.put("originating_package", mOriginatingPackage); jsonObject.put("originating_uri", mOriginatingUri != null ? mOriginatingUri.toString() : null); jsonObject.put("set_originating_package", mSetOriginatingPackage); jsonObject.put("package_source", mPackageSource); jsonObject.put("install_scenario", mInstallScenario); jsonObject.put("request_update_ownership", mRequestUpdateOwnership); jsonObject.put("disable_apk_verification", mDisableApkVerification); jsonObject.put("sign_apk_files", mSignApkFiles); jsonObject.put("force_dex_opt", mForceDexOpt); jsonObject.put("block_trackers", mBlockTrackers); return jsonObject; } public static final JsonDeserializer.Creator DESERIALIZER = InstallerOptions::new; @Override public int describeContents() { return 0; } public static final Creator CREATOR = new Creator() { @Override public InstallerOptions createFromParcel(@NonNull Parcel in) { return new InstallerOptions(in); } @Override @NonNull public InstallerOptions[] newArray(int size) { return new InstallerOptions[size]; } }; @UserIdInt public int getUserId() { return mUserId; } public void setUserId(@UserIdInt int userId) { mUserId = userId; } public int getInstallLocation() { return mInstallLocation; } public void setInstallLocation(int installLocation) { mInstallLocation = installLocation; } @NonNull public String getInstallerName() { return !TextUtils.isEmpty(mInstallerName) ? mInstallerName : BuildConfig.APPLICATION_ID; } public void setInstallerName(@Nullable String installerName) { mInstallerName = installerName; } @Nullable public String getOriginatingPackage() { return mOriginatingPackage; } public void setOriginatingPackage(@Nullable String originatingPackage) { mOriginatingPackage = originatingPackage; } @Nullable public Uri getOriginatingUri() { return mOriginatingUri; } public void setOriginatingUri(@Nullable Uri originatingUri) { mOriginatingUri = originatingUri; } public boolean isSetOriginatingPackage() { return mSetOriginatingPackage; } public void setSetOriginatingPackage(boolean setOriginatingPackage) { mSetOriginatingPackage = setOriginatingPackage; } public int getPackageSource() { return mPackageSource; } public void setPackageSource(int packageSource) { mPackageSource = packageSource; } public int getInstallScenario() { return mInstallScenario; } public void setInstallScenario(int installScenario) { mInstallScenario = installScenario; } public boolean requestUpdateOwnership() { return mRequestUpdateOwnership; } public void requestUpdateOwnership(boolean update) { mRequestUpdateOwnership = update; } public boolean isDisableApkVerification() { return mDisableApkVerification; } public void setDisableApkVerification(boolean disableApkVerification) { mDisableApkVerification = disableApkVerification; } public boolean isSignApkFiles() { return mSignApkFiles; } public void setSignApkFiles(boolean signApkFiles) { mSignApkFiles = signApkFiles; } public boolean isForceDexOpt() { return mForceDexOpt; } public void setForceDexOpt(boolean forceDexOpt) { mForceDexOpt = forceDexOpt; } public boolean isBlockTrackers() { return mBlockTrackers; } public void setBlockTrackers(boolean blockTrackers) { mBlockTrackers = blockTrackers; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/InstallerOptionsFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk.installer; import static io.github.muntashirakon.AppManager.settings.InstallerPreferences.INSTALL_LOCATIONS; import static io.github.muntashirakon.AppManager.settings.InstallerPreferences.INSTALL_LOCATION_NAMES; import static io.github.muntashirakon.AppManager.settings.InstallerPreferences.PKG_SOURCES; import static io.github.muntashirakon.AppManager.settings.InstallerPreferences.PKG_SOURCES_NAMES; import static io.github.muntashirakon.AppManager.utils.UIUtils.getSecondaryText; import static io.github.muntashirakon.AppManager.utils.UIUtils.getSmallerText; import android.Manifest; import android.app.Application; import android.app.Dialog; import android.content.DialogInterface; import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; import android.os.UserHandleHidden; import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.EditText; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.os.BundleCompat; import androidx.core.util.Pair; import androidx.fragment.app.DialogFragment; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModelProvider; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.materialswitch.MaterialSwitch; import com.google.android.material.textfield.TextInputLayout; import java.text.Collator; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.compat.ManifestCompat; import io.github.muntashirakon.AppManager.db.entity.App; import io.github.muntashirakon.AppManager.db.utils.AppDb; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.users.UserInfo; import io.github.muntashirakon.AppManager.users.Users; import io.github.muntashirakon.AppManager.utils.PackageUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.adapters.SelectedArrayAdapter; import io.github.muntashirakon.dialog.SearchableSingleChoiceDialogBuilder; import io.github.muntashirakon.dialog.TextInputDialogBuilder; import io.github.muntashirakon.lifecycle.SingleLiveEvent; import io.github.muntashirakon.view.TextInputLayoutCompat; import io.github.muntashirakon.widget.MaterialSpinner; public class InstallerOptionsFragment extends DialogFragment { public static final String TAG = InstallerOptionsFragment.class.getSimpleName(); private static final String ARG_PACKAGE_NAME = "pkg"; private static final String ARG_TEST_ONLY_APP = "test_only"; private static final String ARG_REF_INSTALLER_OPTIONS = "ref_opt"; public interface OnClickListener { void onClick(DialogInterface dialog, int which, @Nullable InstallerOptions options); } @NonNull public static InstallerOptionsFragment getInstance(@Nullable String packageName, @Nullable Boolean isTestOnly, @NonNull InstallerOptions options, @Nullable OnClickListener clickListener) { InstallerOptionsFragment dialog = new InstallerOptionsFragment(); Bundle args = new Bundle(); args.putString(ARG_PACKAGE_NAME, packageName); if (isTestOnly != null) { args.putBoolean(ARG_TEST_ONLY_APP, isTestOnly); } args.putParcelable(ARG_REF_INSTALLER_OPTIONS, options); dialog.setArguments(args); dialog.setOnClickListener(clickListener); return dialog; } private InstallerOptionsViewModel mModel; private View mDialogView; private MaterialSpinner mUserSelectionSpinner; private MaterialSpinner mInstallLocationSpinner; private MaterialSpinner mPackageSourceSpinner; private TextInputLayout mInstallerAppLayout; private EditText mInstallerAppField; private MaterialSwitch mBlockTrackersSwitch; @Nullable private OnClickListener mClickListener; private String mPackageName; private boolean mIsTestOnly; private InstallerOptions mOptions; private PackageManager mPm; public void setOnClickListener(@Nullable OnClickListener clickListener) { this.mClickListener = clickListener; } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); mModel = new ViewModelProvider(this).get(InstallerOptionsViewModel.class); } @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { mPackageName = requireArguments().getString(ARG_PACKAGE_NAME); mIsTestOnly = requireArguments().getBoolean(ARG_TEST_ONLY_APP, true); mOptions = Objects.requireNonNull(BundleCompat.getParcelable(requireArguments(), ARG_REF_INSTALLER_OPTIONS, InstallerOptions.class)); mDialogView = View.inflate(requireActivity(), R.layout.dialog_installer_options, null); mUserSelectionSpinner = mDialogView.findViewById(R.id.user); mInstallLocationSpinner = mDialogView.findViewById(R.id.install_location); mPackageSourceSpinner = mDialogView.findViewById(R.id.package_source); mInstallerAppLayout = mDialogView.findViewById(R.id.installer); mInstallerAppField = Objects.requireNonNull(mInstallerAppLayout.getEditText()); MaterialSwitch disableVerificationSwitch = mDialogView.findViewById(R.id.action_disable_verification); MaterialSwitch setOriginSwitch = mDialogView.findViewById(R.id.action_set_origin); MaterialSwitch reqUpdateOwnershipSwitch = mDialogView.findViewById(R.id.action_update_ownership); MaterialSwitch signApkSwitch = mDialogView.findViewById(R.id.action_sign_apk); MaterialSwitch forceDexOptSwitch = mDialogView.findViewById(R.id.action_optimize); mBlockTrackersSwitch = mDialogView.findViewById(R.id.action_block_trackers); // Set values and defaults mPm = requireContext().getPackageManager(); boolean canInstallForOtherUsers = SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.INTERACT_ACROSS_USERS_FULL); int selectedUser = getSelectedUserId(canInstallForOtherUsers); boolean canBlockTrackers = SelfPermissions.canModifyAppComponentStates(selectedUser, mPackageName, mIsTestOnly); initUserSpinner(canInstallForOtherUsers); initInstallLocationSpinner(); initPackageSourceSpinner(); initInstallerAppSpinner(); disableVerificationSwitch.setEnabled(SelfPermissions.isSystemOrRootOrShell()); disableVerificationSwitch.setChecked(mOptions.isDisableApkVerification()); disableVerificationSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> mOptions.setDisableApkVerification(isChecked)); setOriginSwitch.setChecked(mOptions.isSetOriginatingPackage()); setOriginSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> mOptions.setSetOriginatingPackage(isChecked)); reqUpdateOwnershipSwitch.setVisibility(Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE ? View.VISIBLE : View.GONE); reqUpdateOwnershipSwitch.setChecked(mOptions.requestUpdateOwnership()); reqUpdateOwnershipSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> mOptions.requestUpdateOwnership(isChecked)); signApkSwitch.setChecked(mOptions.isSignApkFiles()); signApkSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> mOptions.setSignApkFiles(isChecked)); forceDexOptSwitch.setVisibility(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ? View.VISIBLE : View.GONE); forceDexOptSwitch.setChecked(mOptions.isForceDexOpt()); forceDexOptSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> mOptions.setForceDexOpt(isChecked)); mBlockTrackersSwitch.setChecked(canBlockTrackers && mOptions.isBlockTrackers()); mBlockTrackersSwitch.setEnabled(canBlockTrackers); mBlockTrackersSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> mOptions.setBlockTrackers(isChecked)); return new MaterialAlertDialogBuilder(requireActivity()) .setTitle(R.string.installer_options) .setView(mDialogView) .setCancelable(false) .setPositiveButton(R.string.ok, (dialog, which) -> { if (mClickListener != null) { mClickListener.onClick(dialog, which, mOptions); } }) .setNegativeButton(R.string.cancel, (dialog, which) -> { if (mClickListener != null) { mClickListener.onClick(dialog, which, null); } }) .create(); } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return mDialogView; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); mModel.getPackageNameLabelPairLiveData().observe(getViewLifecycleOwner(), this::displayInstallerAppSelectionDialog); } private int getSelectedUserId(boolean canInstallForOtherUsers) { return canInstallForOtherUsers ? mOptions.getUserId() : UserHandleHidden.myUserId(); } private void initUserSpinner(boolean canInstallForOtherUsers) { int selectedUser = getSelectedUserId(canInstallForOtherUsers); List userInfoList = Users.getUsers(); CharSequence[] userNames = new String[userInfoList.size() + 1]; Integer[] userIds = new Integer[userInfoList.size() + 1]; userNames[0] = getString(R.string.backup_all_users); userIds[0] = UserHandleHidden.USER_ALL; int i = 1; int selectedUserPosition = 0; for (UserInfo info : userInfoList) { userNames[i] = info.toLocalizedString(requireContext()); userIds[i] = info.id; if (selectedUser == info.id) { selectedUserPosition = i; } ++i; } ArrayAdapter userAdapter = new SelectedArrayAdapter<>(requireContext(), io.github.muntashirakon.ui.R.layout.auto_complete_dropdown_item_small, userNames); mUserSelectionSpinner.setAdapter(userAdapter); mUserSelectionSpinner.setSelection(selectedUserPosition); mUserSelectionSpinner.setOnItemClickListener((parent, view, position, id) -> { mOptions.setUserId(userIds[position]); // Update block trackers option boolean canBlockTrackers = SelfPermissions.canModifyAppComponentStates(selectedUser, mPackageName, mIsTestOnly); mBlockTrackersSwitch.setChecked(canBlockTrackers && mOptions.isBlockTrackers()); mBlockTrackersSwitch.setEnabled(canBlockTrackers); }); mUserSelectionSpinner.setEnabled(canInstallForOtherUsers); } private void initInstallLocationSpinner() { int installLocation = mOptions.getInstallLocation(); int installLocationPosition = installLocation; CharSequence[] installLocationNames = new CharSequence[INSTALL_LOCATIONS.length]; for (int i = 0; i < INSTALL_LOCATIONS.length; ++i) { installLocationNames[i] = getString(INSTALL_LOCATION_NAMES[i]); if (INSTALL_LOCATIONS[i] == installLocation) { installLocationPosition = i; } } ArrayAdapter installerLocationAdapter = new SelectedArrayAdapter<>(requireContext(), io.github.muntashirakon.ui.R.layout.auto_complete_dropdown_item_small, installLocationNames); mInstallLocationSpinner.setAdapter(installerLocationAdapter); mInstallLocationSpinner.setSelection(installLocationPosition); mInstallLocationSpinner.setOnItemClickListener((parent, view, position, id) -> mOptions.setInstallLocation(INSTALL_LOCATIONS[position])); } private void initPackageSourceSpinner() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { mPackageSourceSpinner.setVisibility(View.GONE); return; } int pkgSource = mOptions.getPackageSource(); CharSequence[] pkgSourceTexts = new CharSequence[PKG_SOURCES_NAMES.length]; for (int i = 0; i < PKG_SOURCES_NAMES.length; ++i) { pkgSourceTexts[i] = getString(PKG_SOURCES_NAMES[i]); } ArrayAdapter pkgSourceAdapter = new SelectedArrayAdapter<>(requireContext(), io.github.muntashirakon.ui.R.layout.auto_complete_dropdown_item_small, pkgSourceTexts); mPackageSourceSpinner.setAdapter(pkgSourceAdapter); mPackageSourceSpinner.setSelection(pkgSource); mPackageSourceSpinner.setOnItemClickListener((parent, view, position, id) -> mOptions.setInstallLocation(PKG_SOURCES[position])); } private void initInstallerAppSpinner() { boolean canInstallApps = SelfPermissions.checkSelfOrRemotePermission(Manifest.permission.INSTALL_PACKAGES); String installer = canInstallApps ? mOptions.getInstallerName() : BuildConfig.APPLICATION_ID; mInstallerAppField.setText(PackageUtils.getPackageLabel(mPm, installer)); TextInputLayoutCompat.fixEndIcon(mInstallerAppLayout); mInstallerAppLayout.setEnabled(canInstallApps); mInstallerAppLayout.setEndIconOnClickListener(view -> new MaterialAlertDialogBuilder(requireActivity()) .setTitle(R.string.installer_app) .setMessage(R.string.installer_app_message) .setPositiveButton(R.string.choose, (dialog1, which1) -> mModel.loadPackageNameLabelPair()) .setNegativeButton(R.string.specify_custom_name, (dialog, which) -> new TextInputDialogBuilder(requireActivity(), R.string.installer_app) .setTitle(R.string.installer_app) .setInputText(mOptions.getInstallerName()) .setPositiveButton(R.string.ok, (dialog1, which1, inputText, isChecked) -> { if (inputText == null) return; String installerApp = inputText.toString().trim(); if (!TextUtils.isEmpty(installerApp)) { mOptions.setInstallerName(installerApp); mInstallerAppField.setText(PackageUtils.getPackageLabel(mPm, installerApp)); } }) .setNegativeButton(R.string.cancel, null) .show()) .setNeutralButton(R.string.reset_to_default, (dialog, which) -> { String installerApp = Prefs.Installer.getInstallerPackageName(); mOptions.setInstallerName(installerApp); mInstallerAppField.setText(PackageUtils.getPackageLabel(mPm, installerApp)); }) .show()); } public void displayInstallerAppSelectionDialog(@NonNull List> appInfo) { ArrayList items = new ArrayList<>(appInfo.size()); ArrayList itemNames = new ArrayList<>(appInfo.size()); for (Pair pair : appInfo) { items.add(pair.first); itemNames.add(new SpannableStringBuilder(pair.second) .append("\n") .append(getSecondaryText(requireContext(), getSmallerText(pair.first)))); } new SearchableSingleChoiceDialogBuilder<>(requireActivity(), items, itemNames) .setTitle(R.string.installer_app) .setSelection(mOptions.getInstallerName()) .setPositiveButton(R.string.save, (dialog, which, selectedInstallerApp) -> { if (selectedInstallerApp != null) { String installerApp = selectedInstallerApp.trim(); if (!TextUtils.isEmpty(installerApp)) { mOptions.setInstallerName(installerApp); mInstallerAppField.setText(PackageUtils.getPackageLabel(mPm, installerApp)); } } }) .setNegativeButton(R.string.cancel, null) .show(); } public static class InstallerOptionsViewModel extends AndroidViewModel { private final MutableLiveData>> mPackageNameLabelPairLiveData = new SingleLiveEvent<>(); public InstallerOptionsViewModel(@NonNull Application application) { super(application); } public LiveData>> getPackageNameLabelPairLiveData() { return mPackageNameLabelPairLiveData; } public void loadPackageNameLabelPair() { ThreadUtils.postOnBackgroundThread(() -> { List appList = new AppDb().getAllApplications(); Map packageNameLabelMap = new HashMap<>(appList.size()); for (App app : appList) { packageNameLabelMap.put(app.packageName, app.packageLabel); } List> appInfo = new ArrayList<>(); for (String packageName : packageNameLabelMap.keySet()) { appInfo.add(new Pair<>(packageName, packageNameLabelMap.get(packageName))); } Collator collator = Collator.getInstance(); Collections.sort(appInfo, (o1, o2) -> collator.compare(o1.second.toString(), o2.second.toString())); mPackageNameLabelPairLiveData.postValue(appInfo); }); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/PackageInstallerActivity.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk.installer; import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.STATUS_FAILURE_ABORTED; import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.STATUS_FAILURE_BLOCKED; import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.STATUS_FAILURE_CONFLICT; import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.STATUS_FAILURE_INCOMPATIBLE; import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.STATUS_FAILURE_INCOMPATIBLE_ROM; import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.STATUS_FAILURE_INVALID; import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.STATUS_FAILURE_SECURITY; import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.STATUS_FAILURE_SESSION_ABANDON; import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.STATUS_FAILURE_SESSION_COMMIT; import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.STATUS_FAILURE_SESSION_CREATE; import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.STATUS_FAILURE_SESSION_WRITE; import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.STATUS_FAILURE_STORAGE; import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.STATUS_SUCCESS; import android.Manifest; import android.annotation.UserIdInt; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageInstaller; import android.content.res.Resources; import android.net.Uri; import android.os.Bundle; import android.os.IBinder; import android.os.UserHandleHidden; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.style.RelativeSizeSpan; import android.view.View; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.appcompat.app.AlertDialog; import androidx.core.content.ContextCompat; import androidx.core.content.pm.PackageInfoCompat; import androidx.lifecycle.ViewModelProvider; import java.util.LinkedList; import java.util.Objects; import java.util.Queue; import io.github.muntashirakon.AppManager.BaseActivity; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.accessibility.AccessibilityMultiplexer; import io.github.muntashirakon.AppManager.apk.ApkSource; import io.github.muntashirakon.AppManager.apk.CachedApkSource; import io.github.muntashirakon.AppManager.apk.splitapk.SplitApkChooser; import io.github.muntashirakon.AppManager.apk.whatsnew.WhatsNewFragment; import io.github.muntashirakon.AppManager.compat.ApplicationInfoCompat; import io.github.muntashirakon.AppManager.compat.PackageManagerCompat; import io.github.muntashirakon.AppManager.details.AppDetailsActivity; import io.github.muntashirakon.AppManager.intercept.IntentCompat; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.types.ForegroundService; import io.github.muntashirakon.AppManager.utils.PackageUtils; import io.github.muntashirakon.AppManager.utils.StoragePermission; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.AppManager.utils.Utils; /** * Activity that manages installing and confirming package installation. Actual installation is done by * {@link PackageInstallerService}. *

* How the installer works: *

    *
  1. When the installation of a package is requested, it is either stored in queue or loaded directly if the queue is * empty. *
  2. Then, it is checked whether there's an already installed package by the same name. If there exists any, the user * is offered to reinstall, upgrade or downgrade the package depending on the features supported by the present mode of * operation. Otherwise, the user is asked to confirm installation. Before doing so, however, a changelog may be * listed if it is enabled in settings. *
  3. Next, if it is a split app, the user is asked to choose the splits to be installed. Otherwise, the installer * proceeds to the next phase directly. *
  4. If display options is enabled, the options are displayed so that the user can tweak the present installer. * Otherwise, the installed proceeds to the next phase. *
  5. Installer takes necessary steps to launch a installer service to initiate the installation. *
*/ public class PackageInstallerActivity extends BaseActivity implements InstallerDialogHelper.OnClickButtonsListener { public static final String TAG = PackageInstallerActivity.class.getSimpleName(); @NonNull public static Intent getLaunchableInstance(@NonNull Context context, @NonNull Uri uri) { Intent intent = new Intent(context, PackageInstallerActivity.class); intent.setData(uri); return intent; } @NonNull public static Intent getLaunchableInstance(@NonNull Context context, ApkSource apkSource) { Intent intent = new Intent(context, PackageInstallerActivity.class); IntentCompat.putWrappedParcelableExtra(intent, EXTRA_APK_FILE_LINK, apkSource); return intent; } @NonNull public static Intent getLaunchableInstance(@NonNull Context context, @NonNull String packageName) { Intent intent = new Intent(context, PackageInstallerActivity.class); intent.setData(Uri.parse("package:" + packageName)); return intent; } private static final String EXTRA_APK_FILE_LINK = "link"; public static final String ACTION_PACKAGE_INSTALLED = BuildConfig.APPLICATION_ID + ".action.PACKAGE_INSTALLED"; private int mSessionId = -1; @Nullable private ApkQueueItem mCurrentItem; private String mPackageName; /** * Whether this activity is currently dealing with an apk */ private boolean mIsDealingWithApk = false; @UserIdInt private int mLastUserId; private InstallerDialogHelper mDialogHelper; private PackageInstallerViewModel mModel; @Nullable private PackageInstallerService mService; private InstallerDialogFragment mInstallerDialogFragment; private boolean initiated = false; private final View.OnClickListener mAppInfoClickListener = v -> { assert mCurrentItem != null; try { ApkSource apkSource = mCurrentItem.getApkSource(); if (apkSource == null) { apkSource = mModel.getApkSource(); } Intent appDetailsIntent = AppDetailsActivity.getIntent(this, apkSource, true); appDetailsIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(appDetailsIntent); } finally { // We cannot trigger cancel here because the cached file will be deleted goToNext(); } }; private final InstallerOptions mInstallerOptions = InstallerOptions.getDefault(); private final Queue mApkQueue = new LinkedList<>(); private final ActivityResultLauncher mConfirmIntentLauncher = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { // User did some interaction and the installer screen is closed now Intent broadcastIntent = new Intent(PackageInstallerCompat.ACTION_INSTALL_INTERACTION_END); broadcastIntent.setPackage(getPackageName()); broadcastIntent.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, mPackageName); broadcastIntent.putExtra(PackageInstaller.EXTRA_SESSION_ID, mSessionId); getApplicationContext().sendBroadcast(broadcastIntent); if (!hasNext() && !mIsDealingWithApk) { // No APKs left, this maybe a solo call finish(); } // else let the original activity decide what to do }); private final AccessibilityMultiplexer mMultiplexer = AccessibilityMultiplexer.getInstance(); private final StoragePermission mStoragePermission = StoragePermission.init(this); private final ServiceConnection mServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { mService = ((ForegroundService.Binder) service).getService(); } @Override public void onServiceDisconnected(ComponentName name) { mService = null; } }; @Override public boolean getTransparentBackground() { return true; } @Override protected void onAuthenticated(@Nullable Bundle savedInstanceState) { final Intent intent = getIntent(); if (intent == null) { triggerCancel(); return; } Log.d(TAG, "On create, intent: %s", intent); if (ACTION_PACKAGE_INSTALLED.equals(intent.getAction())) { onNewIntent(intent); return; } mModel = new ViewModelProvider(this).get(PackageInstallerViewModel.class); if (!bindService( new Intent(this, PackageInstallerService.class), mServiceConnection, BIND_AUTO_CREATE)) { throw new RuntimeException("Unable to bind PackageInstallerService"); } synchronized (mApkQueue) { mApkQueue.addAll(ApkQueueItem.fromIntent(intent, Utils.getRealReferrer(this))); } ApkSource apkSource = IntentCompat.getUnwrappedParcelableExtra(intent, EXTRA_APK_FILE_LINK, ApkSource.class); if (apkSource != null) { synchronized (mApkQueue) { mApkQueue.add(ApkQueueItem.fromApkSource(apkSource)); } } mModel.packageInfoLiveData().observe(this, newPackageInfo -> { if (newPackageInfo == null) { mDialogHelper.showParseFailedDialog(v -> triggerCancel()); return; } // TODO: Resolve dependencies mDialogHelper.onParseSuccess(mModel.getAppLabel(), getVersionInfoWithTrackers(newPackageInfo), mModel.getAppIcon(), v -> displayInstallerOptions((dialog1, which, options) -> { if (options != null) { mInstallerOptions.copy(options); } })); displayChangesOrInstallationPrompt(); }); mModel.packageUninstalledLiveData().observe(this, success -> { if (success) { install(); } else { showInstallationFinishedDialog(mModel.getPackageName(), getString(R.string.failed_to_uninstall_app), null, false); } }); // Init fragment mInstallerDialogFragment = new InstallerDialogFragment(); mInstallerDialogFragment.setCancelable(false); mInstallerDialogFragment.setFragmentStartedCallback(this::init); mInstallerDialogFragment.showNow(getSupportFragmentManager(), InstallerDialogFragment.TAG); } @Override protected void onDestroy() { if (mService != null) { unbindService(mServiceConnection); } unsetInstallFinishedListener(); // Delete remaining cached file if (mCurrentItem != null && (mCurrentItem.getApkSource() instanceof CachedApkSource)) { ((CachedApkSource) mCurrentItem.getApkSource()).cleanup(); } super.onDestroy(); } private void init(@NonNull InstallerDialogFragment fragment, @NonNull AlertDialog dialog) { // Make sure that it's only initiated once if (initiated) { return; } initiated = true; mDialogHelper = new InstallerDialogHelper(fragment, dialog); mDialogHelper.initProgress(v -> triggerCancel()); goToNext(); } @UiThread private void displayChangesOrInstallationPrompt() { // This dialog either calls triggerInstall() or triggerCancel() boolean displayChanges; PackageInfo installedPackageInfo = mModel.getInstalledPackageInfo(); int actionRes; if (installedPackageInfo == null) { // App not installed or data not cleared displayChanges = false; actionRes = R.string.install; } else { // App is installed or the app is uninstalled without clearing data, or the app is uninstalled, // but it's a system app long installedVersionCode = PackageInfoCompat.getLongVersionCode(installedPackageInfo); long thisVersionCode = PackageInfoCompat.getLongVersionCode(mModel.getNewPackageInfo()); displayChanges = Prefs.Installer.displayChanges(); if (installedVersionCode < thisVersionCode) { // Needs update actionRes = R.string.update; } else if (installedVersionCode == thisVersionCode) { // Issue reinstall actionRes = R.string.reinstall; } else { // Downgrade actionRes = R.string.downgrade; } } if (displayChanges) { WhatsNewFragment dialogFragment = WhatsNewFragment.getInstance(mModel.getNewPackageInfo(), mModel.getInstalledPackageInfo()); mDialogHelper.showWhatsNewDialog(actionRes, dialogFragment, new InstallerDialogHelper.OnClickButtonsListener() { @Override public void triggerInstall() { displayInstallationPrompt(actionRes, true); } @Override public void triggerCancel() { PackageInstallerActivity.this.triggerCancel(); } }, mAppInfoClickListener); return; } displayInstallationPrompt(actionRes, false); } private void displayInstallationPrompt(int actionRes, boolean splitOnly) { if (mModel.getApkFile().isSplit()) { SplitApkChooser fragment = SplitApkChooser.getNewInstance(getVersionInfoWithTrackers( mModel.getNewPackageInfo()), getString(actionRes)); mDialogHelper.showApkChooserDialog(actionRes, fragment, this, mAppInfoClickListener); return; } if (!splitOnly) { // In unprivileged mode, a dialog is generated by the system. But we need to display it nonetheless in order // to provide additional features. mDialogHelper.showInstallConfirmationDialog(actionRes, this, mAppInfoClickListener); } else triggerInstall(); } private void displayInstallerOptions(InstallerOptionsFragment.OnClickListener clickListener) { PackageInfo packageInfo = mModel.getNewPackageInfo(); InstallerOptionsFragment dialog = InstallerOptionsFragment.getInstance(packageInfo.packageName, ApplicationInfoCompat.isTestOnly(packageInfo.applicationInfo), mInstallerOptions, clickListener); dialog.show(getSupportFragmentManager(), InstallerOptionsFragment.TAG); } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { outState.clear(); super.onSaveInstanceState(outState); } @UiThread private void install() { if (mModel.getApkFile().hasObb() && !SelfPermissions.checkSelfOrRemotePermission(Manifest.permission.INSTALL_PACKAGES)) { // Need to request permissions if not given mStoragePermission.request(granted -> { if (granted) launchInstallerService(); }); } else launchInstallerService(); } @UiThread private void launchInstallerService() { assert mCurrentItem != null; int userId = mInstallerOptions.getUserId(); mCurrentItem.setInstallerOptions(mInstallerOptions); mCurrentItem.setSelectedSplits(mModel.getSelectedSplitsForInstallation()); mLastUserId = userId == UserHandleHidden.USER_ALL ? UserHandleHidden.myUserId() : userId; boolean canDisplayNotification = Utils.canDisplayNotification(this); boolean alwaysOnBackground = canDisplayNotification && Prefs.Installer.installInBackground(); Intent intent = new Intent(this, PackageInstallerService.class); IntentCompat.putWrappedParcelableExtra(intent, PackageInstallerService.EXTRA_QUEUE_ITEM, mCurrentItem); if (!SelfPermissions.checkSelfOrRemotePermission(Manifest.permission.INSTALL_PACKAGES)) { // For unprivileged mode, use accessibility service if enabled mMultiplexer.enableInstall(true); } ContextCompat.startForegroundService(this, intent); if (!alwaysOnBackground && mService != null) { setInstallFinishedListener(); mDialogHelper.showInstallProgressDialog(canDisplayNotification ? v -> { unsetInstallFinishedListener(); goToNext(); } : null); } else { unsetInstallFinishedListener(); // For some reason, the service is empty // Install next app instead goToNext(); } } @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); Log.d(TAG, "New intent called: %s", intent); setIntent(intent); // Check for action first if (ACTION_PACKAGE_INSTALLED.equals(intent.getAction())) { mSessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1); mPackageName = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME); Intent confirmIntent = IntentCompat.getParcelableExtra(intent, Intent.EXTRA_INTENT, Intent.class); try { if (mPackageName == null || confirmIntent == null) throw new Exception("Empty confirmation intent."); Log.d(TAG, "Requesting user confirmation for package %s", mPackageName); mConfirmIntentLauncher.launch(confirmIntent); } catch (Exception e) { e.printStackTrace(); PackageInstallerCompat.sendCompletedBroadcast(this, mPackageName, PackageInstallerCompat.STATUS_FAILURE_INCOMPATIBLE_ROM, mSessionId); if (!hasNext() && !mIsDealingWithApk) { // No APKs left, this maybe a solo call finish(); } // else let the original activity decide what to do } return; } // New APK files added synchronized (mApkQueue) { mApkQueue.addAll(ApkQueueItem.fromIntent(intent, Utils.getRealReferrer(this))); } UIUtils.displayShortToast(R.string.added_to_queue); } @UiThread @Override public void triggerInstall() { // Calls install(), reinstall() (which in terms called install()) and triggerCancel() if (mModel.getInstalledPackageInfo() == null) { // App not installed install(); return; } InstallerDialogHelper.OnClickButtonsListener reinstallListener = new InstallerDialogHelper.OnClickButtonsListener() { @Override public void triggerInstall() { // Uninstall and then install again reinstall(); } @Override public void triggerCancel() { PackageInstallerActivity.this.triggerCancel(); } }; long installedVersionCode = PackageInfoCompat.getLongVersionCode(mModel.getInstalledPackageInfo()); long thisVersionCode = PackageInfoCompat.getLongVersionCode(mModel.getNewPackageInfo()); if (installedVersionCode > thisVersionCode && !SelfPermissions.checkSelfOrRemotePermission(Manifest.permission.INSTALL_PACKAGES)) { // Need to uninstall and install again SpannableStringBuilder builder = new SpannableStringBuilder() .append(getString(R.string.do_you_want_to_uninstall_and_install)).append(" ") .append(UIUtils.getItalicString(getString(R.string.app_data_will_be_lost))) .append("\n\n"); mDialogHelper.showDowngradeReinstallWarning(builder, reinstallListener, mAppInfoClickListener); return; } if (!mModel.isSignatureDifferent()) { // Signature is either matched or the app isn't installed install(); return; } // Signature is different ApplicationInfo info = mModel.getInstalledPackageInfo().applicationInfo; // Installed package info is never null here. boolean isSystem = ApplicationInfoCompat.isSystemApp(info); SpannableStringBuilder builder = new SpannableStringBuilder(); if (isSystem) { // Cannot reinstall a system app with a different signature builder.append(getString(R.string.app_signing_signature_mismatch_for_system_apps)); } else { // Offer user to uninstall and then install the app again builder.append(getString(R.string.do_you_want_to_uninstall_and_install)).append(" ") .append(UIUtils.getItalicString(getString(R.string.app_data_will_be_lost))); } builder.append("\n\n"); int start = builder.length(); builder.append(getText(R.string.app_signing_install_without_data_loss)); builder.setSpan(new RelativeSizeSpan(0.8f), start, builder.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); mDialogHelper.showSignatureMismatchReinstallWarning(builder, reinstallListener, v -> install(), isSystem); } @Override public void triggerCancel() { // Run cleanup if (mCurrentItem != null && mCurrentItem.getApkSource() instanceof CachedApkSource) { ((CachedApkSource) mCurrentItem.getApkSource()).cleanup(); } goToNext(); } private void reinstall() { if (!SelfPermissions.checkSelfOrRemotePermission(Manifest.permission.DELETE_PACKAGES)) { mMultiplexer.enableUninstall(true); } mModel.uninstallPackage(); } /** * Closes the current APK and start the next */ private void goToNext() { mCurrentItem = null; mMultiplexer.enableInstall(false); mMultiplexer.enableUninstall(false); if (hasNext()) { mIsDealingWithApk = true; mDialogHelper.initProgress(v -> goToNext()); synchronized (mApkQueue) { mCurrentItem = Objects.requireNonNull(mApkQueue.poll()); mModel.getPackageInfo(mCurrentItem); } } else { mIsDealingWithApk = false; mDialogHelper.dismiss(); finish(); } } private boolean hasNext() { synchronized (mApkQueue) { return !mApkQueue.isEmpty(); } } @NonNull private String getVersionInfoWithTrackers(@NonNull final PackageInfo newPackageInfo) { Resources res = getApplication().getResources(); long newVersionCode = PackageInfoCompat.getLongVersionCode(newPackageInfo); String newVersionName = newPackageInfo.versionName; int trackers = mModel.getTrackerCount(); StringBuilder sb = new StringBuilder(res.getString(R.string.version_name_with_code, newVersionName, newVersionCode)); if (trackers > 0) { sb.append(", ").append(res.getQuantityString(R.plurals.no_of_trackers, trackers, trackers)); } return sb.toString(); } public void showInstallationFinishedDialog(String packageName, int result, @Nullable String blockingPackage, @Nullable String statusMessage) { showInstallationFinishedDialog(packageName, getStringFromStatus(result, blockingPackage), statusMessage, result == STATUS_SUCCESS); } public void showInstallationFinishedDialog(String packageName, CharSequence message, @Nullable String statusMessage, boolean displayOpenAndAppInfo) { SpannableStringBuilder ssb = new SpannableStringBuilder(message); if (statusMessage != null) { ssb.append("\n\n").append(UIUtils.getItalicString(statusMessage)); } Intent intent = PackageManagerCompat.getLaunchIntentForPackage(packageName, UserHandleHidden.myUserId()); mDialogHelper.showInstallFinishedDialog(ssb, hasNext() ? R.string.next : R.string.close, v -> goToNext(), displayOpenAndAppInfo && intent != null ? v -> { try { startActivity(intent); } catch (Throwable th) { UIUtils.displayLongToast(th.getMessage()); } finally { goToNext(); } } : null, displayOpenAndAppInfo ? v -> { try { Intent appDetailsIntent = AppDetailsActivity.getIntent(this, packageName, mLastUserId, true); appDetailsIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(appDetailsIntent); } finally { goToNext(); } } : null); } @NonNull private String getStringFromStatus(@PackageInstallerCompat.Status int status, @Nullable String blockingPackage) { switch (status) { case STATUS_SUCCESS: return getString(R.string.installer_app_installed); case STATUS_FAILURE_ABORTED: return getString(R.string.installer_error_aborted); case STATUS_FAILURE_BLOCKED: String blocker = getString(R.string.installer_error_blocked_device); if (blockingPackage != null) { blocker = PackageUtils.getPackageLabel(getPackageManager(), blockingPackage); } return getString(R.string.installer_error_blocked, blocker); case STATUS_FAILURE_CONFLICT: return getString(R.string.installer_error_conflict); case STATUS_FAILURE_INCOMPATIBLE: return getString(R.string.installer_error_incompatible); case STATUS_FAILURE_INVALID: return getString(R.string.installer_error_bad_apks); case STATUS_FAILURE_STORAGE: return getString(R.string.installer_error_storage); case STATUS_FAILURE_SECURITY: return getString(R.string.installer_error_security); case STATUS_FAILURE_SESSION_CREATE: return getString(R.string.installer_error_session_create); case STATUS_FAILURE_SESSION_WRITE: return getString(R.string.installer_error_session_write); case STATUS_FAILURE_SESSION_COMMIT: return getString(R.string.installer_error_session_commit); case STATUS_FAILURE_SESSION_ABANDON: return getString(R.string.installer_error_session_abandon); case STATUS_FAILURE_INCOMPATIBLE_ROM: return getString(R.string.installer_error_lidl_rom); } return getString(R.string.installer_error_generic); } public void setInstallFinishedListener() { if (mService != null) { mService.setOnInstallFinished((packageName, status, blockingPackage, statusMessage) -> { if (isFinishing()) return; showInstallationFinishedDialog(packageName, status, blockingPackage, statusMessage); }); } } public void unsetInstallFinishedListener() { if (mService != null) { mService.setOnInstallFinished(null); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/PackageInstallerBroadcastReceiver.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk.installer; import android.app.Notification; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInstaller; import androidx.annotation.NonNull; import androidx.core.app.PendingIntentCompat; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.intercept.IntentCompat; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.NotificationUtils; import io.github.muntashirakon.AppManager.utils.Utils; class PackageInstallerBroadcastReceiver extends BroadcastReceiver { public static final String TAG = PackageInstallerBroadcastReceiver.class.getSimpleName(); public static final String ACTION_PI_RECEIVER = BuildConfig.APPLICATION_ID + ".action.PI_RECEIVER"; private String mPackageName; private CharSequence mAppLabel; private int mConfirmNotificationId = 0; public void setPackageName(String packageName) { mPackageName = packageName; } public void setAppLabel(CharSequence appLabel) { mAppLabel = appLabel; } @Override public void onReceive(Context nullableContext, @NonNull Intent intent) { Context context = nullableContext != null ? nullableContext : ContextUtils.getContext(); int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1); int sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1); Log.d(TAG, "Session ID: %d", sessionId); switch (status) { case PackageInstaller.STATUS_PENDING_USER_ACTION: Log.d(TAG, "Requesting user confirmation..."); // Send broadcast first Intent broadcastIntent2 = new Intent(PackageInstallerCompat.ACTION_INSTALL_INTERACTION_BEGIN); broadcastIntent2.setPackage(context.getPackageName()); broadcastIntent2.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, mPackageName); broadcastIntent2.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId); context.sendBroadcast(broadcastIntent2); // Open confirmIntent using the PackageInstallerActivity. // If the confirmIntent isn't open via an activity, it will fail for large apk files Intent confirmIntent = IntentCompat.getParcelableExtra(intent, Intent.EXTRA_INTENT, Intent.class); Intent intent2 = new Intent(context, PackageInstallerActivity.class); intent2.setAction(PackageInstallerActivity.ACTION_PACKAGE_INSTALLED); intent2.putExtra(Intent.EXTRA_INTENT, confirmIntent); intent2.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, mPackageName); intent2.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId); intent2.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); boolean appInForeground = Utils.isAppInForeground(); if (appInForeground) { // Open activity directly and issue a silent notification context.startActivity(intent2); } // Delete intent: aborts the operation Intent broadcastCancel = new Intent(PackageInstallerCompat.ACTION_INSTALL_COMPLETED); broadcastCancel.setPackage(context.getPackageName()); broadcastCancel.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, mPackageName); broadcastCancel.putExtra(PackageInstaller.EXTRA_STATUS, PackageInstallerCompat.STATUS_FAILURE_ABORTED); broadcastCancel.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId); // Ask user for permission mConfirmNotificationId = NotificationUtils.displayInstallConfirmNotification(context, builder -> builder .setAutoCancel(false) .setSilent(appInForeground) .setDefaults(Notification.DEFAULT_ALL) .setWhen(System.currentTimeMillis()) .setSmallIcon(R.drawable.ic_default_notification) .setTicker(mAppLabel) .setContentTitle(mAppLabel) .setSubText(context.getString(R.string.package_installer)) // A neat way to find the title is to check for sessionId .setContentText(context.getString(sessionId == -1 ? R.string.confirm_uninstallation : R.string.confirm_installation)) .setContentIntent(PendingIntentCompat.getActivity(context, 0, intent2, PendingIntent.FLAG_UPDATE_CURRENT, false)) .setDeleteIntent(PendingIntentCompat.getBroadcast(context, 0, broadcastCancel, PendingIntent.FLAG_UPDATE_CURRENT, false)) .build()); break; case PackageInstaller.STATUS_SUCCESS: Log.d(TAG, "Install success!"); NotificationUtils.cancelInstallConfirmNotification(context, mConfirmNotificationId); PackageInstallerCompat.sendCompletedBroadcast(context, mPackageName, PackageInstallerCompat.STATUS_SUCCESS, sessionId); break; default: NotificationUtils.cancelInstallConfirmNotification(context, mConfirmNotificationId); Intent broadcastError = new Intent(PackageInstallerCompat.ACTION_INSTALL_COMPLETED); broadcastError.setPackage(context.getPackageName()); String statusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE); broadcastError.putExtra(PackageInstaller.EXTRA_STATUS_MESSAGE, statusMessage); broadcastError.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, mPackageName); broadcastError.putExtra(PackageInstaller.EXTRA_OTHER_PACKAGE_NAME, intent.getStringExtra(PackageInstaller.EXTRA_OTHER_PACKAGE_NAME)); broadcastError.putExtra(PackageInstaller.EXTRA_STATUS, status); broadcastError.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId); context.sendBroadcast(broadcastError); Log.d(TAG, "Install failed! %s", statusMessage); break; } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/PackageInstallerCompat.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk.installer; import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.MATCH_UNINSTALLED_PACKAGES; import android.Manifest; import android.annotation.SuppressLint; import android.annotation.UserIdInt; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.IIntentReceiver; import android.content.IIntentSender; import android.content.Intent; import android.content.IntentFilter; import android.content.IntentSender; import android.content.pm.ApplicationInfo; import android.content.pm.IPackageInstaller; import android.content.pm.IPackageInstallerSession; import android.content.pm.PackageInfo; import android.content.pm.PackageInstaller; import android.content.pm.PackageInstaller.SessionParams; import android.content.pm.PackageInstallerHidden; import android.content.pm.PackageManager; import android.content.pm.VersionedPackage; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.os.Process; import android.os.RemoteException; import android.os.UserHandleHidden; import android.provider.Settings; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.WorkerThread; import androidx.core.app.PendingIntentCompat; import androidx.core.content.ContextCompat; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import aosp.libcore.util.EmptyArray; import dev.rikka.tools.refine.Refine; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.apk.ApkFile; import io.github.muntashirakon.AppManager.apk.ApkUtils; import io.github.muntashirakon.AppManager.compat.ManifestCompat; import io.github.muntashirakon.AppManager.compat.PackageManagerCompat; import io.github.muntashirakon.AppManager.ipc.ProxyBinder; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.progress.ProgressHandler; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.settings.Ops; import io.github.muntashirakon.AppManager.types.UserPackagePair; import io.github.muntashirakon.AppManager.users.Users; import io.github.muntashirakon.AppManager.utils.BroadcastUtils; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.ExUtils; import io.github.muntashirakon.AppManager.utils.FileUtils; import io.github.muntashirakon.AppManager.utils.HuaweiUtils; import io.github.muntashirakon.AppManager.utils.MiuiUtils; import io.github.muntashirakon.AppManager.utils.PackageUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.io.Path; @SuppressLint("ShiftFlags") public final class PackageInstallerCompat { public static final String TAG = PackageInstallerCompat.class.getSimpleName(); public static final String ACTION_INSTALL_STARTED = BuildConfig.APPLICATION_ID + ".action.INSTALL_STARTED"; public static final String ACTION_INSTALL_COMPLETED = BuildConfig.APPLICATION_ID + ".action.INSTALL_COMPLETED"; // For rootless installer to prevent PackageInstallerService from hanging public static final String ACTION_INSTALL_INTERACTION_BEGIN = BuildConfig.APPLICATION_ID + ".action.INSTALL_INTERACTION_BEGIN"; public static final String ACTION_INSTALL_INTERACTION_END = BuildConfig.APPLICATION_ID + ".action.INSTALL_INTERACTION_END"; @IntDef({ STATUS_SUCCESS, STATUS_FAILURE_ABORTED, STATUS_FAILURE_BLOCKED, STATUS_FAILURE_CONFLICT, STATUS_FAILURE_INCOMPATIBLE, STATUS_FAILURE_INVALID, STATUS_FAILURE_STORAGE, // Custom STATUS_FAILURE_SECURITY, STATUS_FAILURE_SESSION_CREATE, STATUS_FAILURE_SESSION_WRITE, STATUS_FAILURE_SESSION_COMMIT, STATUS_FAILURE_SESSION_ABANDON, STATUS_FAILURE_INCOMPATIBLE_ROM, }) @Retention(RetentionPolicy.SOURCE) public @interface Status { } /** * See {@link PackageInstaller#STATUS_SUCCESS} */ public static final int STATUS_SUCCESS = PackageInstaller.STATUS_SUCCESS; /** * See {@link PackageInstaller#STATUS_FAILURE_ABORTED} */ public static final int STATUS_FAILURE_ABORTED = PackageInstaller.STATUS_FAILURE_ABORTED; /** * See {@link PackageInstaller#STATUS_FAILURE_BLOCKED} */ public static final int STATUS_FAILURE_BLOCKED = PackageInstaller.STATUS_FAILURE_BLOCKED; /** * See {@link PackageInstaller#STATUS_FAILURE_CONFLICT} */ public static final int STATUS_FAILURE_CONFLICT = PackageInstaller.STATUS_FAILURE_CONFLICT; /** * See {@link PackageInstaller#STATUS_FAILURE_INCOMPATIBLE} */ public static final int STATUS_FAILURE_INCOMPATIBLE = PackageInstaller.STATUS_FAILURE_INCOMPATIBLE; /** * See {@link PackageInstaller#STATUS_FAILURE_INVALID} */ public static final int STATUS_FAILURE_INVALID = PackageInstaller.STATUS_FAILURE_INVALID; /** * See {@link PackageInstaller#STATUS_FAILURE_STORAGE} */ public static final int STATUS_FAILURE_STORAGE = PackageInstaller.STATUS_FAILURE_STORAGE; // Custom status /** * The operation failed because the apk file(s) are not accessible. */ public static final int STATUS_FAILURE_SECURITY = -2; /** * The operation failed because it failed to create an installer session. */ public static final int STATUS_FAILURE_SESSION_CREATE = -3; /** * The operation failed because it failed to write apk files to session. */ public static final int STATUS_FAILURE_SESSION_WRITE = -4; /** * The operation failed because it could not commit the installer session. */ public static final int STATUS_FAILURE_SESSION_COMMIT = -5; /** * The operation failed because it could not abandon the installer session. This is a redundant * failure. */ public static final int STATUS_FAILURE_SESSION_ABANDON = -6; /** * The operation failed because the current ROM is incompatible with PackageInstaller */ public static final int STATUS_FAILURE_INCOMPATIBLE_ROM = -7; @SuppressLint({"NewApi", "UniqueConstants", "InlinedApi"}) @IntDef(flag = true, value = { INSTALL_REPLACE_EXISTING, INSTALL_ALLOW_TEST, INSTALL_EXTERNAL, INSTALL_INTERNAL, INSTALL_FROM_ADB, INSTALL_ALL_USERS, INSTALL_REQUEST_DOWNGRADE, INSTALL_GRANT_ALL_REQUESTED_PERMISSIONS, INSTALL_ALL_WHITELIST_RESTRICTED_PERMISSIONS, INSTALL_FORCE_VOLUME_UUID, INSTALL_FORCE_PERMISSION_PROMPT, INSTALL_INSTANT_APP, INSTALL_DONT_KILL_APP, INSTALL_FULL_APP, INSTALL_ALLOCATE_AGGRESSIVE, INSTALL_VIRTUAL_PRELOAD, INSTALL_APEX, INSTALL_ENABLE_ROLLBACK, INSTALL_DISABLE_VERIFICATION, INSTALL_ALLOW_DOWNGRADE, INSTALL_ALLOW_DOWNGRADE_API29, INSTALL_STAGED, INSTALL_DRY_RUN, INSTALL_BYPASS_LOW_TARGET_SDK_BLOCK, INSTALL_REQUEST_UPDATE_OWNERSHIP, INSTALL_FROM_MANAGED_USER_OR_PROFILE, INSTALL_IGNORE_DEXOPT_PROFILE, }) @Retention(RetentionPolicy.SOURCE) public @interface InstallFlags { } /** * Flag parameter for {@code #installPackage} to indicate that you want to replace an already * installed package, if one exists. */ public static final int INSTALL_REPLACE_EXISTING = 0x00000002; /** * Flag parameter for {@code #installPackage} to indicate that you want to * allow test packages (those that have set android:testOnly in their * manifest) to be installed. */ public static final int INSTALL_ALLOW_TEST = 0x00000004; /** * Flag parameter for {@code #installPackage} to indicate that this * package has to be installed on the sdcard. * * @deprecated Removed in API 29 (Android 10) */ @SuppressWarnings("DeprecatedIsStillUsed") @Deprecated public static final int INSTALL_EXTERNAL = 0x00000008; /** * Flag parameter for {@code #installPackage} to indicate that this package * has to be installed on the sdcard. */ public static final int INSTALL_INTERNAL = 0x00000010; /** * Flag parameter for {@code #installPackage} to indicate that this install * was initiated via ADB. */ public static final int INSTALL_FROM_ADB = 0x00000020; /** * Flag parameter for {@code #installPackage} to indicate that this install * should immediately be visible to all users. */ public static final int INSTALL_ALL_USERS = 0x00000040; /** * Flag parameter for {@code #installPackage} to indicate that an upgrade to a lower version * of a package than currently installed has been requested. * *

Note that this flag doesn't guarantee that downgrade will be performed. That decision * depends * on whenever: *

    *
  • An app is debuggable. *
  • Or a build is debuggable. *
  • Or {@link #INSTALL_ALLOW_DOWNGRADE} is set. *
*/ @RequiresApi(Build.VERSION_CODES.Q) public static final int INSTALL_REQUEST_DOWNGRADE = 0x00000080; /** * Flag parameter for {@code #installPackage} to indicate that all runtime * permissions should be granted to the package. If {@link #INSTALL_ALL_USERS} * is set the runtime permissions will be granted to all users, otherwise * only to the owner. *

* Previously called {@code #INSTALL_GRANT_RUNTIME_PERMISSIONS} */ @RequiresApi(Build.VERSION_CODES.M) public static final int INSTALL_GRANT_ALL_REQUESTED_PERMISSIONS = 0x00000100; /** * Flag parameter for {@code #installPackage} to indicate that all restricted * permissions should be whitelisted. If {@link #INSTALL_ALL_USERS} * is set the restricted permissions will be whitelisted for all users, otherwise * only to the owner. */ @RequiresApi(Build.VERSION_CODES.Q) public static final int INSTALL_ALL_WHITELIST_RESTRICTED_PERMISSIONS = 0x00400000; @RequiresApi(Build.VERSION_CODES.M) public static final int INSTALL_FORCE_VOLUME_UUID = 0x00000200; /** * Flag parameter for {@code #installPackage} to indicate that we always want to force * the prompt for permission approval. This overrides any special behaviour for internal * components. */ @RequiresApi(Build.VERSION_CODES.N) public static final int INSTALL_FORCE_PERMISSION_PROMPT = 0x00000400; /** * Flag parameter for {@code #installPackage} to indicate that this package is * to be installed as a lightweight "ephemeral" app. *

* Previously known as {@code #INSTALL_EPHEMERAL} */ @RequiresApi(Build.VERSION_CODES.N) public static final int INSTALL_INSTANT_APP = 0x00000800; /** * Flag parameter for {@code #installPackage} to indicate that this package contains * a feature split to an existing application and the existing application should not * be killed during the installation process. */ @RequiresApi(Build.VERSION_CODES.N) public static final int INSTALL_DONT_KILL_APP = 0x00001000; /** * Flag parameter for {@code #installPackage} to indicate that this package is an * upgrade to a package that refers to the SDK via release letter. * * @deprecated Removed in API 29 (Android 10) */ @Deprecated @RequiresApi(Build.VERSION_CODES.N) public static final int INSTALL_FORCE_SDK = 0x00002000; /** * Flag parameter for {@code #installPackage} to indicate that this package is * to be installed as a heavy weight app. This is fundamentally the opposite of * {@link #INSTALL_INSTANT_APP}. */ @RequiresApi(Build.VERSION_CODES.O) public static final int INSTALL_FULL_APP = 0x00004000; /** * Flag parameter for {@code #installPackage} to indicate that this package * is critical to system health or security, meaning the system should use * {@code StorageManager#FLAG_ALLOCATE_AGGRESSIVE} internally. */ @RequiresApi(Build.VERSION_CODES.O) public static final int INSTALL_ALLOCATE_AGGRESSIVE = 0x00008000; /** * Flag parameter for {@code #installPackage} to indicate that this package * is a virtual preload. */ @RequiresApi(Build.VERSION_CODES.O_MR1) public static final int INSTALL_VIRTUAL_PRELOAD = 0x00010000; /** * Flag parameter for {@code #installPackage} to indicate that this package * is an APEX package */ @RequiresApi(Build.VERSION_CODES.Q) public static final int INSTALL_APEX = 0x00020000; /** * Flag parameter for {@code #installPackage} to indicate that rollback * should be enabled for this install. */ @RequiresApi(Build.VERSION_CODES.Q) public static final int INSTALL_ENABLE_ROLLBACK = 0x00040000; /** * Flag parameter for {@code #installPackage} to indicate that package verification should be * disabled for this package. */ @RequiresApi(Build.VERSION_CODES.Q) public static final int INSTALL_DISABLE_VERIFICATION = 0x00080000; /** * Flag parameter for {@code #installPackage} to indicate that * {@link #INSTALL_REQUEST_DOWNGRADE} should be allowed. */ @RequiresApi(Build.VERSION_CODES.Q) public static final int INSTALL_ALLOW_DOWNGRADE_API29 = 0x00100000; /** * Flag parameter for {@code #installPackage} to indicate that this package * is being installed as part of a staged install. */ @RequiresApi(Build.VERSION_CODES.Q) public static final int INSTALL_STAGED = 0x00200000; /** * Flag parameter for {@code #installPackage} to indicate that package should only be verified * but not installed. * * @deprecated Removed in API 30 (Android 11) */ @SuppressWarnings("DeprecatedIsStillUsed") @RequiresApi(Build.VERSION_CODES.Q) @Deprecated public static final int INSTALL_DRY_RUN = 0x00800000; /** * Flag parameter for {@code #installPackage} to indicate that it is okay * to install an update to an app where the newly installed app has a lower * version code than the currently installed app. * * @deprecated Replaced by {@link #INSTALL_ALLOW_DOWNGRADE_API29} in Android 10 */ @SuppressWarnings("DeprecatedIsStillUsed") @Deprecated public static final int INSTALL_ALLOW_DOWNGRADE = 0x00000080; /** * Flag parameter for {@code #installPackage} to bypass the low targer sdk version block * for this install. */ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public static final int INSTALL_BYPASS_LOW_TARGET_SDK_BLOCK = 0x01000000; /** * Flag parameter for {@link SessionParams} to indicate that the * update ownership enforcement is requested. */ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public static final int INSTALL_REQUEST_UPDATE_OWNERSHIP = 1 << 25; /** * Flag parameter for {@link SessionParams} to indicate that this * session is from a managed user or profile. */ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public static final int INSTALL_FROM_MANAGED_USER_OR_PROFILE = 1 << 26; /** * If set, all dexopt profiles are ignored by dexopt during the installation, including the * profile in the DM file and the profile embedded in the APK file. If an invalid profile is * provided during installation, no warning will be reported by {@code adb install}. *

* This option does not affect later dexopt operations (e.g., background dexopt and manual `pm * compile` invocations). */ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public static final int INSTALL_IGNORE_DEXOPT_PROFILE = 1 << 28; @SuppressLint({"NewApi", "InlinedApi"}) @IntDef(flag = true, value = { DELETE_KEEP_DATA, DELETE_ALL_USERS, DELETE_SYSTEM_APP, DELETE_DONT_KILL_APP, DELETE_CHATTY, }) @Retention(RetentionPolicy.SOURCE) public @interface DeleteFlags { } /** * Flag parameter for {@code #deletePackage} to indicate that you don't want to delete the * package's data directory. */ public static final int DELETE_KEEP_DATA = 0x00000001; /** * Flag parameter for {@code #deletePackage} to indicate that you want the * package deleted for all users. */ public static final int DELETE_ALL_USERS = 0x00000002; /** * Flag parameter for {@code #deletePackage} to indicate that, if you are calling * uninstall on a system that has been updated, then don't do the normal process * of uninstalling the update and rolling back to the older system version (which * needs to happen for all users); instead, just mark the app as uninstalled for * the current user. */ public static final int DELETE_SYSTEM_APP = 0x00000004; /** * Flag parameter for {@code #deletePackage} to indicate that, if you are calling * uninstall on a package that is replaced to provide new feature splits, the * existing application should not be killed during the removal process. */ @RequiresApi(Build.VERSION_CODES.N) public static final int DELETE_DONT_KILL_APP = 0x00000008; /** * Flag parameter for {@code #deletePackage} to indicate that package deletion * should be chatty. */ @RequiresApi(Build.VERSION_CODES.P) public static final int DELETE_CHATTY = 0x80000000; public static final String SETTINGS_VERIFIER_VERIFY_ADB_INSTALLS = "verifier_verify_adb_installs"; public interface OnInstallListener { @WorkerThread void onStartInstall(int sessionId, String packageName); // MIUI-begin: MIUI 12.5+ workaround /** * MIUI 12.5+ may require more than one tries in order to have successful installations. * This is only needed during APK installations, not APK uninstallations or install-existing * attempts. * * @param apkFile Underlying APK file if available. */ @WorkerThread default void onAnotherAttemptInMiui(@Nullable ApkFile apkFile) { } // MIUI-end // HyperOS-begin: HyperOS 2.0+ workaround /** * In HyperOS 2.0+, the installer for the system apps must be another system app. The * overridden method must set the package installer to a valid system app. This is only * needed during APK installations, not APK uninstallations or install-existing attempts. * * @param apkFile Underlying APK file if available. */ @WorkerThread default void onSecondAttemptInHyperOsWithoutInstaller(@Nullable ApkFile apkFile) { } // HyperOS-end @WorkerThread void onFinishedInstall(int sessionId, String packageName, int result, @Nullable String blockingPackage, @Nullable String statusMessage); } @NonNull public static PackageInstallerCompat getNewInstance() { return new PackageInstallerCompat(); } private CountDownLatch mInstallWatcher; private CountDownLatch mInteractionWatcher; private boolean mCloseApkFile = true; private boolean mInstallCompleted = false; @Nullable private ApkFile mApkFile; private String mPackageName; @Nullable private CharSequence mAppLabel; private int mSessionId = -1; @Status private int mFinalStatus = STATUS_FAILURE_INVALID; @Nullable private String mStatusMessage; private PackageInstallerBroadcastReceiver mPkgInstallerReceiver; private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, @NonNull Intent intent) { if (intent.getAction() == null) return; int sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1); Log.d(TAG, "Action: %s", intent.getAction()); Log.d(TAG, "Session ID: %d", sessionId); switch (intent.getAction()) { case ACTION_INSTALL_STARTED: // Session successfully created if (mOnInstallListener != null) { mOnInstallListener.onStartInstall(sessionId, mPackageName); } break; case ACTION_INSTALL_INTERACTION_BEGIN: // An installation prompt is being shown to the user // Run indefinitely until user finally decides to do something about it break; case ACTION_INSTALL_INTERACTION_END: // The installation prompt is hidden by the user, either by clicking cancel or install, // or just clicking on some place else (latter is our main focus) if (mSessionId == sessionId) { // The user interaction is done, it doesn't take more than 1 minute now mInteractionWatcher.countDown(); } break; case ACTION_INSTALL_COMPLETED: // Either it failed to create a session or the installation was completed, // regardless of the status: success or failure mFinalStatus = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, STATUS_FAILURE_INVALID); String blockingPackage = intent.getStringExtra(PackageInstaller.EXTRA_OTHER_PACKAGE_NAME); mStatusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE); // Run install completed mInstallCompleted = true; ThreadUtils.postOnBackgroundThread(() -> installCompleted(sessionId, mFinalStatus, blockingPackage, mStatusMessage)); break; } } }; @Nullable private OnInstallListener mOnInstallListener; private IPackageInstaller mPackageInstaller; private PackageInstaller.Session mSession; // MIUI-added: Multiple attempts may be required int mAttempts = 1; private final Context mContext; private final boolean mHasInstallPackagePermission; private int mLastVerifyAdbInstallsResult; private PackageInstallerCompat() { mContext = ContextUtils.getContext(); mHasInstallPackagePermission = SelfPermissions.checkSelfOrRemotePermission(Manifest.permission.INSTALL_PACKAGES); mLastVerifyAdbInstallsResult = -1; } public void setOnInstallListener(@Nullable OnInstallListener onInstallListener) { mOnInstallListener = onInstallListener; } public void setAppLabel(@Nullable CharSequence appLabel) { mAppLabel = appLabel; } @NonNull private static int[] getAllRequestedUsers(int userId) { switch (userId) { case UserHandleHidden.USER_ALL: return Users.getAllUserIds(); case UserHandleHidden.USER_NULL: return EmptyArray.INT; default: return new int[]{userId}; } } public boolean install(@NonNull ApkFile apkFile, @NonNull List selectedSplitIds, @NonNull InstallerOptions options, @Nullable ProgressHandler progressHandler) { ThreadUtils.ensureWorkerThread(); try { mApkFile = apkFile; mPackageName = Objects.requireNonNull(apkFile.getPackageName()); initBroadcastReceiver(); int userId = options.getUserId(); int installFlags = getInstallFlags(userId); int[] allRequestedUsers = getAllRequestedUsers(userId); if (allRequestedUsers.length == 0) { Log.d(TAG, "Install: no users."); callFinish(STATUS_FAILURE_INVALID); return false; } Log.d(TAG, "Installing for users: %s", Arrays.toString(allRequestedUsers)); for (int u : allRequestedUsers) { if (!SelfPermissions.checkCrossUserPermission(u, true)) { installCompleted(mSessionId, STATUS_FAILURE_BLOCKED, "android", "STATUS_FAILURE_BLOCKED: Insufficient permission."); Log.d(TAG, "Install: Requires INTERACT_ACROSS_USERS and INTERACT_ACROSS_USERS_FULL permissions."); return false; } } ThreadUtils.postOnBackgroundThread(() -> { // TODO: 6/6/23 Wait for this task to finish before returning // FIXME: 16/6/23 Needed only for one user? for (int u : allRequestedUsers) { copyObb(apkFile, u); } }); userId = allRequestedUsers[0]; String originatingPackage = options.isSetOriginatingPackage() ? options.getOriginatingPackage() : null; Uri originatingUri = options.isSetOriginatingPackage() ? options.getOriginatingUri() : null; Log.d(TAG, "Install: opening session..."); if (!openSession(userId, installFlags, options.getInstallerName(), options.getInstallLocation(), originatingPackage, originatingUri, options.getInstallScenario(), options.getPackageSource(), options.requestUpdateOwnership(), options.isDisableApkVerification())) { return false; } List selectedEntries = new ArrayList<>(); long totalSize = 0; for (ApkFile.Entry entry : apkFile.getEntries()) { if (selectedSplitIds.contains(entry.id)) { selectedEntries.add(entry); try { totalSize += entry.getFile(options.isSignApkFiles()).length(); } catch (IOException e) { callFinish(STATUS_FAILURE_INVALID); Log.e(TAG, "Install: Cannot retrieve the selected APK files.", e); return abandon(); } } } Log.d(TAG, "Install: selected entries: %s", selectedSplitIds); // Write apk files for (ApkFile.Entry entry : selectedEntries) { long entrySize = entry.getFileSize(options.isSignApkFiles()); try (InputStream apkInputStream = entry.getInputStream(options.isSignApkFiles()); OutputStream apkOutputStream = mSession.openWrite(entry.getFileName(), 0, entrySize)) { FileUtils.copy(apkInputStream, apkOutputStream, totalSize, progressHandler); mSession.fsync(apkOutputStream); Log.d(TAG, "Install: copied entry %s", entry.name); } catch (IOException e) { callFinish(STATUS_FAILURE_SESSION_WRITE); Log.e(TAG, "Install: Cannot copy files to session.", e); return abandon(); } catch (SecurityException e) { callFinish(STATUS_FAILURE_SECURITY); Log.e(TAG, "Install: Cannot access apk files.", e); return abandon(); } } Log.d(TAG, "Install: Running installation..."); // Commit return commit(userId); } finally { unregisterReceiver(); restoreVerifySettings(); } } public boolean install(@NonNull Path[] apkFiles, @NonNull String packageName, @NonNull InstallerOptions options) { return install(apkFiles, packageName, options, null); } public boolean install(@NonNull Path[] apkFiles, @NonNull String packageName, @NonNull InstallerOptions options, @Nullable ProgressHandler progressHandler) { ThreadUtils.ensureWorkerThread(); try { mApkFile = null; mPackageName = Objects.requireNonNull(packageName); initBroadcastReceiver(); int userId = options.getUserId(); int installFlags = getInstallFlags(userId); int[] allRequestedUsers = getAllRequestedUsers(userId); if (allRequestedUsers.length == 0) { Log.d(TAG, "Install: no users."); callFinish(STATUS_FAILURE_INVALID); return false; } Log.d(TAG, "Installing for users: %s", Arrays.toString(allRequestedUsers)); for (int u : allRequestedUsers) { if (!SelfPermissions.checkCrossUserPermission(u, true)) { installCompleted(mSessionId, STATUS_FAILURE_BLOCKED, "android", "STATUS_FAILURE_BLOCKED: Insufficient permission."); Log.d(TAG, "Install: Requires INTERACT_ACROSS_USERS and INTERACT_ACROSS_USERS_FULL permissions."); return false; } } userId = allRequestedUsers[0]; String originatingPackage = options.isSetOriginatingPackage() ? options.getOriginatingPackage() : null; Uri originatingUri = options.isSetOriginatingPackage() ? options.getOriginatingUri() : null; if (!openSession(userId, installFlags, options.getInstallerName(), options.getInstallLocation(), originatingPackage, originatingUri, options.getInstallScenario(), options.getPackageSource(), options.requestUpdateOwnership(), options.isDisableApkVerification())) { return false; } long totalSize = 0; for (Path apkFile : apkFiles) { totalSize += apkFile.length(); } // Write apk files for (Path apkFile : apkFiles) { try (InputStream apkInputStream = apkFile.openInputStream(); OutputStream apkOutputStream = mSession.openWrite(apkFile.getName(), 0, apkFile.length())) { FileUtils.copy(apkInputStream, apkOutputStream, totalSize, progressHandler); mSession.fsync(apkOutputStream); } catch (IOException e) { callFinish(STATUS_FAILURE_SESSION_WRITE); Log.e(TAG, "Install: Cannot copy files to session.", e); return abandon(); } catch (SecurityException e) { callFinish(STATUS_FAILURE_SECURITY); Log.e(TAG, "Install: Cannot access apk files.", e); return abandon(); } } // Commit return commit(userId); } finally { unregisterReceiver(); restoreVerifySettings(); } } private boolean commit(int userId) { IntentSender sender; LocalIntentReceiver intentReceiver; if (mHasInstallPackagePermission) { Log.d(TAG, "Commit: Commit via LocalIntentReceiver..."); try { intentReceiver = new LocalIntentReceiver(); sender = intentReceiver.getIntentSender(); } catch (Exception e) { callFinish(STATUS_FAILURE_SESSION_COMMIT); Log.e(TAG, "Commit: Could not commit session.", e); return false; } } else { Log.d(TAG, "Commit: Calling activity to request permission..."); intentReceiver = null; Intent callbackIntent = new Intent(PackageInstallerBroadcastReceiver.ACTION_PI_RECEIVER); callbackIntent.setPackage(BuildConfig.APPLICATION_ID); PendingIntent pendingIntent = PendingIntentCompat.getBroadcast(mContext, 0, callbackIntent, 0, true); sender = pendingIntent.getIntentSender(); } Log.d(TAG, "Commit: Committing..."); try { mSession.commit(sender); } catch (Throwable e) { // primarily RemoteException callFinish(STATUS_FAILURE_SESSION_COMMIT); Log.e(TAG, "Commit: Could not commit session.", e); return false; } if (intentReceiver == null) { Log.d(TAG, "Commit: Waiting for user interaction..."); // Wait for user interaction (if needed) try { // Wait for user interaction mInteractionWatcher.await(); // Wait for the installation to complete mInstallWatcher.await(1, TimeUnit.MINUTES); } catch (InterruptedException e) { Log.e(TAG, "Installation interrupted.", e); } } else { Intent resultIntent = intentReceiver.getResult(); mFinalStatus = resultIntent.getIntExtra(PackageInstaller.EXTRA_STATUS, 0); mStatusMessage = resultIntent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE); } Log.d(TAG, "Commit: Finishing..."); // We might want to use {@code callFinish(finalStatus);} here, but it doesn't always work // since the object is garbage collected almost immediately. if (!mInstallCompleted) { installCompleted(mSessionId, mFinalStatus, null, mStatusMessage); } if (mFinalStatus == PackageInstaller.STATUS_SUCCESS && userId != UserHandleHidden.myUserId()) { BroadcastUtils.sendPackageAltered(ContextUtils.getContext(), new String[]{mPackageName}); } return mFinalStatus == PackageInstaller.STATUS_SUCCESS; } @SuppressWarnings("BooleanMethodIsAlwaysInverted") private boolean openSession(@UserIdInt int userId, @InstallFlags int installFlags, String installerName, int installLocation, @Nullable String originatingPackage, @Nullable Uri originatingUri, int installScenario, int packageSource, boolean requestUpdateOwnership, boolean disableVerification) { // Changing package installer in stock Huawei with UID 2000 does not work boolean canChangeInstaller = mHasInstallPackagePermission && (!HuaweiUtils.isStockHuawei() || Users.getSelfOrRemoteUid() != Ops.SHELL_UID); String requestedInstallerPackageName = canChangeInstaller ? installerName : null; String installerPackageName = Build.VERSION.SDK_INT < Build.VERSION_CODES.P && canChangeInstaller ? installerName : BuildConfig.APPLICATION_ID; try { mPackageInstaller = PackageManagerCompat.getPackageInstaller(); } catch (RemoteException e) { callFinish(STATUS_FAILURE_SESSION_CREATE); Log.e(TAG, "OpenSession: Could not get PackageInstaller.", e); return false; } // Clean old sessions cleanOldSessions(); // Create install session SessionParams sessionParams = new SessionParams(SessionParams.MODE_FULL_INSTALL); if (disableVerification) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && SelfPermissions.isSystemOrRootOrShell()) { // This disables verification for this UID temporarily ExUtils.exceptionAsIgnored(() -> mPackageInstaller.disableVerificationForUid(Users.getSelfOrRemoteUid())); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { installFlags |= INSTALL_DISABLE_VERIFICATION; } // In addition, we may also want to use the traditional methods if (SelfPermissions.isShell()) { mLastVerifyAdbInstallsResult = Settings.Global.getInt(mContext.getContentResolver(), SETTINGS_VERIFIER_VERIFY_ADB_INSTALLS, 1); if (mLastVerifyAdbInstallsResult != 0) { Settings.Global.putInt(mContext.getContentResolver(), SETTINGS_VERIFIER_VERIFY_ADB_INSTALLS, 0); } } } Refine.unsafeCast(sessionParams).installFlags |= installFlags; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { Refine.unsafeCast(sessionParams).installerPackageName = requestedInstallerPackageName; } // Set installation location sessionParams.setInstallLocation(installLocation); // Set origin if (originatingUri != null) { sessionParams.setOriginatingUri(originatingUri); } if (originatingPackage != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { int uid = PackageUtils.getAppUid(new UserPackagePair(originatingPackage, UserHandleHidden.myUserId())); if (uid >= 0) { sessionParams.setOriginatingUid(uid); Log.d(TAG, "Setting originating uid: %d for %s", uid, originatingPackage); } } // Set install reason if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { sessionParams.setInstallReason(PackageManager.INSTALL_REASON_USER); } // Set install user action and install scenario if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // We hope system will not prompt an install confirmation sessionParams.setRequireUserAction(SessionParams.USER_ACTION_NOT_REQUIRED); sessionParams.setInstallScenario(installScenario); } // Set package source (shell uses PACKAGE_SOURCE_OTHER) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { sessionParams.setPackageSource(packageSource); } // Set ownership (disable by default in shell) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { sessionParams.setApplicationEnabledSettingPersistent(); sessionParams.setRequestUpdateOwnership(requestUpdateOwnership); } try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { mSessionId = mPackageInstaller.createSession(sessionParams, installerPackageName, null, userId); } else { //noinspection deprecation mSessionId = mPackageInstaller.createSession(sessionParams, installerPackageName, userId); } Log.d(TAG, "OpenSession: session id %d", mSessionId); } catch (RemoteException e) { callFinish(STATUS_FAILURE_SESSION_CREATE); Log.e(TAG, "OpenSession: Failed to create install session.", e); return false; } try { mSession = Refine.unsafeCast(new PackageInstallerHidden.Session(IPackageInstallerSession.Stub.asInterface( new ProxyBinder(mPackageInstaller.openSession(mSessionId).asBinder())))); Log.d(TAG, "OpenSession: session opened."); } catch (RemoteException e) { callFinish(STATUS_FAILURE_SESSION_CREATE); Log.e(TAG, "OpenSession: Failed to open install session.", e); return false; } sendStartedBroadcast(mPackageName, mSessionId); return true; } private void restoreVerifySettings() { if (mLastVerifyAdbInstallsResult == 1) { int val = Settings.Global.getInt(mContext.getContentResolver(), SETTINGS_VERIFIER_VERIFY_ADB_INSTALLS, 1); if (val != 1) { // Restore value Settings.Global.putInt(mContext.getContentResolver(), SETTINGS_VERIFIER_VERIFY_ADB_INSTALLS, 1); } } } @InstallFlags private static int getInstallFlags(@UserIdInt int userId) { int flags = INSTALL_FROM_ADB | INSTALL_ALLOW_TEST | INSTALL_REPLACE_EXISTING; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { flags |= INSTALL_FULL_APP; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { flags |= INSTALL_REQUEST_DOWNGRADE | INSTALL_ALLOW_DOWNGRADE_API29; } else flags |= INSTALL_ALLOW_DOWNGRADE; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { flags |= INSTALL_BYPASS_LOW_TARGET_SDK_BLOCK; } if (userId == UserHandleHidden.USER_ALL) { flags |= INSTALL_ALL_USERS; } return flags; } public boolean installExisting(@NonNull String packageName, @UserIdInt int userId) { ThreadUtils.ensureWorkerThread(); mPackageName = Objects.requireNonNull(packageName); if (mOnInstallListener != null) { mOnInstallListener.onStartInstall(mSessionId, packageName); } mInstallWatcher = new CountDownLatch(0); mInteractionWatcher = new CountDownLatch(0); if (!SelfPermissions.canInstallExistingPackages()) { installCompleted(mSessionId, STATUS_FAILURE_BLOCKED, "android", "STATUS_FAILURE_BLOCKED: Insufficient permission."); Log.d(TAG, "InstallExisting: Requires INSTALL_PACKAGES permission."); return false; } // User ID must be a real user List userIdWithoutInstalledPkg = new ArrayList<>(); switch (userId) { case UserHandleHidden.USER_ALL: { int[] userIds = Users.getUsersIds(); for (int u : userIds) { try { PackageManagerCompat.getPackageInfo(packageName, PackageManagerCompat.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES, u); } catch (Throwable th) { userIdWithoutInstalledPkg.add(u); } } break; } case UserHandleHidden.USER_NULL: installCompleted(mSessionId, STATUS_FAILURE_INVALID, null, "STATUS_FAILURE_INVALID: No user is selected."); Log.d(TAG, "InstallExisting: No user is selected."); return false; default: try { PackageManagerCompat.getPackageInfo(packageName, PackageManagerCompat.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES, userId); installCompleted(mSessionId, STATUS_FAILURE_ABORTED, null, "STATUS_FAILURE_ABORTED: Already installed."); Log.d(TAG, "InstallExisting: Already installed."); return false; } catch (Throwable th) { userIdWithoutInstalledPkg.add(userId); } } if (userIdWithoutInstalledPkg.isEmpty()) { installCompleted(mSessionId, STATUS_FAILURE_INVALID, null, "STATUS_FAILURE_INVALID: Could not find a valid user to perform install-existing."); Log.d(TAG, "InstallExisting: Could not find any valid user."); return false; } int installFlags = 0; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { installFlags |= INSTALL_ALL_WHITELIST_RESTRICTED_PERMISSIONS; } int installReason; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { installReason = PackageManager.INSTALL_REASON_USER; } else installReason = 0; for (int u : userIdWithoutInstalledPkg) { if (!SelfPermissions.checkCrossUserPermission(u, true)) { installCompleted(mSessionId, STATUS_FAILURE_BLOCKED, "android", "STATUS_FAILURE_BLOCKED: Insufficient permission."); Log.d(TAG, "InstallExisting: Requires INTERACT_ACROSS_USERS and INTERACT_ACROSS_USERS_FULL permissions."); return false; } try { int res = PackageManagerCompat.installExistingPackageAsUser(packageName, u, installFlags, installReason, null); if (res != 1 /* INSTALL_SUCCEEDED */) { installCompleted(mSessionId, res, null, null); Log.e(TAG, "InstallExisting: Install failed with code %d", res); return false; } if (u != UserHandleHidden.myUserId()) { BroadcastUtils.sendPackageAdded(ContextUtils.getContext(), new String[]{packageName}); } } catch (Throwable th) { installCompleted(mSessionId, STATUS_FAILURE_ABORTED, null, "STATUS_FAILURE_ABORTED: " + th.getMessage()); Log.e(TAG, "InstallExisting: Could not install package for user %s", th, u); return false; } } installCompleted(mSessionId, STATUS_SUCCESS, null, null); return true; } @WorkerThread private void copyObb(@NonNull ApkFile apkFile, @UserIdInt int userId) { if (!apkFile.hasObb()) return; boolean tmpCloseApkFile = mCloseApkFile; // Disable closing apk file in case the installation is finished already. mCloseApkFile = false; try { // Get writable OBB directory Path writableObbDir = ApkUtils.getOrCreateObbDir(mPackageName, userId); // Delete old files for (Path oldFile : writableObbDir.listFiles()) { oldFile.delete(); } apkFile.extractObb(writableObbDir); ThreadUtils.postOnMainThread(() -> UIUtils.displayLongToast(R.string.obb_files_extracted_successfully)); } catch (Exception e) { Log.e(TAG, e); ThreadUtils.postOnMainThread(() -> UIUtils.displayLongToast(R.string.failed_to_extract_obb_files)); } finally { if (mInstallWatcher.getCount() != 0) { // Reset close apk file if the installation isn't completed mCloseApkFile = tmpCloseApkFile; } else { // Install completed, close apk file if requested if (tmpCloseApkFile) apkFile.close(); } } } private void cleanOldSessions() { if (Users.getSelfOrRemoteUid() != Process.myUid()) { // Only clean sessions for this UID return; } List sessionInfoList; try { sessionInfoList = mPackageInstaller.getMySessions(mContext.getPackageName(), UserHandleHidden.myUserId()).getList(); } catch (Throwable e) { Log.w(TAG, "CleanOldSessions: Could not get previous sessions.", e); return; } for (PackageInstaller.SessionInfo sessionInfo : sessionInfoList) { try { mPackageInstaller.abandonSession(sessionInfo.getSessionId()); } catch (Throwable e) { Log.w(TAG, "CleanOldSessions: Unable to abandon session", e); } } } private boolean abandon() { if (mSession != null) { try { mSession.close(); } catch (Exception e) { // RemoteException Log.e(TAG, "Abandon: Failed to abandon session."); } } return false; } private void callFinish(int result) { sendCompletedBroadcast(mContext, mPackageName, result, mSessionId); } private void installCompleted(int sessionId, int finalStatus, @Nullable String blockingPackage, @Nullable String statusMessage) { ThreadUtils.ensureWorkerThread(); if (finalStatus == STATUS_FAILURE_ABORTED && mSessionId == sessionId && mOnInstallListener != null) { boolean privileged = SelfPermissions.checkSelfPermission(Manifest.permission.INSTALL_PACKAGES); // MIUI-begin: In MIUI 12.5 and 20.2.0, it might be required to try installing the APK files more than once. if (!privileged && MiuiUtils.isActualMiuiVersionAtLeast("12.5", "20.2.0") && Objects.equals(statusMessage, "INSTALL_FAILED_ABORTED: Permission denied") && mAttempts <= 3) { // Try once more ++mAttempts; Log.i(TAG, "MIUI: Installation attempt no %d for package %s", mAttempts, mPackageName); mInteractionWatcher.countDown(); mInstallWatcher.countDown(); // Remove old broadcast receivers unregisterReceiver(); mOnInstallListener.onAnotherAttemptInMiui(mApkFile); return; } // MIUI-end // HyperOS-begin: In HyperOS 2.0, installer package needs to be altered if (privileged // TODO: 1/10/25 Check for HyperOS? && statusMessage != null && statusMessage.startsWith("INSTALL_FAILED_HYPEROS_ISOLATION_VIOLATION: ") && mAttempts <= 2) { // Try a second time with installer set to shell ++mAttempts; Log.i(TAG, "HyperOS: %s", statusMessage); Log.i(TAG, "HyperOS: Second attempt for %s", mPackageName); mInteractionWatcher.countDown(); mInstallWatcher.countDown(); // Remove old broadcast receivers unregisterReceiver(); mOnInstallListener.onSecondAttemptInHyperOsWithoutInstaller(mApkFile); return; } // HyperOS-end } // No need to check package name since it's been checked before if (finalStatus == STATUS_FAILURE_SESSION_CREATE || (mSessionId == sessionId)) { if (mOnInstallListener != null) { mOnInstallListener.onFinishedInstall(sessionId, mPackageName, finalStatus, blockingPackage, statusMessage); } if (mCloseApkFile && mApkFile != null) { mApkFile.close(); } mInteractionWatcher.countDown(); mInstallWatcher.countDown(); } } @SuppressWarnings("deprecation") public boolean uninstall(String packageName, @UserIdInt int userId, boolean keepData) { ThreadUtils.ensureWorkerThread(); boolean hasDeletePackagesPermission = SelfPermissions.checkSelfOrRemotePermission(Manifest.permission.DELETE_PACKAGES); mPackageName = Objects.requireNonNull(packageName); String callerPackageName = SelfPermissions.getCallingPackage(Users.getSelfOrRemoteUid()); initBroadcastReceiver(); try { if (userId == UserHandleHidden.USER_ALL && Users.getAllUserIds().length > 1 && !SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.INTERACT_ACROSS_USERS_FULL)) { installCompleted(mSessionId, STATUS_FAILURE_BLOCKED, "android", "STATUS_FAILURE_BLOCKED: Insufficient permission."); Log.d(TAG, "Uninstall: Requires INTERACT_ACROSS_USERS and INTERACT_ACROSS_USERS_FULL permissions."); return false; } int flags; try { flags = getDeleteFlags(packageName, userId, keepData); } catch (Exception e) { callFinish(STATUS_FAILURE_SESSION_CREATE); Log.e(TAG, "Uninstall: Could not get PackageInstaller.", e); return false; } userId = getCorrectUserIdForUninstallation(packageName, userId); try { mPackageInstaller = PackageManagerCompat.getPackageInstaller(); } catch (RemoteException e) { callFinish(STATUS_FAILURE_SESSION_CREATE); Log.e(TAG, "Uninstall: Could not get PackageInstaller.", e); return false; } // Perform uninstallation IntentSender sender; LocalIntentReceiver intentReceiver; if (hasDeletePackagesPermission) { Log.d(TAG, "Uninstall: Uninstall via LocalIntentReceiver..."); try { intentReceiver = new LocalIntentReceiver(); sender = intentReceiver.getIntentSender(); } catch (Exception e) { callFinish(STATUS_FAILURE_SESSION_COMMIT); Log.e(TAG, "Uninstall: Could not uninstall %s", e, packageName); return false; } } else { Log.d(TAG, "Uninstall: Calling activity to request permission..."); intentReceiver = null; Intent callbackIntent = new Intent(PackageInstallerBroadcastReceiver.ACTION_PI_RECEIVER); callbackIntent.setPackage(BuildConfig.APPLICATION_ID); PendingIntent pendingIntent = PendingIntentCompat.getBroadcast(mContext, 0, callbackIntent, 0, true); sender = pendingIntent.getIntentSender(); } Log.d(TAG, "Uninstall: Uninstalling..."); try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { mPackageInstaller.uninstall(new VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST), callerPackageName, flags, sender, userId); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { mPackageInstaller.uninstall(packageName, callerPackageName, flags, sender, userId); } else mPackageInstaller.uninstall(packageName, flags, sender, userId); } catch (Throwable th) { // primarily RemoteException callFinish(STATUS_FAILURE_SESSION_COMMIT); Log.e(TAG, "Uninstall: Could not uninstall %s", th, packageName); return false; } if (intentReceiver == null) { Log.d(TAG, "Uninstall: Waiting for user interaction..."); // Wait for user interaction (if needed) try { // Wait for user interaction mInteractionWatcher.await(); // Wait for the installation to complete mInstallWatcher.await(1, TimeUnit.MINUTES); } catch (InterruptedException e) { Log.e(TAG, "Installation interrupted.", e); } } else { Intent resultIntent = intentReceiver.getResult(); mFinalStatus = resultIntent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE); mStatusMessage = resultIntent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE); } Log.d(TAG, "Uninstall: Finished with status %d", mFinalStatus); if (!mInstallCompleted) { installCompleted(mSessionId, mFinalStatus, null, mStatusMessage); } if (mFinalStatus == PackageInstaller.STATUS_SUCCESS && userId != UserHandleHidden.myUserId()) { BroadcastUtils.sendPackageAltered(ContextUtils.getContext(), new String[]{packageName}); } return mFinalStatus == PackageInstaller.STATUS_SUCCESS; } finally { unregisterReceiver(); } } @DeleteFlags private static int getDeleteFlags(@NonNull String packageName, @UserIdInt int userId, boolean keepData) throws PackageManager.NameNotFoundException, RemoteException { int flags = 0; if (userId != UserHandleHidden.USER_ALL) { PackageInfo info = PackageManagerCompat.getPackageInfo(packageName, MATCH_UNINSTALLED_PACKAGES | PackageManagerCompat.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES, userId); final boolean isSystem = (info.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; // If we are being asked to delete a system app for just one // user set flag so it disables rather than reverting to system // version of the app. if (isSystem) { flags |= DELETE_SYSTEM_APP; } } else { flags |= DELETE_ALL_USERS; } if (keepData) { flags |= DELETE_KEEP_DATA; } return flags; } private static int getCorrectUserIdForUninstallation(@NonNull String packageName, @UserIdInt int userId) { if (userId == UserHandleHidden.USER_ALL) { int[] users = Users.getAllUserIds(); for (int user : users) { try { PackageManagerCompat.getPackageInfo(packageName, MATCH_UNINSTALLED_PACKAGES | PackageManagerCompat.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES, user); return user; } catch (Throwable ignore) { } } } return userId; } // https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/pm/PackageManagerShellCommand.java;l=3855;drc=d31ee388115d17c2fd337f2806b37390c7d29834 private static class LocalIntentReceiver { private final LinkedBlockingQueue mResult = new LinkedBlockingQueue<>(); private final IIntentSender.Stub mLocalSender = new IIntentSender.Stub() { @Override public int send(int code, Intent intent, String resolvedType, IIntentReceiver finishedReceiver, String requiredPermission) { send(intent); return 0; } @Override public int send(int code, Intent intent, String resolvedType, IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) { send(intent); return 0; } @Override public void send(int code, Intent intent, String resolvedType, IBinder whitelistToken, IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) { send(intent); } public void send(Intent intent) { try { mResult.offer(intent, 5, TimeUnit.SECONDS); } catch (InterruptedException e) { throw new RuntimeException(e); } } }; @SuppressWarnings("JavaReflectionMemberAccess") public IntentSender getIntentSender() throws Exception { return IntentSender.class.getConstructor(IBinder.class) .newInstance(mLocalSender.asBinder()); } public Intent getResult() { try { return mResult.take(); } catch (InterruptedException e) { throw new RuntimeException(e); } } } private void unregisterReceiver() { if (mPkgInstallerReceiver != null) { ContextUtils.unregisterReceiver(mContext, mPkgInstallerReceiver); } ContextUtils.unregisterReceiver(mContext, mBroadcastReceiver); } private void initBroadcastReceiver() { mInstallWatcher = new CountDownLatch(1); mInteractionWatcher = new CountDownLatch(1); mPkgInstallerReceiver = new PackageInstallerBroadcastReceiver(); mPkgInstallerReceiver.setAppLabel(mAppLabel); mPkgInstallerReceiver.setPackageName(mPackageName); ContextCompat.registerReceiver(mContext, mPkgInstallerReceiver, new IntentFilter(PackageInstallerBroadcastReceiver.ACTION_PI_RECEIVER), ContextCompat.RECEIVER_NOT_EXPORTED); // Add receivers IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(ACTION_INSTALL_COMPLETED); intentFilter.addAction(ACTION_INSTALL_STARTED); intentFilter.addAction(ACTION_INSTALL_INTERACTION_BEGIN); intentFilter.addAction(ACTION_INSTALL_INTERACTION_END); ContextCompat.registerReceiver(mContext, mBroadcastReceiver, intentFilter, ContextCompat.RECEIVER_NOT_EXPORTED); } private void sendStartedBroadcast(@NonNull String packageName, int sessionId) { Intent broadcastIntent = new Intent(ACTION_INSTALL_STARTED); broadcastIntent.setPackage(mContext.getPackageName()); broadcastIntent.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, packageName); broadcastIntent.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId); mContext.sendBroadcast(broadcastIntent); } static void sendCompletedBroadcast(@NonNull Context context, @NonNull String packageName, @Status int status, int sessionId) { Intent broadcastIntent = new Intent(ACTION_INSTALL_COMPLETED); broadcastIntent.setPackage(context.getPackageName()); broadcastIntent.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, packageName); broadcastIntent.putExtra(PackageInstaller.EXTRA_STATUS, status); broadcastIntent.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId); context.sendBroadcast(broadcastIntent); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/PackageInstallerService.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk.installer; import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.STATUS_FAILURE_ABORTED; import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.STATUS_FAILURE_BLOCKED; import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.STATUS_FAILURE_CONFLICT; import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.STATUS_FAILURE_INCOMPATIBLE; import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.STATUS_FAILURE_INCOMPATIBLE_ROM; import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.STATUS_FAILURE_INVALID; import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.STATUS_FAILURE_SECURITY; import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.STATUS_FAILURE_SESSION_ABANDON; import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.STATUS_FAILURE_SESSION_COMMIT; import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.STATUS_FAILURE_SESSION_CREATE; import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.STATUS_FAILURE_SESSION_WRITE; import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.STATUS_FAILURE_STORAGE; import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.STATUS_SUCCESS; import static io.github.muntashirakon.AppManager.history.ops.OpHistoryManager.HISTORY_TYPE_INSTALLER; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.os.Build; import android.os.PowerManager; import android.os.UserHandleHidden; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import androidx.core.app.PendingIntentCompat; import androidx.core.app.ServiceCompat; import java.util.List; import java.util.Objects; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.apk.ApkFile; import io.github.muntashirakon.AppManager.apk.ApkSource; import io.github.muntashirakon.AppManager.apk.CachedApkSource; import io.github.muntashirakon.AppManager.apk.dexopt.DexOptimizer; import io.github.muntashirakon.AppManager.compat.PackageManagerCompat; import io.github.muntashirakon.AppManager.history.ops.OpHistoryManager; import io.github.muntashirakon.AppManager.intercept.IntentCompat; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.main.MainActivity; import io.github.muntashirakon.AppManager.progress.NotificationProgressHandler; import io.github.muntashirakon.AppManager.progress.NotificationProgressHandler.NotificationInfo; import io.github.muntashirakon.AppManager.progress.ProgressHandler; import io.github.muntashirakon.AppManager.progress.QueuedProgressHandler; import io.github.muntashirakon.AppManager.rules.compontents.ComponentUtils; import io.github.muntashirakon.AppManager.types.ForegroundService; import io.github.muntashirakon.AppManager.types.UserPackagePair; import io.github.muntashirakon.AppManager.utils.CpuUtils; import io.github.muntashirakon.AppManager.utils.NotificationUtils; import io.github.muntashirakon.AppManager.utils.PackageUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; public class PackageInstallerService extends ForegroundService { public static final String TAG = PackageInstallerService.class.getSimpleName(); public static final String EXTRA_QUEUE_ITEM = "queue_item"; public static final String CHANNEL_ID = BuildConfig.APPLICATION_ID + ".channel.INSTALL"; public interface OnInstallFinished { @UiThread void onFinished(String packageName, int status, @Nullable String blockingPackage, @Nullable String statusMessage); } public PackageInstallerService() { super(TAG); } @Nullable private OnInstallFinished mOnInstallFinished; private QueuedProgressHandler mProgressHandler; private NotificationInfo mNotificationInfo; private PowerManager.WakeLock mWakeLock; @Override public void onCreate() { super.onCreate(); mWakeLock = CpuUtils.getPartialWakeLock("installer"); mWakeLock.acquire(); } @Override public int onStartCommand(@Nullable Intent intent, int flags, int startId) { if (isWorking()) { return super.onStartCommand(intent, flags, startId); } mProgressHandler = new NotificationProgressHandler( this, new NotificationProgressHandler.NotificationManagerInfo(CHANNEL_ID, "Install Progress", NotificationManagerCompat.IMPORTANCE_LOW), NotificationUtils.HIGH_PRIORITY_NOTIFICATION_INFO, NotificationUtils.HIGH_PRIORITY_NOTIFICATION_INFO ); mProgressHandler.setProgressTextInterface(ProgressHandler.PROGRESS_PERCENT); Intent notificationIntent = new Intent(this, MainActivity.class); PendingIntent pendingIntent = PendingIntentCompat.getActivity(this, 0, notificationIntent, 0, false); mNotificationInfo = new NotificationInfo() .setBody(getString(R.string.install_in_progress)) .setOperationName(getText(R.string.package_installer)) .setDefaultAction(pendingIntent); mProgressHandler.onAttach(this, mNotificationInfo); return super.onStartCommand(intent, flags, startId); } @Override protected void onHandleIntent(@Nullable Intent intent) { ApkQueueItem apkQueueItem = getQueueItem(intent); if (apkQueueItem == null) { return; } InstallerOptions options = apkQueueItem.getInstallerOptions() != null ? apkQueueItem.getInstallerOptions() : InstallerOptions.getDefault(); List selectedSplitIds = Objects.requireNonNull(apkQueueItem.getSelectedSplits()); // Install package PackageInstallerCompat installer = PackageInstallerCompat.getNewInstance(); installer.setAppLabel(apkQueueItem.getAppLabel()); installer.setOnInstallListener(new PackageInstallerCompat.OnInstallListener() { @Override public void onStartInstall(int sessionId, String packageName) { } // MIUI-begin: MIUI 12.5+ workaround @Override public void onAnotherAttemptInMiui(@Nullable ApkFile apkFile) { if (apkFile != null) { installer.install(apkFile, selectedSplitIds, options, mProgressHandler); } } // MIUI-end // HyperOS-begin: HyperOS 2.0+ workaround @Override public void onSecondAttemptInHyperOsWithoutInstaller(@Nullable ApkFile apkFile) { if (apkFile != null) { options.setInstallerName("com.android.shell"); installer.install(apkFile, selectedSplitIds, options, mProgressHandler); } } // HyerOS-end @Override public void onFinishedInstall(int sessionId, String packageName, int result, @Nullable String blockingPackage, @Nullable String statusMessage) { boolean success = result == STATUS_SUCCESS; OpHistoryManager.addHistoryItem(HISTORY_TYPE_INSTALLER, apkQueueItem, success); if (success) { // Block trackers if requested if (options.isBlockTrackers()) { ComponentUtils.blockTrackingComponents(new UserPackagePair(packageName, options.getUserId())); } // Perform force dex optimization if requested if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && options.isForceDexOpt()) { // Ignore the result because it's irrelevant new DexOptimizer(PackageManagerCompat.getPackageManager(), packageName).forceDexOpt(); } } finishInstallation(packageName, result, apkQueueItem.getAppLabel(), blockingPackage, statusMessage); } }); // Two possibilities: 1. Install-existing, 2. ApkFile/Uri if (apkQueueItem.isInstallExisting()) { // Install existing (need no progress) String packageName = apkQueueItem.getPackageName(); if (packageName == null) { // No package name supplied, abort return; } installer.installExisting(packageName, options.getUserId()); } else { // ApkFile/Uri ApkSource apkSource = apkQueueItem.getApkSource(); if (apkSource == null) { // No apk file, abort return; } ApkFile apkFile; try { try { apkFile = apkSource.resolve(); } catch (Throwable th) { Log.w(TAG, "Could not get ApkFile", th); OpHistoryManager.addHistoryItem(HISTORY_TYPE_INSTALLER, apkQueueItem, false); String packageName = apkQueueItem.getPackageName(); finishInstallation(packageName != null ? packageName : "Unknown Package", STATUS_FAILURE_INVALID, apkQueueItem.getAppLabel(), null, null); return; } installer.install(apkFile, selectedSplitIds, options, mProgressHandler); } finally { // Delete the cached file if (apkSource instanceof CachedApkSource) { ((CachedApkSource) apkSource).cleanup(); } } } } @Override protected void onQueued(@Nullable Intent intent) { ApkQueueItem apkQueueItem = getQueueItem(intent); String appLabel = apkQueueItem != null ? apkQueueItem.getAppLabel() : null; Object notificationInfo = new NotificationInfo() .setAutoCancel(true) .setOperationName(getString(R.string.package_installer)) .setTitle(appLabel) .setBody(getString(R.string.added_to_queue)) .setTime(System.currentTimeMillis()); mProgressHandler.onQueue(notificationInfo); } @Override protected void onStartIntent(@Nullable Intent intent) { // Set app name in the ongoing notification ApkQueueItem apkQueueItem = getQueueItem(intent); String appName; if (apkQueueItem != null) { String appLabel = apkQueueItem.getAppLabel(); appName = appLabel != null ? appLabel : apkQueueItem.getPackageName(); } else appName = null; CharSequence title; if (appName != null) { title = getString(R.string.installing_package, appName); } else { title = getString(R.string.install_in_progress); } mNotificationInfo.setTitle(title); mProgressHandler.onProgressStart(-1, 0, mNotificationInfo); } @Override public void onDestroy() { ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE); if (mProgressHandler != null) { mProgressHandler.onDetach(this); } CpuUtils.releaseWakeLock(mWakeLock); super.onDestroy(); } public void setOnInstallFinished(@Nullable OnInstallFinished onInstallFinished) { this.mOnInstallFinished = onInstallFinished; } @Nullable private ApkQueueItem getQueueItem(@Nullable Intent intent) { if (intent == null) { return null; } return IntentCompat.getUnwrappedParcelableExtra(intent, EXTRA_QUEUE_ITEM, ApkQueueItem.class); } private void finishInstallation(@NonNull String packageName, int status, @Nullable String appLabel, @Nullable String blockingPackage, @Nullable String statusMessage) { if (mOnInstallFinished != null) { ThreadUtils.postOnMainThread(() -> { if (mOnInstallFinished != null) { mOnInstallFinished.onFinished(packageName, status, blockingPackage, statusMessage); } }); } else { sendNotification(packageName, status, appLabel, blockingPackage, statusMessage); } } private void sendNotification(@NonNull String packageName, @PackageInstallerCompat.Status int status, @Nullable String appLabel, @Nullable String blockingPackage, @Nullable String statusMessage) { Intent intent = PackageManagerCompat.getLaunchIntentForPackage(packageName, UserHandleHidden.myUserId()); PendingIntent defaultAction = intent != null ? PendingIntentCompat.getActivity(this, 0, intent, PendingIntent.FLAG_ONE_SHOT, false) : null; String subject = getStringFromStatus(this, status, appLabel, blockingPackage); NotificationCompat.Style content = statusMessage != null ? new NotificationCompat.BigTextStyle() .bigText(subject + "\n\n" + statusMessage) : null; Object notificationInfo = new NotificationInfo() .setAutoCancel(true) .setTime(System.currentTimeMillis()) .setOperationName(getText(R.string.package_installer)) .setTitle(appLabel) .setBody(subject) .setStyle(content) .setDefaultAction(defaultAction); NotificationInfo progressNotificationInfo = (NotificationInfo) mProgressHandler.getLastMessage(); if (progressNotificationInfo != null) { progressNotificationInfo.setBody(getString(R.string.done)); } mProgressHandler.setProgressTextInterface(null); ThreadUtils.postOnMainThread(() -> mProgressHandler.onResult(notificationInfo)); } @NonNull public static String getStringFromStatus(@NonNull Context context, @PackageInstallerCompat.Status int status, @Nullable CharSequence appLabel, @Nullable String blockingPackage) { switch (status) { case STATUS_SUCCESS: return context.getString(R.string.package_name_is_installed_successfully, appLabel); case STATUS_FAILURE_ABORTED: return context.getString(R.string.installer_error_aborted); case STATUS_FAILURE_BLOCKED: String blocker = context.getString(R.string.installer_error_blocked_device); if (blockingPackage != null) { blocker = PackageUtils.getPackageLabel(context.getPackageManager(), blockingPackage); } return context.getString(R.string.installer_error_blocked, blocker); case STATUS_FAILURE_CONFLICT: return context.getString(R.string.installer_error_conflict); case STATUS_FAILURE_INCOMPATIBLE: return context.getString(R.string.installer_error_incompatible); case STATUS_FAILURE_INVALID: return context.getString(R.string.installer_error_bad_apks); case STATUS_FAILURE_STORAGE: return context.getString(R.string.installer_error_storage); case STATUS_FAILURE_SECURITY: return context.getString(R.string.installer_error_security); case STATUS_FAILURE_SESSION_CREATE: return context.getString(R.string.installer_error_session_create); case STATUS_FAILURE_SESSION_WRITE: return context.getString(R.string.installer_error_session_write); case STATUS_FAILURE_SESSION_COMMIT: return context.getString(R.string.installer_error_session_commit); case STATUS_FAILURE_SESSION_ABANDON: return context.getString(R.string.installer_error_session_abandon); case STATUS_FAILURE_INCOMPATIBLE_ROM: return context.getString(R.string.installer_error_lidl_rom); } return context.getString(R.string.installer_error_generic); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/PackageInstallerViewModel.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk.installer; import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.GET_SIGNING_CERTIFICATES; import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.GET_SIGNING_CERTIFICATES_APK; import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.MATCH_DISABLED_COMPONENTS; import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.MATCH_UNINSTALLED_PACKAGES; import android.annotation.SuppressLint; import android.app.Application; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; import android.os.UserHandleHidden; import androidx.annotation.AnyThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Set; import java.util.concurrent.Future; import io.github.muntashirakon.AppManager.apk.ApkFile; import io.github.muntashirakon.AppManager.apk.ApkSource; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.rules.compontents.ComponentUtils; import io.github.muntashirakon.AppManager.utils.PackageUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.io.IoUtils; public class PackageInstallerViewModel extends AndroidViewModel { private final PackageManager mPm; private PackageInfo mNewPackageInfo; private PackageInfo mInstalledPackageInfo; private ApkSource mApkSource; private ApkFile mApkFile; private String mPackageName; private String mAppLabel; private Drawable mAppIcon; private boolean mIsSignatureDifferent = false; private int mTrackerCount; @Nullable private Future mPackageInfoResult; private final MutableLiveData mPackageInfoLiveData = new MutableLiveData<>(); private final MutableLiveData mPackageUninstalledLiveData = new MutableLiveData<>(); private final Set mSelectedSplits = new HashSet<>(); public PackageInstallerViewModel(@NonNull Application application) { super(application); mPm = application.getPackageManager(); } @Override protected void onCleared() { IoUtils.closeQuietly(mApkFile); if (mPackageInfoResult != null) { mPackageInfoResult.cancel(true); } super.onCleared(); } public LiveData packageInfoLiveData() { return mPackageInfoLiveData; } public LiveData packageUninstalledLiveData() { return mPackageUninstalledLiveData; } @AnyThread public void getPackageInfo(ApkQueueItem apkQueueItem) { if (mPackageInfoResult != null) { mPackageInfoResult.cancel(true); } mSelectedSplits.clear(); mPackageInfoResult = ThreadUtils.postOnBackgroundThread(() -> { try { // Three possibilities: 1. Install-existing, 2. ApkFile, 3. Uri if (apkQueueItem.isInstallExisting()) { if (apkQueueItem.getPackageName() == null) { throw new IllegalArgumentException("Package name not set for install-existing."); } getExistingPackageInfoInternal(apkQueueItem.getPackageName()); } else if (apkQueueItem.getApkSource() != null) { mApkSource = apkQueueItem.getApkSource(); getPackageInfoInternal(); } else { throw new IllegalArgumentException("Invalid queue item."); } apkQueueItem.setApkSource(mApkSource); apkQueueItem.setPackageName(mPackageName); apkQueueItem.setAppLabel(mAppLabel); } catch (Throwable th) { Log.e("PIVM", "Couldn't fetch package info", th); mPackageInfoLiveData.postValue(null); } }); } public void uninstallPackage() { ThreadUtils.postOnBackgroundThread(() -> { PackageInstallerCompat installer = PackageInstallerCompat.getNewInstance(); installer.setAppLabel(mAppLabel); mPackageUninstalledLiveData.postValue(installer.uninstall(mPackageName, UserHandleHidden.USER_ALL, false)); }); } public PackageInfo getNewPackageInfo() { return mNewPackageInfo; } @Nullable public PackageInfo getInstalledPackageInfo() { return mInstalledPackageInfo; } public String getAppLabel() { return mAppLabel; } public Drawable getAppIcon() { return mAppIcon; } public String getPackageName() { return mPackageName; } public ApkFile getApkFile() { return mApkFile; } public ApkSource getApkSource() { return mApkSource; } public int getTrackerCount() { return mTrackerCount; } public boolean isSignatureDifferent() { return mIsSignatureDifferent; } public Set getSelectedSplits() { return mSelectedSplits; } @NonNull public ArrayList getSelectedSplitsForInstallation() { if (mApkFile.isSplit()) { if (mSelectedSplits.isEmpty()) { throw new IllegalArgumentException("No splits selected."); } return new ArrayList<>(mSelectedSplits); } return new ArrayList<>(Collections.singletonList(mApkFile.getBaseEntry().id)); } private void getPackageInfoInternal() throws PackageManager.NameNotFoundException, IOException, ApkFile.ApkFileException { mApkFile = mApkSource.resolve(); mNewPackageInfo = loadNewPackageInfo(); mPackageName = mNewPackageInfo.packageName; if (ThreadUtils.isInterrupted()) { return; } try { mInstalledPackageInfo = loadInstalledPackageInfo(mPackageName); if (ThreadUtils.isInterrupted()) { return; } } catch (PackageManager.NameNotFoundException ignore) { } mAppLabel = mPm.getApplicationLabel(mNewPackageInfo.applicationInfo).toString(); mAppIcon = mPm.getApplicationIcon(mNewPackageInfo.applicationInfo); mTrackerCount = ComponentUtils.getTrackerComponentsCountForPackage(mNewPackageInfo); if (ThreadUtils.isInterrupted()) { return; } if (mNewPackageInfo != null && mInstalledPackageInfo != null) { mIsSignatureDifferent = PackageUtils.isSignatureDifferent(mNewPackageInfo, mInstalledPackageInfo); } mPackageInfoLiveData.postValue(mNewPackageInfo); } private void getExistingPackageInfoInternal(@NonNull String packageName) throws PackageManager.NameNotFoundException, IOException, ApkFile.ApkFileException { mPackageName = packageName; mInstalledPackageInfo = loadInstalledPackageInfo(packageName); mApkSource = ApkSource.getApkSource(mInstalledPackageInfo.applicationInfo); mApkFile = mApkSource.resolve(); mNewPackageInfo = loadNewPackageInfo(); mAppLabel = mPm.getApplicationLabel(mNewPackageInfo.applicationInfo).toString(); mAppIcon = mPm.getApplicationIcon(mNewPackageInfo.applicationInfo); mTrackerCount = ComponentUtils.getTrackerComponentsCountForPackage(mNewPackageInfo); if (mNewPackageInfo != null && mInstalledPackageInfo != null) { mIsSignatureDifferent = PackageUtils.isSignatureDifferent(mNewPackageInfo, mInstalledPackageInfo); } mPackageInfoLiveData.postValue(mNewPackageInfo); } @WorkerThread @NonNull private PackageInfo loadNewPackageInfo() throws PackageManager.NameNotFoundException, IOException { String apkPath = mApkFile.getBaseEntry().getFile(false).getAbsolutePath(); int flags = PackageManager.GET_PERMISSIONS | PackageManager.GET_ACTIVITIES | PackageManager.GET_RECEIVERS | PackageManager.GET_PROVIDERS | PackageManager.GET_SERVICES | MATCH_DISABLED_COMPONENTS | GET_SIGNING_CERTIFICATES_APK | PackageManager.GET_CONFIGURATIONS | PackageManager.GET_SHARED_LIBRARY_FILES; PackageInfo packageInfo = mPm.getPackageArchiveInfo(apkPath, flags); if (packageInfo == null) { // Previous method could return null if the APK isn't signed. So, try without it. packageInfo = mPm.getPackageArchiveInfo(apkPath, flags & ~GET_SIGNING_CERTIFICATES_APK); } if (packageInfo == null) { throw new PackageManager.NameNotFoundException("Package cannot be parsed."); } packageInfo.applicationInfo.sourceDir = apkPath; packageInfo.applicationInfo.publicSourceDir = apkPath; return packageInfo; } @WorkerThread @NonNull private PackageInfo loadInstalledPackageInfo(String packageName) throws PackageManager.NameNotFoundException { @SuppressLint("WrongConstant") PackageInfo packageInfo = mPm.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS | PackageManager.GET_ACTIVITIES | PackageManager.GET_RECEIVERS | PackageManager.GET_PROVIDERS | PackageManager.GET_SERVICES | MATCH_DISABLED_COMPONENTS | GET_SIGNING_CERTIFICATES | MATCH_UNINSTALLED_PACKAGES | PackageManager.GET_CONFIGURATIONS | PackageManager.GET_SHARED_LIBRARY_FILES); if (packageInfo == null) throw new PackageManager.NameNotFoundException("Package not found."); return packageInfo; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/SupportedAppStores.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk.installer; import androidx.annotation.NonNull; import java.util.HashMap; public final class SupportedAppStores { public static HashMap SUPPORTED_APP_STORES = new HashMap() {{ // Sorted by app label put("com.aurora.store", "Aurora Store"); put("com.looker.droidify", "Droid-ify"); put("org.fdroid.fdroid", "F-Droid"); put("org.fdroid.basic", "F-Droid Basic"); put("eu.bubu1.fdroidclassic", "F-Droid Classic"); put("com.machiav3lli.fdroid", "Neo Store"); }}; public static boolean isAppStoreSupported(@NonNull String packageName) { return SUPPORTED_APP_STORES.containsKey(packageName); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/list/AppListItem.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk.list; import android.graphics.Bitmap; public class AppListItem { public final String packageName; private Bitmap mIcon; private String mPackageLabel; private long mVersionCode; private String mVersionName; private int mMinSdk; private int mTargetSdk; private String mSignatureSha256; private long mFirstInstallTime; private long mLastUpdateTime; private String mInstallerPackageName; private String mInstallerPackageLabel; public AppListItem(String packageName) { this.packageName = packageName; } public Bitmap getIcon() { return mIcon; } public void setIcon(Bitmap icon) { mIcon = icon; } public String getPackageLabel() { return mPackageLabel; } public void setPackageLabel(String packageLabel) { mPackageLabel = packageLabel; } public long getVersionCode() { return mVersionCode; } public void setVersionCode(long versionCode) { mVersionCode = versionCode; } public String getVersionName() { return mVersionName; } public void setVersionName(String versionName) { mVersionName = versionName; } public int getMinSdk() { return mMinSdk; } public void setMinSdk(int minSdk) { mMinSdk = minSdk; } public int getTargetSdk() { return mTargetSdk; } public void setTargetSdk(int targetSdk) { mTargetSdk = targetSdk; } public String getSignatureSha256() { return mSignatureSha256; } public void setSignatureSha256(String signatureSha256) { mSignatureSha256 = signatureSha256; } public long getFirstInstallTime() { return mFirstInstallTime; } public void setFirstInstallTime(long firstInstallTime) { mFirstInstallTime = firstInstallTime; } public long getLastUpdateTime() { return mLastUpdateTime; } public void setLastUpdateTime(long lastUpdateTime) { mLastUpdateTime = lastUpdateTime; } public String getInstallerPackageName() { return mInstallerPackageName; } public void setInstallerPackageName(String installerPackageName) { mInstallerPackageName = installerPackageName; } public String getInstallerPackageLabel() { return mInstallerPackageLabel; } public void setInstallerPackageLabel(String installerPackageLabel) { mInstallerPackageLabel = installerPackageLabel; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/list/ListExporter.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk.list; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Build; import android.os.UserHandleHidden; import android.text.TextUtils; import android.util.Xml; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.core.content.pm.PackageInfoCompat; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.xmlpull.v1.XmlSerializer; import java.io.IOException; import java.io.Writer; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; import io.github.muntashirakon.AppManager.compat.PackageManagerCompat; import io.github.muntashirakon.AppManager.utils.DateUtils; import io.github.muntashirakon.AppManager.utils.ExUtils; import io.github.muntashirakon.AppManager.utils.PackageUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.csv.CsvWriter; public final class ListExporter { public static final int EXPORT_TYPE_CSV = 0; public static final int EXPORT_TYPE_JSON = 1; public static final int EXPORT_TYPE_XML = 2; public static final int EXPORT_TYPE_MARKDOWN = 3; @IntDef({EXPORT_TYPE_CSV, EXPORT_TYPE_JSON, EXPORT_TYPE_XML, EXPORT_TYPE_MARKDOWN}) @Retention(RetentionPolicy.SOURCE) public @interface ExportType { } public static void export(@NonNull Context context, @NonNull Writer writer, @ExportType int exportType, @NonNull List packageInfoList) throws IOException { List appListItems = getAppListItems(context, packageInfoList); switch (exportType) { case EXPORT_TYPE_CSV: exportCsv(writer, appListItems); return; case EXPORT_TYPE_JSON: try { exportJson(writer, appListItems); } catch (JSONException e) { ExUtils.rethrowAsIOException(e); } return; case EXPORT_TYPE_XML: exportXml(writer, appListItems); return; case EXPORT_TYPE_MARKDOWN: exportMarkdown(context, writer, appListItems); return; } throw new IllegalArgumentException("Invalid export type: " + exportType); } private static void exportXml(@NonNull Writer writer, @NonNull List appListItems) throws IOException { XmlSerializer xmlSerializer = Xml.newSerializer(); xmlSerializer.setOutput(writer); xmlSerializer.startDocument("UTF-8", true); xmlSerializer.docdecl("packages SYSTEM \"https://raw.githubusercontent.com/MuntashirAkon/AppManager/master/schema/packages.dtd\""); xmlSerializer.startTag("", "packages"); xmlSerializer.attribute("", "version", String.valueOf(1)); for (AppListItem appListItem : appListItems) { xmlSerializer.startTag("", "package"); xmlSerializer.attribute("", "name", appListItem.packageName); xmlSerializer.attribute("", "label", appListItem.getPackageLabel()); xmlSerializer.attribute("", "versionCode", String.valueOf(appListItem.getVersionCode())); xmlSerializer.attribute("", "versionName", appListItem.getVersionName()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { xmlSerializer.attribute("", "minSdk", String.valueOf(appListItem.getMinSdk())); } xmlSerializer.attribute("", "targetSdk", String.valueOf(appListItem.getTargetSdk())); xmlSerializer.attribute("", "signature", appListItem.getSignatureSha256()); xmlSerializer.attribute("", "firstInstallTime", String.valueOf(appListItem.getFirstInstallTime())); xmlSerializer.attribute("", "lastUpdateTime", String.valueOf(appListItem.getLastUpdateTime())); if (appListItem.getInstallerPackageName() != null) { xmlSerializer.attribute("", "installerPackageName", appListItem.getInstallerPackageName()); if (appListItem.getInstallerPackageLabel() != null) { xmlSerializer.attribute("", "installerPackageLabel", appListItem.getInstallerPackageLabel()); } } xmlSerializer.endTag("", "package"); } xmlSerializer.endTag("", "packages"); xmlSerializer.endDocument(); xmlSerializer.flush(); } private static void exportCsv(@NonNull Writer writer, @NonNull List appListItems) throws IOException { CsvWriter csvWriter = new CsvWriter(writer); // Add header if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { csvWriter.addLine(new String[]{"name", "label", "versionCode", "versionName", "minSdk", "targetSdk", "signature", "firstInstallTime", "lastUpdateTime", "installerPackageName", "installerPackageLabel"}); } else { csvWriter.addLine(new String[]{"name", "label", "versionCode", "versionName", "targetSdk", "signature", "firstInstallTime", "lastUpdateTime", "installerPackageName", "installerPackageLabel"}); } for (AppListItem item : appListItems) { String installerPackage = item.getInstallerPackageName() != null ? item.getInstallerPackageName() : ""; String installerLabel = item.getInstallerPackageLabel() != null ? item.getInstallerPackageLabel() : ""; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { csvWriter.addLine(new String[]{item.packageName, item.getPackageLabel(), String.valueOf(item.getVersionCode()), item.getVersionName(), String.valueOf(item.getMinSdk()), String.valueOf(item.getTargetSdk()), item.getSignatureSha256(), String.valueOf(item.getFirstInstallTime()), String.valueOf(item.getLastUpdateTime()), installerPackage, installerLabel}); } else { csvWriter.addLine(new String[]{item.packageName, item.getPackageLabel(), String.valueOf(item.getVersionCode()), item.getVersionName(), String.valueOf(item.getTargetSdk()), item.getSignatureSha256(), String.valueOf(item.getFirstInstallTime()), String.valueOf(item.getLastUpdateTime()), installerPackage, installerLabel}); } } } private static void exportJson(@NonNull Writer writer, @NonNull List appListItems) throws JSONException, IOException { // Should reflect packages.dtd JSONArray array = new JSONArray(); for (AppListItem item : appListItems) { JSONObject object = new JSONObject(); object.put("name", item.packageName); object.put("label", item.getPackageLabel()); object.put("versionCode", item.getVersionCode()); object.put("versionName", item.getVersionName()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { object.put("minSdk", item.getMinSdk()); } object.put("targetSdk", item.getTargetSdk()); object.put("signature", item.getSignatureSha256()); object.put("firstInstallTime", item.getFirstInstallTime()); object.put("lastUpdateTime", item.getLastUpdateTime()); if (item.getInstallerPackageName() != null) { object.put("installerPackageName", item.getInstallerPackageName()); if (item.getInstallerPackageLabel() != null) { object.put("installerPackageLabel", item.getInstallerPackageLabel()); } } array.put(object); } writer.write(array.toString(4)); } private static void exportMarkdown(@NonNull Context context, @NonNull Writer writer, @NonNull List appListItems) throws IOException { writer.write("# Package Info\n\n"); for (AppListItem appListItem : appListItems) { writer.append("## ").append(appListItem.getPackageLabel()).append("\n\n") .append("**Package name:** ").append(appListItem.packageName).append("\n") .append("**Version:** ").append(appListItem.getVersionName()).append(" (") .append(String.valueOf(appListItem.getVersionCode())).append(")\n"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { writer.append("**Min SDK:** ").append(String.valueOf(appListItem.getMinSdk())) .append(", "); } writer.append("**Target SDK:** ").append(String.valueOf(appListItem.getTargetSdk())) .append("\n") .append("**Date installed:** ") .append(DateUtils.formatDateTime(context, appListItem.getFirstInstallTime())) .append(", **Date updated:** ") .append(DateUtils.formatDateTime(context, appListItem.getLastUpdateTime())) .append("\n"); if (appListItem.getInstallerPackageName() != null) { writer.append("**Installer:** "); if (appListItem.getInstallerPackageLabel() != null) { writer.append(appListItem.getInstallerPackageLabel()).append(" ("); } writer.append(appListItem.getInstallerPackageName()); if (appListItem.getInstallerPackageLabel() != null) { writer.append(")"); } } writer.append("\n\n"); } } @NonNull private static List getAppListItems(@NonNull Context context, @NonNull List packageInfoList) { List appListItems = new ArrayList<>(packageInfoList.size()); PackageManager pm = context.getPackageManager(); for (PackageInfo packageInfo : packageInfoList) { ApplicationInfo applicationInfo = packageInfo.applicationInfo; AppListItem item = new AppListItem(packageInfo.packageName); appListItems.add(item); item.setIcon(UIUtils.getBitmapFromDrawable(applicationInfo.loadIcon(pm))); item.setPackageLabel(applicationInfo.loadLabel(pm).toString()); item.setVersionCode(PackageInfoCompat.getLongVersionCode(packageInfo)); item.setVersionName(packageInfo.versionName); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { item.setMinSdk(applicationInfo.minSdkVersion); } item.setTargetSdk(applicationInfo.targetSdkVersion); String[] signatureSha256 = PackageUtils.getSigningCertSha256Checksum(packageInfo, false); item.setSignatureSha256(TextUtils.join(",", signatureSha256)); item.setFirstInstallTime(packageInfo.firstInstallTime); item.setLastUpdateTime(packageInfo.lastUpdateTime); String installerPackageName = PackageManagerCompat.getInstallerPackageName( packageInfo.packageName, UserHandleHidden.getUserId(applicationInfo.uid)); if (installerPackageName != null) { item.setInstallerPackageName(installerPackageName); String installerPackageLabel; try { installerPackageLabel = pm.getApplicationInfo(installerPackageName, 0) .loadLabel(pm).toString(); if (!installerPackageLabel.equals(installerPackageName)) { item.setInstallerPackageLabel(installerPackageLabel); } } catch (PackageManager.NameNotFoundException ignore) { } } } return appListItems; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/parser/AndroidBinXmlDecoder.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk.parser; import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; import static org.xmlpull.v1.XmlPullParser.END_TAG; import static org.xmlpull.v1.XmlPullParser.START_DOCUMENT; import static org.xmlpull.v1.XmlPullParser.START_TAG; import android.text.TextUtils; import androidx.annotation.NonNull; import com.reandroid.apk.AndroidFrameworks; import com.reandroid.arsc.chunk.PackageBlock; import com.reandroid.arsc.chunk.xml.ResXmlDocument; import com.reandroid.arsc.chunk.xml.ResXmlPullParser; import com.reandroid.arsc.io.BlockReader; import org.xmlpull.v1.XmlPullParserException; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintStream; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.StandardCharsets; import io.github.muntashirakon.AppManager.utils.IntegerUtils; import io.github.muntashirakon.io.IoUtils; public class AndroidBinXmlDecoder { public static boolean isBinaryXml(@NonNull ByteBuffer buffer) { buffer.order(ByteOrder.LITTLE_ENDIAN); buffer.mark(); int version = IntegerUtils.getUInt16(buffer); int header = IntegerUtils.getUInt16(buffer); buffer.reset(); // 0x0000 is NULL header. The only example of application using a NULL header is NP Manager return (version == 0x0003 || version == 0x0000) && header == 0x0008; } @NonNull public static String decode(@NonNull byte[] data) throws IOException { return decode(ByteBuffer.wrap(data)); } @NonNull public static String decode(@NonNull InputStream is) throws IOException { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); byte[] buf = new byte[IoUtils.DEFAULT_BUFFER_SIZE]; int n; while (-1 != (n = is.read(buf))) { buffer.write(buf, 0, n); } return decode(buffer.toByteArray()); } @NonNull public static String decode(@NonNull ByteBuffer byteBuffer) throws IOException { try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) { decode(byteBuffer, bos); byte[] bs = bos.toByteArray(); return new String(bs, StandardCharsets.UTF_8); } } public static void decode(@NonNull ByteBuffer byteBuffer, @NonNull OutputStream os) throws IOException { try (BlockReader reader = new BlockReader(byteBuffer.array()); PrintStream out = new PrintStream(os)) { ResXmlDocument resXmlDocument = new ResXmlDocument(); resXmlDocument.readBytes(reader); resXmlDocument.setPackageBlock(getFrameworkPackageBlock()); try (ResXmlPullParser parser = new ResXmlPullParser(resXmlDocument)) { StringBuilder indent = new StringBuilder(10); final String indentStep = " "; out.println(""); XML_BUILDER: while (true) { int type = parser.next(); switch (type) { case START_TAG: { out.printf("%s<%s%s", indent, getNamespacePrefix(parser.getPrefix()), parser.getName()); indent.append(indentStep); int nsStart = parser.getNamespaceCount(parser.getDepth() - 1); int nsEnd = parser.getNamespaceCount(parser.getDepth()); for (int i = nsStart; i < nsEnd; ++i) { out.printf("\n%sxmlns:%s=\"%s\"", indent, parser.getNamespacePrefix(i), parser.getNamespaceUri(i)); } for (int i = 0; i != parser.getAttributeCount(); ++i) { out.printf("\n%s%s%s=\"%s\"", indent, getNamespacePrefix(parser.getAttributePrefix(i)), parser.getAttributeName(i), parser.getAttributeValue(i)); } out.println(">"); break; } case END_TAG: { indent.setLength(indent.length() - indentStep.length()); out.printf("%s%n", indent, getNamespacePrefix(parser.getPrefix()), parser.getName()); break; } case END_DOCUMENT: break XML_BUILDER; case START_DOCUMENT: // Unreachable statement break; } } } } catch (XmlPullParserException e) { throw new IOException(e); } } @NonNull private static String getNamespacePrefix(String prefix) { if (TextUtils.isEmpty(prefix)) { return ""; } return prefix + ":"; } @NonNull public static PackageBlock getFrameworkPackageBlock() { if (sFrameworkPackageBlock != null) { return sFrameworkPackageBlock; } sFrameworkPackageBlock = AndroidFrameworks.getLatest().getTableBlock().getAllPackages().next(); return sFrameworkPackageBlock; } private static PackageBlock sFrameworkPackageBlock; } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/parser/AndroidBinXmlEncoder.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk.parser; import androidx.annotation.NonNull; import com.reandroid.apk.xmlencoder.XMLEncodeSource; import com.reandroid.xml.source.XMLFileParserSource; import com.reandroid.xml.source.XMLParserSource; import com.reandroid.xml.source.XMLStringParserSource; import java.io.File; import java.io.IOException; public class AndroidBinXmlEncoder { @NonNull public static byte[] encodeFile(@NonNull File file) throws IOException { return encode(new XMLFileParserSource(file.getName(), file)); } @NonNull public static byte[] encodeString(@NonNull String xml) throws IOException { return encode(new XMLStringParserSource("String.xml", xml)); } @NonNull private static byte[] encode(@NonNull XMLParserSource xmlSource) throws IOException { XMLEncodeSource xmlEncodeSource = new XMLEncodeSource(AndroidBinXmlDecoder.getFrameworkPackageBlock(), xmlSource); return xmlEncodeSource.getBytes(); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/parser/ManifestComponent.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk.parser; import android.content.ComponentName; import java.util.ArrayList; import java.util.List; public class ManifestComponent { public final ComponentName cn; public final List intentFilters; public ManifestComponent(ComponentName cn) { this.cn = cn; intentFilters = new ArrayList<>(); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/parser/ManifestIntentFilter.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk.parser; import androidx.collection.ArraySet; import java.util.ArrayList; import java.util.List; import java.util.Set; public class ManifestIntentFilter { public final Set actions = new ArraySet<>(); public final Set categories = new ArraySet<>(); public final List data = new ArrayList<>(); public int priority; public static class ManifestData { public String scheme; public String host; public String port; public String path; public String pathPattern; public String pathPrefix; public String pathSuffix; public String pathAdvancedPattern; public String mimeType; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/parser/ManifestParser.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk.parser; import android.content.ComponentName; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.reandroid.arsc.chunk.xml.ResXmlAttribute; import com.reandroid.arsc.chunk.xml.ResXmlDocument; import com.reandroid.arsc.chunk.xml.ResXmlElement; import com.reandroid.arsc.io.BlockReader; import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Objects; import io.github.muntashirakon.AppManager.logs.Log; public class ManifestParser { public static final String TAG = ManifestParser.class.getSimpleName(); // manifest private static final String TAG_MANIFEST = "manifest"; private static final String ATTR_MANIFEST_PACKAGE = "package"; // manifest -> application private static final String TAG_APPLICATION = "application"; // manifest -> application -> activity|activity-alias|service|receiver|provider private static final String TAG_ACTIVITY = "activity"; private static final String TAG_ACTIVITY_ALIAS = "activity-alias"; private static final String TAG_SERVICE = "service"; private static final String TAG_RECEIVER = "receiver"; private static final String TAG_PROVIDER = "provider"; private static final String ATTR_NAME = "name"; // android:name // manifest -> application -> (component) -> intent-filter private static final String TAG_INTENT_FILTER = "intent-filter"; private static final String ATTR_PRIORITY = "priority"; // android:priority // manifest -> application -> (component) -> intent-filter -> action|category|data private static final String TAG_ACTION = "action"; private static final String TAG_CATEGORY = "category"; private static final String TAG_DATA = "data"; private final @NonNull ByteBuffer mManifestBytes; private String mPackageName; public ManifestParser(@NonNull byte[] manifestBytes) { this(ByteBuffer.wrap(manifestBytes)); } public ManifestParser(@NonNull ByteBuffer manifestBytes) { mManifestBytes = manifestBytes; } public List parseComponents() throws IOException { try (BlockReader reader = new BlockReader(mManifestBytes.array())) { ResXmlDocument xmlBlock = new ResXmlDocument(); xmlBlock.readBytes(reader); xmlBlock.setPackageBlock(AndroidBinXmlDecoder.getFrameworkPackageBlock()); ResXmlElement resManifestElement = xmlBlock.getDocumentElement(); // manifest if (!TAG_MANIFEST.equals(resManifestElement.getName())) { throw new IOException("\"manifest\" tag not found."); } String packageName = getAttributeValue(resManifestElement, ATTR_MANIFEST_PACKAGE); if (packageName == null) { throw new IOException("\"manifest\" does not have required attribute \"package\"."); } mPackageName = packageName; // manifest -> application ResXmlElement resApplicationElement = null; Iterator resXmlElementIt = resManifestElement.getElements(TAG_APPLICATION); if (resXmlElementIt.hasNext()) { resApplicationElement = resXmlElementIt.next(); } if (resXmlElementIt.hasNext()) { throw new IOException("\"manifest\" has duplicate \"application\" tags."); } if (resApplicationElement == null) { Log.i(TAG, "package %s does not have \"application\" tag.", mPackageName); return Collections.emptyList(); } // manifest -> application -> component List componentIfList = new ArrayList<>(resApplicationElement.getElementsCount()); String tagName; resXmlElementIt = resApplicationElement.getElements(); while (resXmlElementIt.hasNext()) { ResXmlElement elem = resXmlElementIt.next(); tagName = elem.getName(); if (tagName != null) { switch (tagName) { case TAG_ACTIVITY: case TAG_ACTIVITY_ALIAS: case TAG_SERVICE: case TAG_RECEIVER: case TAG_PROVIDER: componentIfList.add(parseComponentInfo(elem)); break; } } } return componentIfList; } } @NonNull private ManifestComponent parseComponentInfo(@NonNull ResXmlElement componentElement) throws IOException { String componentName = getAttributeValue(componentElement, ATTR_NAME); if (componentName == null) { throw new IOException("\"" + componentElement.getName() + "\" does not have required attribute \"android:name\"."); } ManifestComponent componentIf = new ManifestComponent(new ComponentName(mPackageName, componentName)); // manifest -> application -> component -> intent-filter Iterator resXmlElementIt = componentElement.getElements(TAG_INTENT_FILTER); while (resXmlElementIt.hasNext()) { ResXmlElement elem = resXmlElementIt.next(); componentIf.intentFilters.add(parseIntentFilter(elem)); } return componentIf; } @NonNull private ManifestIntentFilter parseIntentFilter(@NonNull ResXmlElement intentFilterElement) { ManifestIntentFilter intentFilter = new ManifestIntentFilter(); String priorityString = getAttributeValue(intentFilterElement, ATTR_PRIORITY); if (priorityString != null) { intentFilter.priority = Integer.parseInt(priorityString); } // manifest -> application -> component -> intent-filter -> action|category|data Iterator resXmlElementIt = intentFilterElement.getElements(); String tagName; while (resXmlElementIt.hasNext()) { ResXmlElement elem = resXmlElementIt.next(); tagName = elem.getName(); if (tagName != null) { switch (tagName) { case TAG_ACTION: intentFilter.actions.add(Objects.requireNonNull(getAttributeValue(elem, ATTR_NAME))); break; case TAG_CATEGORY: intentFilter.categories.add(Objects.requireNonNull(getAttributeValue(elem, ATTR_NAME))); break; case TAG_DATA: intentFilter.data.add(parseData(elem)); break; } } } return intentFilter; } @NonNull private ManifestIntentFilter.ManifestData parseData(@NonNull ResXmlElement dataElement) { ManifestIntentFilter.ManifestData data = new ManifestIntentFilter.ManifestData(); ResXmlAttribute attribute; for (int i = 0; i < dataElement.getAttributeCount(); ++i) { attribute = dataElement.getAttributeAt(i); if (attribute.equalsName("scheme")) { data.scheme = attribute.getValueAsString(); } else if (attribute.equalsName("host")) { data.host = attribute.getValueAsString(); } else if (attribute.equalsName("port")) { data.port = attribute.getValueAsString(); } else if (attribute.equalsName("path")) { data.path = attribute.getValueAsString(); } else if (attribute.equalsName("pathPrefix")) { data.pathPrefix = attribute.getValueAsString(); } else if (attribute.equalsName("pathSuffix")) { data.pathSuffix = attribute.getValueAsString(); } else if (attribute.equalsName("pathPattern")) { data.pathPattern = attribute.getValueAsString(); } else if (attribute.equalsName("pathAdvancedPattern")) { data.pathAdvancedPattern = attribute.getValueAsString(); } else if (attribute.equalsName("mimeType")) { data.mimeType = attribute.getValueAsString(); } else { Log.i(TAG, "Unknown intent-filter > data attribute %s", attribute.getName()); } } return data; } @Nullable private String getAttributeValue(@NonNull ResXmlElement element, @NonNull String attrName) { ResXmlAttribute attribute; for (int i = 0; i < element.getAttributeCount(); ++i) { attribute = element.getAttributeAt(i); if (attribute.equalsName(attrName)) { return attribute.getValueAsString(); } } return null; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/signing/SigSchemes.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk.signing; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import java.util.ArrayList; import java.util.List; public class SigSchemes { @IntDef(flag = true, value = { SIG_SCHEME_V1, SIG_SCHEME_V2, SIG_SCHEME_V3, SIG_SCHEME_V4, }) public @interface SignatureScheme { } public static final int SIG_SCHEME_V1 = 1 << 0; public static final int SIG_SCHEME_V2 = 1 << 1; public static final int SIG_SCHEME_V3 = 1 << 2; public static final int SIG_SCHEME_V4 = 1 << 3; public static final int TOTAL_SIG_SCHEME = 4; public static final int DEFAULT_SCHEMES = SIG_SCHEME_V1 | SIG_SCHEME_V2; @SignatureScheme private int mFlags; public SigSchemes(@SignatureScheme int flags) { this.mFlags = flags; } public boolean isEmpty() { return mFlags == 0; } public int getFlags() { return mFlags; } public void setFlags(int flags) { this.mFlags = flags; } @NonNull public List getAllItems() { List allItems = new ArrayList<>(); for (int i = 0; i < TOTAL_SIG_SCHEME; ++i) { allItems.add(1 << i); } return allItems; } public boolean v1SchemeEnabled() { return (mFlags & SIG_SCHEME_V1) != 0; } public boolean v2SchemeEnabled() { return (mFlags & SIG_SCHEME_V2) != 0; } public boolean v3SchemeEnabled() { return (mFlags & SIG_SCHEME_V3) != 0; } public boolean v4SchemeEnabled() { return (mFlags & SIG_SCHEME_V4) != 0; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/signing/Signer.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk.signing; import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.apksig.ApkSigner; import com.android.apksig.ApkVerifier; import java.io.File; import java.security.KeyStoreException; import java.security.Principal; import java.security.PrivateKey; import java.security.PublicKey; import java.security.SignatureException; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; import java.security.interfaces.DSAKey; import java.security.interfaces.DSAParams; import java.security.interfaces.ECKey; import java.security.interfaces.RSAKey; import java.util.Collections; import java.util.List; import aosp.libcore.util.HexEncoding; import io.github.muntashirakon.AppManager.crypto.ks.KeyPair; import io.github.muntashirakon.AppManager.crypto.ks.KeyStoreManager; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.utils.DigestUtils; import io.github.muntashirakon.AppManager.utils.ExUtils; public class Signer { public static final String TAG = "Signer"; public static final String SIGNING_KEY_ALIAS = "signing_key"; public static boolean canSign() { try { // In order to sign an APK, a signing key must be inserted return KeyStoreManager.getInstance().containsKey(Signer.SIGNING_KEY_ALIAS); } catch (Exception e) { // Signing not configured return false; } } @NonNull public static Signer getInstance(SigSchemes sigSchemes) throws SignatureException { try { KeyStoreManager manager = KeyStoreManager.getInstance(); KeyPair signingKey = manager.getKeyPair(SIGNING_KEY_ALIAS); if (signingKey == null) { throw new KeyStoreException("Alias " + SIGNING_KEY_ALIAS + " does not exist in KeyStore."); } return new Signer(sigSchemes, signingKey.getPrivateKey(), (X509Certificate) signingKey.getCertificate()); } catch (Exception e) { throw new SignatureException(e); } } @NonNull private final PrivateKey mPrivateKey; @NonNull private final X509Certificate mCertificate; @NonNull private final SigSchemes mSigSchemes; @Nullable private File mIdsigFile; private Signer(@NonNull SigSchemes sigSchemes, @NonNull PrivateKey privateKey, @NonNull X509Certificate certificate) { mSigSchemes = sigSchemes; mPrivateKey = privateKey; mCertificate = certificate; } public boolean isV4SchemeEnabled() { return mSigSchemes.v4SchemeEnabled(); } public void setIdsigFile(@Nullable File idsigFile) { mIdsigFile = idsigFile; } public boolean sign(File in, File out, int minSdk, boolean alignFileSize) { ApkSigner.SignerConfig signerConfig = new ApkSigner.SignerConfig.Builder("CERT", mPrivateKey, Collections.singletonList(mCertificate)).build(); ApkSigner.Builder builder = new ApkSigner.Builder(Collections.singletonList(signerConfig)); builder.setInputApk(in); builder.setOutputApk(out); builder.setCreatedBy("AppManager"); builder.setAlignFileSize(alignFileSize); if (minSdk != -1) builder.setMinSdkVersion(minSdk); builder.setV1SigningEnabled(mSigSchemes.v1SchemeEnabled()); builder.setV2SigningEnabled(mSigSchemes.v2SchemeEnabled()); builder.setV3SigningEnabled(mSigSchemes.v3SchemeEnabled()); if (mSigSchemes.v4SchemeEnabled()) { if (mIdsigFile == null) { throw new RuntimeException("idsig file is mandatory for v4 signature scheme."); } builder.setV4SigningEnabled(true); builder.setV4SignatureOutputFile(mIdsigFile); } else { builder.setV4SigningEnabled(false); } ApkSigner signer = builder.build(); Log.i(TAG, "SignApk: %s", in); try { if (alignFileSize && !ZipAlign.verify(in, ZipAlign.ALIGNMENT_4, true)) { ZipAlign.align(in, ZipAlign.ALIGNMENT_4, true); } signer.sign(); Log.i(TAG, "The signature is complete and the output file is %s", out); return true; } catch (Exception e) { Log.w(TAG, e); return false; } } public static boolean verify(@NonNull SigSchemes sigSchemes, @NonNull File apk, @Nullable File idsig) { ApkVerifier.Builder builder = new ApkVerifier.Builder(apk) .setMaxCheckedPlatformVersion(Build.VERSION.SDK_INT); if (sigSchemes.v4SchemeEnabled()) { if (idsig == null) { throw new RuntimeException("idsig file is mandatory for v4 signature scheme."); } builder.setV4SignatureFile(idsig); } ApkVerifier verifier = builder.build(); try { ApkVerifier.Result result = verifier.verify(); Log.i(TAG, "%s", apk); boolean isVerify = result.isVerified(); if (isVerify) { if (sigSchemes.v1SchemeEnabled() && result.isVerifiedUsingV1Scheme()) { Log.i(TAG, "V1 signature verified."); } else Log.w(TAG, "V1 signature verification failed/disabled."); if (sigSchemes.v2SchemeEnabled() && result.isVerifiedUsingV2Scheme()) { Log.i(TAG, "V2 signature verified."); } else Log.w(TAG, "V2 signature verification failed/disabled."); if (sigSchemes.v3SchemeEnabled()) { if (result.isVerifiedUsingV3Scheme()) { Log.i(TAG, "V3 signature verified."); } else Log.w(TAG, "V3 signature verification failed."); if (result.isVerifiedUsingV31Scheme()) { Log.i(TAG, "V3.1 signature verified."); } else Log.w(TAG, "V3.1 signature verification failed."); } else Log.w(TAG, "V3 signature verification disabled."); if (sigSchemes.v4SchemeEnabled() && result.isVerifiedUsingV4Scheme()) { Log.i(TAG, "V4 signature verified."); } else Log.w(TAG, "V4 signature verification failed/disabled."); if (result.isSourceStampVerified()) { Log.i(TAG, "SourceStamp verified."); } else Log.w(TAG, "SourceStamp not verified/unavailable."); int i = 0; List signerCertificates = result.getSignerCertificates(); Log.i(TAG, "Number of signatures: %d", signerCertificates.size()); for (X509Certificate logCert : signerCertificates) { i++; logCert(logCert, "Signature" + i); } } for (ApkVerifier.IssueWithParams warn : result.getWarnings()) { Log.w(TAG, "%s", warn); } for (ApkVerifier.IssueWithParams err : result.getErrors()) { Log.e(TAG, "%s", err); } if (sigSchemes.v1SchemeEnabled()) { for (ApkVerifier.Result.V1SchemeSignerInfo signer : result.getV1SchemeIgnoredSigners()) { String name = signer.getName(); for (ApkVerifier.IssueWithParams err : signer.getErrors()) { Log.e(TAG, "%s: %s", name, err); } for (ApkVerifier.IssueWithParams err : signer.getWarnings()) { Log.w(TAG, "%s: %s", name, err); } } } return isVerify; } catch (Exception e) { Log.w(TAG, "Verification failed.", e); return false; } } @Nullable public static String getSourceStampSource(@NonNull ApkVerifier.Result.SourceStampInfo sourceStampInfo) { byte[] certBytes = ExUtils.exceptionAsNull(() -> sourceStampInfo.getCertificate().getEncoded()); if (certBytes == null) { return null; } String sourceStampHash = DigestUtils.getHexDigest(DigestUtils.SHA_256, certBytes); if (sourceStampHash.equals("3257d599a49d2c961a471ca9843f59d341a405884583fc087df4237b733bbd6d")) { return "Google Play"; } return null; } private static void logCert(@NonNull X509Certificate x509Certificate, CharSequence charSequence) throws CertificateEncodingException { int bitLength; Principal subjectDN = x509Certificate.getSubjectDN(); Log.i(TAG, "%s - Unique distinguished name: %s", charSequence, subjectDN); logEncoded(charSequence, x509Certificate.getEncoded()); PublicKey publicKey = x509Certificate.getPublicKey(); if (publicKey instanceof RSAKey) { bitLength = ((RSAKey) publicKey).getModulus().bitLength(); } else if (publicKey instanceof ECKey) { bitLength = ((ECKey) publicKey).getParams().getOrder().bitLength(); } else if (publicKey instanceof DSAKey) { DSAParams params = ((DSAKey) publicKey).getParams(); if (params != null) { bitLength = params.getP().bitLength(); } else bitLength = -1; } else { bitLength = -1; } Log.i(TAG, "%s - key size: %s", charSequence, (bitLength != -1 ? String.valueOf(bitLength) : "Unknown")); String algorithm = publicKey.getAlgorithm(); Log.i(TAG, "%s - key algorithm: %s", charSequence, algorithm); logEncoded(charSequence, publicKey.getEncoded()); } private static void logEncoded(CharSequence charSequence, byte[] bArr) { log(charSequence + " - SHA-256: ", DigestUtils.getDigest(DigestUtils.SHA_256, bArr)); log(charSequence + " - SHA-1: ", DigestUtils.getDigest(DigestUtils.SHA_1, bArr)); log(charSequence + " - MD5: ", DigestUtils.getDigest(DigestUtils.MD5, bArr)); } private static void log(String str, byte[] bArr) { Log.i(TAG, str); Log.w(TAG, HexEncoding.encodeToString(bArr)); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/signing/SignerInfo.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk.signing; import android.content.pm.Signature; import android.content.pm.SigningInfo; import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import com.android.apksig.ApkVerifier; import com.android.apksig.SigningCertificateLineage; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.List; public class SignerInfo { @Nullable private final X509Certificate[] mCurrentSignerCerts; @Nullable private final X509Certificate[] mSignerCertsInLineage; @Nullable private final X509Certificate[] mAllSignerCerts; @Nullable private final X509Certificate mSourceStampCert; public SignerInfo(@NonNull ApkVerifier.Result apkVerifierResult) { List certificates = apkVerifierResult.getSignerCertificates(); if (certificates == null || certificates.isEmpty()) { mCurrentSignerCerts = null; } else { mCurrentSignerCerts = new X509Certificate[certificates.size()]; int i = 0; for (X509Certificate certificate : certificates) { mCurrentSignerCerts[i++] = certificate; } } // Collect source stamp certificate ApkVerifier.Result.SourceStampInfo sourceStampInfo = apkVerifierResult.getSourceStampInfo(); mSourceStampCert = sourceStampInfo != null ? sourceStampInfo.getCertificate() : null; if (mCurrentSignerCerts == null || mCurrentSignerCerts.length > 1) { // Skip checking rotation because the app has multiple signers or no signer at all mAllSignerCerts = mCurrentSignerCerts; mSignerCertsInLineage = null; return; } SigningCertificateLineage lineage = apkVerifierResult.getSigningCertificateLineage(); if (lineage == null) { // There is no SigningCertificateLineage block mAllSignerCerts = mCurrentSignerCerts; mSignerCertsInLineage = null; return; } List certificatesInLineage = lineage.getCertificatesInLineage(); if (certificatesInLineage == null || certificatesInLineage.isEmpty()) { // There is no certificate in the SigningCertificateLineage block mAllSignerCerts = mCurrentSignerCerts; mSignerCertsInLineage = null; return; } // At this point, currentSignatures is a singleton array mSignerCertsInLineage = certificatesInLineage.toArray(new X509Certificate[0]); mAllSignerCerts = new X509Certificate[mCurrentSignerCerts.length + certificatesInLineage.size()]; int i = 0; // Add the current signature on top for (X509Certificate signature : mCurrentSignerCerts) { mAllSignerCerts[i++] = signature; } for (X509Certificate certificate : certificatesInLineage) { mAllSignerCerts[i++] = certificate; } } @RequiresApi(Build.VERSION_CODES.P) public SignerInfo(@Nullable SigningInfo signingInfo) { mSourceStampCert = null; if (signingInfo == null) { mCurrentSignerCerts = null; mSignerCertsInLineage = null; mAllSignerCerts = null; return; } Signature[] currentSignatures = signingInfo.getApkContentsSigners(); Signature[] lineageSignatures = signingInfo.getSigningCertificateHistory(); boolean isLineage = !signingInfo.hasMultipleSigners() && signingInfo.hasPastSigningCertificates(); // Validation if (currentSignatures == null || currentSignatures.length == 0) { // Invalid signatures mCurrentSignerCerts = null; mSignerCertsInLineage = null; mAllSignerCerts = null; return; } if (isLineage && (lineageSignatures == null || lineageSignatures.length == 0)) { // Invalid lineage signatures mCurrentSignerCerts = null; mSignerCertsInLineage = null; mAllSignerCerts = null; return; } int totalSigner = currentSignatures.length + (isLineage ? lineageSignatures.length : 0); mCurrentSignerCerts = new X509Certificate[currentSignatures.length]; mAllSignerCerts = new X509Certificate[totalSigner]; for (int i = 0; i < currentSignatures.length; ++i) { X509Certificate cert = generateCertificateOrFail(currentSignatures[i]); mCurrentSignerCerts[i] = cert; mAllSignerCerts[i] = cert; } if (isLineage) { mSignerCertsInLineage = new X509Certificate[lineageSignatures.length]; for (int i = currentSignatures.length, j = 0; i < totalSigner; ++i, ++j) { X509Certificate cert = generateCertificateOrFail(lineageSignatures[j]); mSignerCertsInLineage[j] = cert; mAllSignerCerts[i] = cert; } } else mSignerCertsInLineage = null; } public SignerInfo(@Nullable Signature[] signatures) { mSourceStampCert = null; mSignerCertsInLineage = null; if (signatures != null && signatures.length > 0) { mAllSignerCerts = new X509Certificate[signatures.length]; mCurrentSignerCerts = new X509Certificate[signatures.length]; for (int i = 0; i < signatures.length; ++i) { X509Certificate cert = generateCertificateOrFail(signatures[i]); mAllSignerCerts[i] = cert; mCurrentSignerCerts[i] = cert; } } else { mCurrentSignerCerts = null; mAllSignerCerts = null; } } public boolean hasMultipleSigners() { return mCurrentSignerCerts != null && mCurrentSignerCerts.length > 1; } public boolean hasProofOfRotation() { return !hasMultipleSigners() && mSignerCertsInLineage != null; } @Nullable public X509Certificate[] getCurrentSignerCerts() { return mCurrentSignerCerts; } @Nullable public X509Certificate getSourceStampCert() { return mSourceStampCert; } @Nullable public X509Certificate[] getSignerCertsInLineage() { return mSignerCertsInLineage; } /** * Retrieve all signatures, including the lineage ones. The current signature(s) are on top of the array. * *

If the APK has multiple signers, all signatures are the current signatures, and if the APK has only one * signer, the first signature is the current signature and rests are the lineage signature. */ @Nullable public X509Certificate[] getAllSignerCerts() { return mAllSignerCerts; } private static X509Certificate generateCertificateOrFail(Signature signature) { try (InputStream is = new ByteArrayInputStream(signature.toByteArray())) { return (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(is); } catch (IOException | CertificateException e) { throw new RuntimeException("Invalid signature", e); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/signing/ZipAlign.java ================================================ // SPDX-License-Identifier: Apache-2.0 OR GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk.signing; import androidx.annotation.NonNull; import com.reandroid.archive.ArchiveEntry; import com.reandroid.archive.ArchiveFile; import com.reandroid.archive.writer.ApkFileWriter; import com.reandroid.archive.writer.ZipAligner; import java.io.File; import java.io.IOException; import java.util.Iterator; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.io.Paths; public class ZipAlign { public static final String TAG = ZipAlign.class.getSimpleName(); public static final int ALIGNMENT_4 = 4; private static final int ALIGNMENT_PAGE = 4096; public static void align(@NonNull File input, @NonNull File output, int alignment, boolean pageAlignSharedLibs) throws IOException { File dir = output.getParentFile(); if (!Paths.exists(dir)) { dir.mkdirs(); } try (ArchiveFile archive = new ArchiveFile(input); ApkFileWriter apkWriter = new ApkFileWriter(output, archive.getInputSources())) { apkWriter.setZipAligner(getZipAligner(alignment, pageAlignSharedLibs)); apkWriter.write(); } if (!verify(output, alignment, pageAlignSharedLibs)) { throw new IOException("Could not verify aligned APK file."); } } public static void align(@NonNull File inFile, int alignment, boolean pageAlignSharedLibs) throws IOException { File tmp = toTmpFile(inFile); tmp.delete(); try { align(inFile, tmp, alignment, pageAlignSharedLibs); inFile.delete(); tmp.renameTo(inFile); } catch (IOException e) { tmp.delete(); throw e; } } public static boolean verify(@NonNull File file, int alignment, boolean pageAlignSharedLibs) { ArchiveFile zipFile; boolean foundBad = false; Log.d(TAG, "Verifying alignment of %s...", file); try { zipFile = new ArchiveFile(file); } catch (IOException e) { Log.e(TAG, "Unable to open '%s' for verification", e, file); return false; } Iterator entryIterator = zipFile.iterator(); while (entryIterator.hasNext()) { ArchiveEntry pEntry = entryIterator.next(); String name = pEntry.getName(); long fileOffset = pEntry.getFileOffset(); if (pEntry.getMethod() == ZipEntry.DEFLATED) { Log.d(TAG, "%8d %s (OK - compressed)", fileOffset, name); } else if (pEntry.isDirectory()) { // Directory entries do not need to be aligned. Log.d(TAG, "%8d %s (OK - directory)", fileOffset, name); } else { int alignTo = getAlignment(pEntry, alignment, pageAlignSharedLibs); if ((fileOffset % alignTo) != 0) { Log.w(TAG, "%8d %s (BAD - %d)\n", fileOffset, name, (fileOffset % alignTo)); foundBad = true; break; } else { Log.d(TAG, "%8d %s (OK)\n", fileOffset, name); } } } Log.d(TAG, "Verification %s\n", foundBad ? "FAILED" : "successful"); try { zipFile.close(); } catch (IOException e) { Log.w(TAG, "Unable to close '%s'", e, file); } return !foundBad; } private static int getAlignment(@NonNull ArchiveEntry entry, int defaultAlignment, boolean pageAlignSharedLibs) { if (!pageAlignSharedLibs) { return defaultAlignment; } String name = entry.getName(); if (name.startsWith("lib/") && name.endsWith(".so")) { return ALIGNMENT_PAGE; } else { return defaultAlignment; } } @NonNull public static ZipAligner getZipAligner(int defaultAlignment, boolean pageAlignSharedLibs) { ZipAligner zipAligner = new ZipAligner(); zipAligner.setDefaultAlignment(defaultAlignment); if (pageAlignSharedLibs) { Pattern patternNativeLib = Pattern.compile("^lib/.+\\.so$"); zipAligner.setFileAlignment(patternNativeLib, ALIGNMENT_PAGE); } return zipAligner; } @NonNull private static File toTmpFile(@NonNull File file) { String name = file.getName() + ".align.tmp"; File dir = file.getParentFile(); if (dir == null) { return new File(name); } return new File(dir, name); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/splitapk/ApksMetadata.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk.splitapk; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Build; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringDef; import androidx.core.content.pm.PackageInfoCompat; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.File; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.zip.ZipOutputStream; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.apk.ApkUtils; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.self.filecache.FileCache; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.JSONUtils; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; public class ApksMetadata { public static final String TAG = ApksMetadata.class.getSimpleName(); public static final String META_FILE = "info.json"; public static final String ICON_FILE = "icon.png"; public static class Dependency { public static final String DEPENDENCY_MATCH_EXACT = "exact"; public static final String DEPENDENCY_MATCH_GREATER = "greater"; public static final String DEPENDENCY_MATCH_LESS = "less"; @StringDef({DEPENDENCY_MATCH_EXACT, DEPENDENCY_MATCH_GREATER, DEPENDENCY_MATCH_LESS}) @Retention(RetentionPolicy.SOURCE) public @interface DependencyMatch { } public String packageName; public String displayName; public String versionName; public long versionCode; @Nullable public String[] signatures; @DependencyMatch public String match; public boolean required; @Nullable public String path; } public static class BuildInfo { public final long timestamp; public final String builderId; public final String builderLabel; public final String builderVersion; public final String platform; public BuildInfo() { timestamp = System.currentTimeMillis(); builderId = BuildConfig.APPLICATION_ID; builderLabel = ContextUtils.getContext().getString(R.string.app_name); builderVersion = BuildConfig.VERSION_NAME; platform = "android"; } public BuildInfo(long timestamp, String builderId, String builderLabel, String builderVersion, String platform) { this.timestamp = timestamp; this.builderId = builderId; this.builderLabel = builderLabel; this.builderVersion = builderVersion; this.platform = platform; } } public long exportTimestamp; public long metaVersion = 1L; public String packageName; public String displayName; public String versionName; public long versionCode; public long minSdk = 0L; public long targetSdk; public BuildInfo buildInfo; public final List dependencies = new ArrayList<>(); private final PackageInfo mPackageInfo; public ApksMetadata() { mPackageInfo = null; } public ApksMetadata(PackageInfo packageInfo) { mPackageInfo = packageInfo; } public void readMetadata(String jsonString) throws JSONException { JSONObject jsonObject = new JSONObject(jsonString); metaVersion = jsonObject.getLong("info_version"); packageName = jsonObject.getString("package_name"); displayName = jsonObject.getString("display_name"); versionName = jsonObject.getString("version_name"); versionCode = jsonObject.getLong("version_code"); minSdk = jsonObject.optLong("min_sdk", 0); targetSdk = jsonObject.getLong("target_sdk"); // Build info JSONObject buildInfoObject = jsonObject.optJSONObject("build_info"); if (buildInfoObject != null) { buildInfo = new BuildInfo(buildInfoObject.getLong("timestamp"), buildInfoObject.getString("builder_id"), buildInfoObject.getString("builder_label"), buildInfoObject.getString("builder_version"), buildInfoObject.getString("platform")); } // Dependencies JSONArray dependencyInfoArray = jsonObject.optJSONArray("dependencies"); if (dependencyInfoArray != null) { for (int i = 0; i < dependencyInfoArray.length(); ++i) { JSONObject dependencyInfoObject = dependencyInfoArray.getJSONObject(i); Dependency dependency = new Dependency(); dependency.packageName = dependencyInfoObject.getString("package_name"); dependency.displayName = dependencyInfoObject.getString("display_name"); dependency.versionName = dependencyInfoObject.getString("version_name"); dependency.versionCode = dependencyInfoObject.getLong("version_code"); String signatures = JSONUtils.getString(dependencyInfoObject, "signature", null); if (signatures != null) { dependency.signatures = signatures.split(","); } dependency.match = dependencyInfoObject.getString("match"); dependency.required = dependencyInfoObject.getBoolean("required"); dependency.path = JSONUtils.getString(dependencyInfoObject, "path", null); dependencies.add(dependency); } } } public void writeMetadata(@NonNull ZipOutputStream zipOutputStream) throws IOException { // Fetch meta PackageManager pm = ContextUtils.getContext().getPackageManager(); ApplicationInfo applicationInfo = mPackageInfo.applicationInfo; packageName = mPackageInfo.packageName; displayName = applicationInfo.loadLabel(pm).toString(); versionName = mPackageInfo.versionName; versionCode = PackageInfoCompat.getLongVersionCode(mPackageInfo); exportTimestamp = 946684800000L; // Fake time if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { minSdk = applicationInfo.minSdkVersion; } targetSdk = applicationInfo.targetSdkVersion; String[] sharedLibraries = applicationInfo.sharedLibraryFiles; if (sharedLibraries != null) { for (String file : sharedLibraries) { if (!file.endsWith(".apk")) { continue; } PackageInfo packageInfo = pm.getPackageArchiveInfo(file, PackageManager.GET_SHARED_LIBRARY_FILES); if (packageInfo == null) { Log.w(TAG, "Could not fetch package info for file %s", file); continue; } if (packageInfo.applicationInfo.sourceDir == null) { packageInfo.applicationInfo.sourceDir = file; } if (packageInfo.applicationInfo.publicSourceDir == null) { packageInfo.applicationInfo.publicSourceDir = file; } // Save as APKS first File tempFile = FileCache.getGlobalFileCache().createCachedFile("apks"); try { Path tempPath = Paths.get(tempFile); SplitApkExporter.saveApks(packageInfo, tempPath); String path = packageInfo.packageName + ApkUtils.EXT_APKS; SplitApkExporter.addFile(zipOutputStream, tempPath, path, exportTimestamp); // Add as dependency Dependency dependency = new Dependency(); dependency.packageName = packageInfo.packageName; dependency.displayName = packageInfo.applicationInfo.loadLabel(pm).toString(); dependency.versionName = packageInfo.versionName; dependency.versionCode = PackageInfoCompat.getLongVersionCode(packageInfo); dependency.required = true; dependency.signatures = null; dependency.match = Dependency.DEPENDENCY_MATCH_EXACT; dependency.path = path; dependencies.add(dependency); } finally { FileCache.getGlobalFileCache().delete(tempFile); } } } // Write meta byte[] meta = getMetadataAsJson().getBytes(StandardCharsets.UTF_8); SplitApkExporter.addBytes(zipOutputStream, meta, ApksMetadata.META_FILE, exportTimestamp); } @NonNull public String getMetadataAsJson() { JSONObject jsonObject = new JSONObject(); try { jsonObject.put("info_version", metaVersion); jsonObject.put("package_name", packageName); jsonObject.put("display_name", displayName); jsonObject.put("version_name", versionName); jsonObject.put("version_code", versionCode); jsonObject.put("min_sdk", minSdk); jsonObject.put("target_sdk", targetSdk); // Skip build info for privacy // Put dependencies JSONArray dependenciesArray = new JSONArray(); for (Dependency dependency : dependencies) { JSONObject dependencyObject = new JSONObject(); dependencyObject.put("package_name", dependency.packageName); dependencyObject.put("display_name", dependency.displayName); dependencyObject.put("version_name", dependency.versionName); dependencyObject.put("version_code", dependency.versionCode); if (dependency.signatures != null) { dependencyObject.put("signature", TextUtils.join(",", dependency.signatures)); } dependencyObject.put("match", dependency.match); dependencyObject.put("required", dependency.required); if (dependency.path != null) { dependencyObject.put("path", dependency.path); } dependenciesArray.put(dependencyObject); } if (dependenciesArray.length() > 0) { jsonObject.put("dependencies", dependenciesArray); } } catch (JSONException e) { e.printStackTrace(); } return jsonObject.toString(); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/splitapk/SplitApkChooser.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk.splitapk; import android.content.pm.ApplicationInfo; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; import aosp.libcore.util.EmptyArray; import io.github.muntashirakon.AppManager.apk.ApkFile; import io.github.muntashirakon.AppManager.apk.ApkSource; import io.github.muntashirakon.AppManager.apk.installer.PackageInstallerViewModel; import io.github.muntashirakon.AppManager.utils.ArrayUtils; import io.github.muntashirakon.dialog.SearchableMultiChoiceDialogBuilder; public class SplitApkChooser extends Fragment { public static final String TAG = SplitApkChooser.class.getSimpleName(); private static final String EXTRA_ACTION_NAME = "name"; private static final String EXTRA_VERSION_INFO = "version"; @NonNull public static SplitApkChooser getNewInstance(@NonNull String versionInfo, @Nullable String actionName) { SplitApkChooser splitApkChooser = new SplitApkChooser(); Bundle args = new Bundle(); args.putString(EXTRA_ACTION_NAME, actionName); args.putString(EXTRA_VERSION_INFO, versionInfo); splitApkChooser.setArguments(args); return splitApkChooser; } private PackageInstallerViewModel mViewModel; private List mApkEntries; private SearchableMultiChoiceDialogBuilder mViewBuilder; private Set mSelectedSplits; private final HashMap /* seen types */> mSeenSplits = new HashMap<>(); @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); mViewModel = new ViewModelProvider(requireActivity()).get(PackageInstallerViewModel.class); mSelectedSplits = mViewModel.getSelectedSplits(); } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { ApkFile apkFile = mViewModel.getApkFile(); if (apkFile == null) { throw new IllegalArgumentException("ApkFile cannot be empty."); } if (!apkFile.isSplit()) { throw new RuntimeException("Apk file does not contain any split."); } mApkEntries = apkFile.getEntries(); String[] entryIds = new String[mApkEntries.size()]; CharSequence[] entryNames = new CharSequence[mApkEntries.size()]; for (int i = 0; i < mApkEntries.size(); ++i) { ApkFile.Entry entry = mApkEntries.get(i); entryIds[i] = entry.id; entryNames[i] = entry.toLocalizedString(requireActivity()); } mViewBuilder = new SearchableMultiChoiceDialogBuilder<>(requireActivity(), entryIds, entryNames) .showSelectAll(false) .addDisabledItems(getUnsupportedOrRequiredSplitIds()); mViewBuilder.create(); // Necessary to trigger multichoice dialog return mViewBuilder.getView(); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { mViewBuilder.addSelections(getInitialSelections()) .setOnMultiChoiceClickListener((dialog, which, item, isChecked) -> { if (isChecked) { mViewBuilder.addSelections(select(which)); } else { int[] itemsToDeselect = deselect(which); if (itemsToDeselect == null) { // This item can't be deselected, reselect the item mViewBuilder.addSelections(new int[]{which}); } else { mViewBuilder.removeSelections(itemsToDeselect); } } mViewBuilder.reloadListUi(); }); } @NonNull public int[] getInitialSelections() { List selections = new ArrayList<>(); try { HashSet splitNames = new HashSet<>(); // See if the app has been installed if (mViewModel.getInstalledPackageInfo() != null) { ApplicationInfo info = mViewModel.getInstalledPackageInfo().applicationInfo; try (ApkFile installedApkFile = ApkSource.getApkSource(info).resolve()) { for (ApkFile.Entry apkEntry : installedApkFile.getEntries()) { splitNames.add(apkEntry.name); } } } if (splitNames.size() > 0) { for (ApkFile.Entry apkEntry : mApkEntries) { if (!splitNames.contains(apkEntry.name)) { // Ignore splits that weren't selected in the previous installation continue; } mSelectedSplits.add(apkEntry.id); HashSet types = mSeenSplits.get(apkEntry.getFeature()); if (types == null) { types = new HashSet<>(); mSeenSplits.put(apkEntry.getFeature(), types); } types.add(apkEntry.type); } // Fall-through deliberately to see if there are any new requirements } } catch (ApkFile.ApkFileException ignored) { } // Set up features for (int i = 0; i < mApkEntries.size(); ++i) { ApkFile.Entry apkEntry = mApkEntries.get(i); if (mSelectedSplits.contains(apkEntry.id)) { // Features already set selections.add(i); continue; } if (apkEntry.isRequired()) { // Required splits are selected by default mSelectedSplits.add(apkEntry.id); selections.add(i); HashSet types = mSeenSplits.get(apkEntry.getFeature()); if (types == null) { types = new HashSet<>(); mSeenSplits.put(apkEntry.getFeature(), types); } types.add(apkEntry.type); } } // Select feature-dependencies based on the items selected above. // Only selecting the first item works because the splits are already ranked. for (int i = 0; i < mApkEntries.size(); ++i) { ApkFile.Entry apkEntry = mApkEntries.get(i); if (mSelectedSplits.contains(apkEntry.id)) { // Already selected continue; } HashSet types = mSeenSplits.get(apkEntry.getFeature()); if (types == null) { // This feature was not selected earlier continue; } switch (apkEntry.type) { case ApkFile.APK_BASE: case ApkFile.APK_SPLIT_FEATURE: case ApkFile.APK_SPLIT_UNKNOWN: case ApkFile.APK_SPLIT: // Never reached. break; case ApkFile.APK_SPLIT_DENSITY: if (!types.contains(ApkFile.APK_SPLIT_DENSITY)) { types.add(ApkFile.APK_SPLIT_DENSITY); selections.add(i); mSelectedSplits.add(apkEntry.id); } break; case ApkFile.APK_SPLIT_ABI: if (!types.contains(ApkFile.APK_SPLIT_ABI)) { types.add(ApkFile.APK_SPLIT_ABI); selections.add(i); mSelectedSplits.add(apkEntry.id); } break; case ApkFile.APK_SPLIT_LOCALE: if (!types.contains(ApkFile.APK_SPLIT_LOCALE)) { types.add(ApkFile.APK_SPLIT_LOCALE); selections.add(i); mSelectedSplits.add(apkEntry.id); } break; default: throw new RuntimeException("Invalid split type."); } } return ArrayUtils.convertToIntArray(selections); } @NonNull private List getUnsupportedOrRequiredSplitIds() { List unsupportedOrRequiredSplits = new ArrayList<>(); for (ApkFile.Entry apkEntry : mApkEntries) { if (!apkEntry.supported() || apkEntry.isRequired()) { unsupportedOrRequiredSplits.add(apkEntry.id); } } return unsupportedOrRequiredSplits; } @NonNull private int[] select(int index) { List selections = new ArrayList<>(); ApkFile.Entry selectedEntry = mApkEntries.get(index); String feature = selectedEntry.getFeature(); HashSet types = mSeenSplits.get(feature); if (types == null) { types = new HashSet<>(); mSeenSplits.put(feature, types); } mSelectedSplits.add(selectedEntry.id); // We don't need to add it to selections because it's already checked for (int i = 0; i < mApkEntries.size(); ++i) { ApkFile.Entry apkEntry = mApkEntries.get(i); if (Objects.equals(apkEntry.getFeature(), feature) && apkEntry.type != selectedEntry.type) { // Match only the entries with the same feature and select at least one item for each required type. switch (apkEntry.type) { case ApkFile.APK_BASE: case ApkFile.APK_SPLIT_FEATURE: // FIXME: 7/7/23 Never reached? selections.add(i); mSelectedSplits.add(apkEntry.id); break; case ApkFile.APK_SPLIT_UNKNOWN: case ApkFile.APK_SPLIT: break; case ApkFile.APK_SPLIT_DENSITY: if (!types.contains(ApkFile.APK_SPLIT_DENSITY)) { types.add(ApkFile.APK_SPLIT_DENSITY); selections.add(i); mSelectedSplits.add(apkEntry.id); } break; case ApkFile.APK_SPLIT_ABI: if (!types.contains(ApkFile.APK_SPLIT_ABI)) { types.add(ApkFile.APK_SPLIT_ABI); selections.add(i); mSelectedSplits.add(apkEntry.id); } break; case ApkFile.APK_SPLIT_LOCALE: if (!types.contains(ApkFile.APK_SPLIT_LOCALE)) { types.add(ApkFile.APK_SPLIT_LOCALE); selections.add(i); mSelectedSplits.add(apkEntry.id); } break; default: throw new RuntimeException("Invalid split type."); } } } return ArrayUtils.convertToIntArray(selections); } @Nullable private int[] deselect(int index) { ApkFile.Entry deselectedEntry = mApkEntries.get(index); if (deselectedEntry.isRequired()) { // 1. This is a required split, can't be deselected return null; } boolean featureSplit = deselectedEntry.type == ApkFile.APK_SPLIT_FEATURE; String deselectedFeature = deselectedEntry.getFeature(); if (featureSplit) { // 2. If this is a feature split (base.apk is always a required split), deselect all the associated splits List deselectedSplits = new ArrayList<>(); mSeenSplits.remove(deselectedFeature); for (int i = 0; i < mApkEntries.size(); ++i) { ApkFile.Entry apkEntry = mApkEntries.get(i); if (Objects.equals(apkEntry.getFeature(), deselectedFeature)) { // Split has the same feature if (mSelectedSplits.contains(apkEntry.id)) { deselectedSplits.add(i); mSelectedSplits.remove(apkEntry.id); } } } return ArrayUtils.convertToIntArray(deselectedSplits); } // 3. This isn't a feature split. Find all the splits by the same type and see if at least one split is // selected. If not, this split can't be deselected. boolean selectedAnySplits = false; for (int i = 0; i < mApkEntries.size(); ++i) { ApkFile.Entry apkEntry = mApkEntries.get(i); if (i != index && deselectedEntry.type == apkEntry.type && Objects.equals(apkEntry.getFeature(), deselectedFeature) && mSelectedSplits.contains(apkEntry.id)) { // Split has the same type and is selected selectedAnySplits = true; break; } } if (selectedAnySplits) { // At least one item is selected, deselect the current one mSelectedSplits.remove(deselectedEntry.id); return EmptyArray.INT; } // This entry can't be deselected return null; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/splitapk/SplitApkExporter.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk.splitapk; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.graphics.Bitmap; import android.os.UserHandleHidden; import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.zip.Deflater; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import io.github.muntashirakon.AppManager.apk.ApkUtils; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.DigestUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.io.IoUtils; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; /** * Used to generate app bundle with .apks extension. This file has all the apks as well as 3 other * file, such as icon.png, meta.sai_v1.json, meta.sai_v2.json.
* meta.sai_v1.json contains the following properties: export_timestamp (long), label (string), * package (string), version_code (long) and version_name (string).
* meta.sai_v2.json contains the following properties: export_timestamp (long), split_apk (boolean), * label (string), meta_version (long), min_sdk (long), package (string), target_sdk (long), * version_code (long), version_name (string), backup_components [ size (long), type (string) ] */ public final class SplitApkExporter { @WorkerThread public static void saveApks(@NonNull PackageInfo packageInfo, @NonNull Path apksFile) throws IOException { try (OutputStream outputStream = apksFile.openOutputStream(); ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) { zipOutputStream.setMethod(ZipOutputStream.DEFLATED); zipOutputStream.setLevel(Deflater.BEST_COMPRESSION); saveApkInternal(zipOutputStream, packageInfo); } } static void saveApkInternal(@NonNull ZipOutputStream zipOutputStream, @NonNull PackageInfo packageInfo) throws IOException { ApplicationInfo applicationInfo = packageInfo.applicationInfo; List apkFiles = getAllApkFiles(applicationInfo); Collections.sort(apkFiles); // Metadata ApksMetadata apksMetadata = new ApksMetadata(packageInfo); apksMetadata.writeMetadata(zipOutputStream); // Add icon Bitmap bitmap = UIUtils.getBitmapFromDrawable(applicationInfo.loadIcon(ContextUtils.getContext().getPackageManager())); ByteArrayOutputStream pngOutputStream = new ByteArrayOutputStream(); bitmap.compress(Bitmap.CompressFormat.PNG, 100, pngOutputStream); addBytes(zipOutputStream, pngOutputStream.toByteArray(), ApksMetadata.ICON_FILE, apksMetadata.exportTimestamp); // Add apk files for (Path apkFile : apkFiles) { addFile(zipOutputStream, apkFile, apkFile.getName(), apksMetadata.exportTimestamp); } // Add OBB files if possible Path obbDir = null; try { obbDir = ApkUtils.getObbDir(packageInfo.packageName, UserHandleHidden.getUserId(applicationInfo.uid)); } catch (IOException ignore) { } if (obbDir != null) { Path[] obbFiles = obbDir.listFiles(); for (Path obbFile : obbFiles) { addFile(zipOutputStream, obbFile, obbFile.getName(), apksMetadata.exportTimestamp); } } } static void addFile(@NonNull ZipOutputStream zipOutputStream, @NonNull Path filePath, @NonNull String name, long timestamp) throws IOException { ZipEntry zipEntry = new ZipEntry(name); zipEntry.setMethod(ZipEntry.DEFLATED); zipEntry.setSize(filePath.length()); zipEntry.setCrc(DigestUtils.calculateCrc32(filePath)); zipEntry.setTime(timestamp); zipOutputStream.putNextEntry(zipEntry); try (InputStream apkInputStream = filePath.openInputStream()) { IoUtils.copy(apkInputStream, zipOutputStream); } zipOutputStream.closeEntry(); } static void addBytes(@NonNull ZipOutputStream zipOutputStream, @NonNull byte[] bytes, @NonNull String name, long timestamp) throws IOException { ZipEntry zipEntry = new ZipEntry(name); zipEntry.setMethod(ZipEntry.DEFLATED); zipEntry.setSize(bytes.length); zipEntry.setCrc(DigestUtils.calculateCrc32(bytes)); zipEntry.setTime(timestamp); zipOutputStream.putNextEntry(zipEntry); zipOutputStream.write(bytes); zipOutputStream.closeEntry(); } @NonNull private static List getAllApkFiles(@NonNull ApplicationInfo applicationInfo) { List apkFiles = new ArrayList<>(); apkFiles.add(Paths.get(applicationInfo.publicSourceDir)); if (applicationInfo.splitPublicSourceDirs != null) { // FIXME: 8/5/22 This does not work for disabled apps for (String splitPath : applicationInfo.splitPublicSourceDirs) apkFiles.add(Paths.get(splitPath)); } return apkFiles; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/whatsnew/ApkWhatsNewFinder.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk.whatsnew; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.FeatureInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PermissionInfo; import android.os.Build; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; import androidx.core.content.pm.PackageInfoCompat; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Set; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.rules.RuleType; import io.github.muntashirakon.AppManager.rules.compontents.ComponentUtils; import io.github.muntashirakon.AppManager.utils.ArrayUtils; import io.github.muntashirakon.AppManager.utils.LangUtils; import io.github.muntashirakon.AppManager.utils.PackageUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.Utils; public class ApkWhatsNewFinder { @IntDef(value = { CHANGE_ADD, CHANGE_REMOVED, CHANGE_INFO }) @Retention(RetentionPolicy.SOURCE) public @interface ChangeType { } public static final int CHANGE_ADD = 1; public static final int CHANGE_REMOVED = 2; public static final int CHANGE_INFO = 3; public static final int VERSION_INFO = 0; public static final int TRACKER_INFO = 1; public static final int SIGNING_CERT_SHA256 = 2; public static final int PERMISSION_INFO = 3; public static final int COMPONENT_INFO = 4; public static final int FEATURE_INFO = 5; public static final int SDK_INFO = 6; private static final int INFO_COUNT = 7; private static ApkWhatsNewFinder sInstance; public static ApkWhatsNewFinder getInstance() { if (sInstance == null) sInstance = new ApkWhatsNewFinder(); return sInstance; } private final Set mTmpInfo = new HashSet<>(); /** * Get changes between two packages: one is the apk file and other is the installed app * * @param newPkgInfo Package info fetched with {@link PackageManager#getPackageArchiveInfo(String, int)} * with the following flags: {@link PackageManager#GET_META_DATA}, {@link PackageManager#GET_SIGNATURES} * {@link PackageManager#GET_PERMISSIONS}, {@link PackageManager#GET_CONFIGURATIONS}, * {@link PackageManager#GET_SHARED_LIBRARY_FILES} * @param oldPkgInfo Package info fetched with {@link PackageManager#getPackageInfo(String, int)} * with the following flags: {@link PackageManager#GET_META_DATA}, {@link PackageManager#GET_SIGNATURES} or * {@link PackageManager#GET_SIGNING_CERTIFICATES}, {@link PackageManager#GET_PERMISSIONS}, * {@link PackageManager#GET_CONFIGURATIONS}, {@link PackageManager#GET_SHARED_LIBRARY_FILES} * @return Changes */ @WorkerThread @NonNull public Change[][] getWhatsNew(@NonNull Context context, @NonNull PackageInfo newPkgInfo, @NonNull PackageInfo oldPkgInfo) { ApplicationInfo newAppInfo = newPkgInfo.applicationInfo; ApplicationInfo oldAppInfo = oldPkgInfo.applicationInfo; Change[][] changes = new Change[INFO_COUNT][]; String[] componentInfo = context.getResources().getStringArray(R.array.whats_new_titles); // Version info long newVersionCode = PackageInfoCompat.getLongVersionCode(newPkgInfo); long oldVersionCode = PackageInfoCompat.getLongVersionCode(oldPkgInfo); if (newVersionCode != oldVersionCode) { String newVersionInfo = newPkgInfo.versionName + " (" + newVersionCode + ')'; String oldVersionInfo = oldPkgInfo.versionName + " (" + oldVersionCode + ')'; changes[VERSION_INFO] = new Change[]{ new Change(CHANGE_INFO, componentInfo[VERSION_INFO]), new Change(CHANGE_ADD, newVersionInfo), new Change(CHANGE_REMOVED, oldVersionInfo) }; } else changes[VERSION_INFO] = ArrayUtils.emptyArray(Change.class); if (ThreadUtils.isInterrupted()) { return changes; } // Tracker info HashMap newPkgComponents = PackageUtils.collectComponentClassNames(newPkgInfo); HashMap oldPkgComponents = PackageUtils.collectComponentClassNames(oldPkgInfo); List componentChanges = new ArrayList<>(); componentChanges.add(new Change(CHANGE_INFO, componentInfo[COMPONENT_INFO])); componentChanges.addAll(findChanges(newPkgComponents.keySet(), oldPkgComponents.keySet())); int newTrackerCount = 0; int oldTrackerCount = 0; for (Change component : componentChanges) { if (ComponentUtils.isTracker(component.value)) { if (component.changeType == CHANGE_ADD) ++newTrackerCount; else if (component.changeType == CHANGE_REMOVED) ++oldTrackerCount; } } if (newTrackerCount == 0 && oldTrackerCount == 0) { changes[TRACKER_INFO] = ArrayUtils.emptyArray(Change.class); } else { Change newTrackers = new Change(CHANGE_ADD, context.getResources() .getQuantityString(R.plurals.no_of_trackers, newTrackerCount, newTrackerCount)); Change oldTrackers = new Change(CHANGE_REMOVED, context.getResources() .getQuantityString(R.plurals.no_of_trackers, oldTrackerCount, oldTrackerCount)); changes[TRACKER_INFO] = new Change[]{new Change(CHANGE_INFO, componentInfo[TRACKER_INFO]), newTrackers, oldTrackers}; } if (ThreadUtils.isInterrupted()) { return changes; } // Sha256 of signing certificates Set newCertSha256 = new HashSet<>(Arrays.asList(PackageUtils.getSigningCertSha256Checksum(newPkgInfo, true))); Set oldCertSha256 = new HashSet<>(Arrays.asList(PackageUtils.getSigningCertSha256Checksum(oldPkgInfo))); List certSha256Changes = new ArrayList<>(); certSha256Changes.add(new Change(CHANGE_INFO, componentInfo[SIGNING_CERT_SHA256])); certSha256Changes.addAll(findChanges(newCertSha256, oldCertSha256)); changes[SIGNING_CERT_SHA256] = certSha256Changes.size() == 1 ? ArrayUtils.emptyArray(Change.class) : certSha256Changes.toArray(new Change[0]); if (ThreadUtils.isInterrupted()) { return changes; } // Permissions Set newPermissions = new HashSet<>(); Set oldPermissions = new HashSet<>(); if (newPkgInfo.permissions != null) for (PermissionInfo permissionInfo : newPkgInfo.permissions) newPermissions.add(permissionInfo.name); if (newPkgInfo.requestedPermissions != null) newPermissions.addAll(Arrays.asList(newPkgInfo.requestedPermissions)); if (oldPkgInfo.permissions != null) for (PermissionInfo permissionInfo : oldPkgInfo.permissions) oldPermissions.add(permissionInfo.name); if (oldPkgInfo.requestedPermissions != null) oldPermissions.addAll(Arrays.asList(oldPkgInfo.requestedPermissions)); List permissionChanges = new ArrayList<>(); permissionChanges.add(new Change(CHANGE_INFO, componentInfo[PERMISSION_INFO])); permissionChanges.addAll(findChanges(newPermissions, oldPermissions)); changes[PERMISSION_INFO] = permissionChanges.size() == 1 ? ArrayUtils.emptyArray(Change.class) : permissionChanges.toArray(new Change[0]); // Component info changes[COMPONENT_INFO] = componentChanges.size() == 1 ? ArrayUtils.emptyArray(Change.class) : componentChanges.toArray(new Change[0]); if (ThreadUtils.isInterrupted()) { return changes; } // Feature info Set newFeatures = new HashSet<>(); Set oldFeatures = new HashSet<>(); if (newPkgInfo.reqFeatures != null) for (FeatureInfo featureInfo : newPkgInfo.reqFeatures) if (featureInfo.name != null) newFeatures.add(featureInfo.name); else newFeatures.add("OpenGL ES v" + Utils.getGlEsVersion(featureInfo.reqGlEsVersion)); if (oldPkgInfo.reqFeatures != null) for (FeatureInfo featureInfo : oldPkgInfo.reqFeatures) if (featureInfo.name != null) oldFeatures.add(featureInfo.name); else oldFeatures.add("OpenGL ES v" + Utils.getGlEsVersion(featureInfo.reqGlEsVersion)); List featureChanges = new ArrayList<>(); featureChanges.add(new Change(CHANGE_INFO, componentInfo[FEATURE_INFO])); featureChanges.addAll(findChanges(newFeatures, oldFeatures)); changes[FEATURE_INFO] = featureChanges.size() == 1 ? ArrayUtils.emptyArray(Change.class) : featureChanges.toArray(new Change[0]); if (ThreadUtils.isInterrupted()) { return changes; } // SDK final StringBuilder newSdk = new StringBuilder(context.getString(R.string.sdk_max)) .append(LangUtils.getSeparatorString()).append(newAppInfo.targetSdkVersion); final StringBuilder oldSdk = new StringBuilder(context.getString(R.string.sdk_max)) .append(LangUtils.getSeparatorString()).append(oldAppInfo.targetSdkVersion); if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) { newSdk.append(", ").append(context.getString(R.string.sdk_min)) .append(LangUtils.getSeparatorString()).append(newAppInfo.minSdkVersion); oldSdk.append(", ").append(context.getString(R.string.sdk_min)) .append(LangUtils.getSeparatorString()).append(oldAppInfo.minSdkVersion); } if (!newSdk.toString().equals(oldSdk.toString())) { changes[SDK_INFO] = new Change[]{ new Change(CHANGE_INFO, componentInfo[SDK_INFO]), new Change(CHANGE_ADD, newSdk.toString()), new Change(CHANGE_REMOVED, oldSdk.toString()) }; } else changes[SDK_INFO] = ArrayUtils.emptyArray(Change.class); return changes; } @NonNull private List findChanges(Set newInfo, Set oldInfo) { List changeList = new ArrayList<>(); mTmpInfo.clear(); mTmpInfo.addAll(newInfo); newInfo.removeAll(oldInfo); for (String info : newInfo) changeList.add(new Change(CHANGE_ADD, info)); oldInfo.removeAll(mTmpInfo); for (String info : oldInfo) changeList.add(new Change(CHANGE_REMOVED, info)); return changeList; } public static class Change { @ChangeType public int changeType; @NonNull public String value; public Change(int changeType, @NonNull String value) { this.changeType = changeType; this.value = value; } @NonNull @Override public String toString() { return "Change{" + "changeType=" + changeType + ", value='" + value + '\'' + '}'; } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/whatsnew/WhatsNewDialogFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk.whatsnew; import android.app.Dialog; import android.content.pm.PackageInfo; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.os.BundleCompat; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.util.Objects; import io.github.muntashirakon.AppManager.R; public class WhatsNewDialogFragment extends DialogFragment { public static final String TAG = WhatsNewDialogFragment.class.getSimpleName(); private static final String ARG_NEW_PKG_INFO = "new_pkg"; private static final String ARG_OLD_PKG_INFO = "old_pkg"; @NonNull public static WhatsNewDialogFragment getInstance(@NonNull PackageInfo newPkgInfo, @NonNull PackageInfo oldPkgInfo) { WhatsNewDialogFragment dialog = new WhatsNewDialogFragment(); Bundle args = new Bundle(); args.putParcelable(ARG_NEW_PKG_INFO, newPkgInfo); args.putParcelable(ARG_OLD_PKG_INFO, oldPkgInfo); dialog.setArguments(args); return dialog; } private WhatsNewRecyclerAdapter mAdapter; private PackageInfo mNewPkgInfo; private PackageInfo mOldPkgInfo; private View mDialogView; @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { mDialogView = View.inflate(requireContext(), R.layout.dialog_whats_new, null); return new MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.whats_new) .setView(mDialogView) .setNegativeButton(R.string.ok, null) .create(); } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return mDialogView; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { WhatsNewDialogViewModel viewModel = new ViewModelProvider(this).get(WhatsNewDialogViewModel.class); mNewPkgInfo = Objects.requireNonNull(BundleCompat.getParcelable(requireArguments(), ARG_NEW_PKG_INFO, PackageInfo.class)); mOldPkgInfo = Objects.requireNonNull(BundleCompat.getParcelable(requireArguments(), ARG_OLD_PKG_INFO, PackageInfo.class)); RecyclerView recyclerView = mDialogView.findViewById(android.R.id.list); recyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); mAdapter = new WhatsNewRecyclerAdapter(requireContext(), mNewPkgInfo.packageName); recyclerView.setAdapter(mAdapter); viewModel.getChangesLiveData().observe(this, mAdapter::setAdapterList); viewModel.loadChanges(mNewPkgInfo, mOldPkgInfo); } @Override public void show(@NonNull FragmentManager manager, @Nullable String tag) { FragmentTransaction ft = manager.beginTransaction(); ft.add(this, tag); ft.commitAllowingStateLoss(); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/whatsnew/WhatsNewDialogViewModel.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk.whatsnew; import android.app.Application; import android.content.pm.PackageInfo; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.Future; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.utils.ThreadUtils; public class WhatsNewDialogViewModel extends AndroidViewModel { private final MutableLiveData> mChangesLiveData = new MutableLiveData<>(); @Nullable private Future mWhatsNewResult; public WhatsNewDialogViewModel(@NonNull Application application) { super(application); } @Override protected void onCleared() { if (mWhatsNewResult != null) { mWhatsNewResult.cancel(true); } super.onCleared(); } public LiveData> getChangesLiveData() { return mChangesLiveData; } public void loadChanges(PackageInfo newPkgInfo, PackageInfo oldPkgInfo) { mWhatsNewResult = ThreadUtils.postOnBackgroundThread(() -> { ApkWhatsNewFinder.Change[][] changes = ApkWhatsNewFinder.getInstance().getWhatsNew(getApplication(), newPkgInfo, oldPkgInfo); List changeList = new ArrayList<>(); for (ApkWhatsNewFinder.Change[] changes1 : changes) { if (changes1.length > 0) { Collections.addAll(changeList, changes1); } } if (changeList.size() == 0) { changeList.add(new ApkWhatsNewFinder.Change(ApkWhatsNewFinder.CHANGE_INFO, getApplication().getString(R.string.no_changes))); } mChangesLiveData.postValue(changeList); }); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/whatsnew/WhatsNewFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk.whatsnew; import android.content.pm.PackageInfo; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.os.BundleCompat; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import java.util.Objects; import io.github.muntashirakon.AppManager.R; public class WhatsNewFragment extends Fragment { public static final String TAG = WhatsNewFragment.class.getSimpleName(); private static final String ARG_NEW_PKG_INFO = "new_pkg"; private static final String ARG_OLD_PKG_INFO = "old_pkg"; @NonNull public static WhatsNewFragment getInstance(@NonNull PackageInfo newPkgInfo, @NonNull PackageInfo oldPkgInfo) { WhatsNewFragment dialog = new WhatsNewFragment(); Bundle args = new Bundle(); args.putParcelable(ARG_NEW_PKG_INFO, newPkgInfo); args.putParcelable(ARG_OLD_PKG_INFO, oldPkgInfo); dialog.setArguments(args); return dialog; } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return View.inflate(requireContext(), R.layout.dialog_whats_new, null); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { WhatsNewDialogViewModel viewModel = new ViewModelProvider(this).get(WhatsNewDialogViewModel.class); PackageInfo newPkgInfo = Objects.requireNonNull(BundleCompat.getParcelable(requireArguments(), ARG_NEW_PKG_INFO, PackageInfo.class)); PackageInfo oldPkgInfo = Objects.requireNonNull(BundleCompat.getParcelable(requireArguments(), ARG_OLD_PKG_INFO, PackageInfo.class)); RecyclerView recyclerView = view.findViewById(android.R.id.list); recyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); WhatsNewRecyclerAdapter adapter = new WhatsNewRecyclerAdapter(requireContext(), newPkgInfo.packageName); recyclerView.setAdapter(adapter); viewModel.getChangesLiveData().observe(getViewLifecycleOwner(), adapter::setAdapterList); viewModel.loadChanges(newPkgInfo, oldPkgInfo); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/whatsnew/WhatsNewRecyclerAdapter.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.apk.whatsnew; import android.content.Context; import android.graphics.Typeface; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import com.google.android.material.textview.MaterialTextView; import java.util.ArrayList; import java.util.List; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.util.AdapterUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.AppManager.utils.appearance.ColorCodes; import io.github.muntashirakon.widget.RecyclerView; class WhatsNewRecyclerAdapter extends RecyclerView.Adapter { private final List mAdapterList = new ArrayList<>(); private final int mColorAdd; private final int mColorRemove; private final int mColorNeutral; private final Typeface mTypefaceNormal; private final Typeface mTypefaceMedium; private final String mPackageName; WhatsNewRecyclerAdapter(Context context, @NonNull String packageName) { mPackageName = packageName; mColorAdd = ColorCodes.getWhatsNewPlusIndicatorColor(context); mColorRemove = ColorCodes.getWhatsNewMinusIndicatorColor(context); mColorNeutral = UIUtils.getTextColorPrimary(context); mTypefaceNormal = Typeface.create("sans-serif", Typeface.NORMAL); mTypefaceMedium = Typeface.create("sans-serif-medium", Typeface.NORMAL); } void setAdapterList(List list) { AdapterUtils.notifyDataSetChanged(this, mAdapterList, list); } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { int layoutId; if (viewType == ApkWhatsNewFinder.CHANGE_INFO) { layoutId = R.layout.item_text_view; } else { layoutId = R.layout.item_whats_new; } View view = LayoutInflater.from(parent.getContext()).inflate(layoutId, parent, false); return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { ApkWhatsNewFinder.Change change = mAdapterList.get(position); if (change.value.startsWith(mPackageName)) { change.value = change.value.replaceFirst(mPackageName, ""); } switch (change.changeType) { case ApkWhatsNewFinder.CHANGE_ADD: holder.changeSign.setText("+"); holder.changeSign.setTextColor(mColorAdd); holder.textView.setText(change.value); holder.textView.setTextColor(mColorAdd); break; case ApkWhatsNewFinder.CHANGE_INFO: holder.textView.setText(change.value); holder.textView.setTextColor(mColorNeutral); holder.textView.setTypeface(mTypefaceMedium); holder.textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16); break; case ApkWhatsNewFinder.CHANGE_REMOVED: holder.changeSign.setText("-"); holder.changeSign.setTextColor(mColorRemove); holder.textView.setText(change.value); holder.textView.setTextColor(mColorRemove); break; } } @Override public int getItemCount() { return mAdapterList.size(); } @Override public int getItemViewType(int position) { return mAdapterList.get(position).changeType; } static class ViewHolder extends RecyclerView.ViewHolder { final MaterialTextView changeSign; final MaterialTextView textView; public ViewHolder(@NonNull View itemView) { super(itemView); changeSign = itemView.findViewById(android.R.id.text2); textView = itemView.findViewById(android.R.id.text1); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/app/AndroidFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.app; import android.content.Context; import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import java.util.Optional; public class AndroidFragment extends Fragment { @NonNull protected Optional getFragmentContext() { return Optional.ofNullable(getContext()); } @NonNull protected Optional getFragmentActivity() { return Optional.ofNullable(getActivity()); } @NonNull protected Optional getActionBar() { FragmentActivity activity = getActivity(); if (activity instanceof AppCompatActivity) { return Optional.ofNullable(((AppCompatActivity) activity).getSupportActionBar()); } return Optional.empty(); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/backup/BackupCryptSetupHelper.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.backup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import io.github.muntashirakon.AppManager.crypto.AESCrypto; import io.github.muntashirakon.AppManager.crypto.Crypto; import io.github.muntashirakon.AppManager.crypto.CryptoException; import io.github.muntashirakon.AppManager.crypto.DummyCrypto; import io.github.muntashirakon.AppManager.crypto.ECCCrypto; import io.github.muntashirakon.AppManager.crypto.OpenPGPCrypto; import io.github.muntashirakon.AppManager.crypto.RSACrypto; import io.github.muntashirakon.AppManager.crypto.ks.CompatUtil; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.utils.ContextUtils; public class BackupCryptSetupHelper { @NonNull @CryptoUtils.Mode public final String mode; public final int version; @NonNull public final Crypto crypto; private String keyIds; private byte[] aes; private byte[] iv; public BackupCryptSetupHelper(@NonNull String mode, int version) throws CryptoException { this.mode = mode; this.version = version; this.crypto = setup(); } @Nullable public String getKeyIds() { return keyIds; } @Nullable public byte[] getAes() { return aes; } @Nullable public byte[] getIv() { return iv; } @NonNull private Crypto setup() throws CryptoException { switch (mode) { case CryptoUtils.MODE_OPEN_PGP: keyIds = Prefs.Encryption.getOpenPgpKeyIds(); return new OpenPGPCrypto(ContextUtils.getContext(), keyIds); case CryptoUtils.MODE_AES: { iv = generateIv(); AESCrypto aesCrypto = new AESCrypto(iv); if (version < 4) { // Old backups use 32 bit MAC aesCrypto.setMacSizeBits(AESCrypto.MAC_SIZE_BITS_OLD); } return aesCrypto; } case CryptoUtils.MODE_RSA: { iv = generateIv(); RSACrypto rsaCrypto = new RSACrypto(iv, null); if (version < 4) { // Old backups use 32 bit MAC rsaCrypto.setMacSizeBits(AESCrypto.MAC_SIZE_BITS_OLD); } aes = rsaCrypto.getEncryptedAesKey(); return rsaCrypto; } case CryptoUtils.MODE_ECC: { iv = generateIv(); ECCCrypto eccCrypto = new ECCCrypto(iv, null); if (version < 4) { // Old backups use 32 bit MAC eccCrypto.setMacSizeBits(AESCrypto.MAC_SIZE_BITS_OLD); } aes = eccCrypto.getEncryptedAesKey(); return eccCrypto; } case CryptoUtils.MODE_NO_ENCRYPTION: default: return new DummyCrypto(); } } @NonNull private static byte[] generateIv() { byte[] iv = new byte[AESCrypto.GCM_IV_SIZE_BYTES]; CompatUtil.getPrng().nextBytes(iv); return iv; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/backup/BackupDataDirectoryInfo.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.backup; import android.annotation.SuppressLint; import android.annotation.UserIdInt; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Locale; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; public class BackupDataDirectoryInfo { public static final String TAG = BackupDataDirectoryInfo.class.getSimpleName(); @SuppressLint("SdCardPath") @NonNull public static BackupDataDirectoryInfo getInfo(@NonNull String dataDir, @UserIdInt int userId) { String storageCe = String.format(Locale.ROOT, "/data/user/%d/", userId); if (dataDir.startsWith("/data/data/") || dataDir.startsWith(storageCe)) { return new BackupDataDirectoryInfo(dataDir, true, TYPE_INTERNAL, TYPE_CREDENTIAL_PROTECTED); } String storageDe = String.format(Locale.ROOT, "/data/user_de/%d/", userId); if (dataDir.startsWith(storageDe)) { return new BackupDataDirectoryInfo(dataDir, true, TYPE_INTERNAL, TYPE_DEVICE_PROTECTED); } if (dataDir.startsWith("/sdcard/")) { return getExternalInfo(dataDir, "/sdcard/"); } if (dataDir.startsWith("/storage/sdcard/")) { return getExternalInfo(dataDir, "/storage/sdcard/"); } if (dataDir.startsWith("/storage/sdcard0/")) { return getExternalInfo(dataDir, "/storage/sdcard0/"); } String storageEmulatedDir = String.format(Locale.ROOT, "/storage/emulated/%d/", userId); if (dataDir.startsWith(storageEmulatedDir)) { return getExternalInfo(dataDir, storageEmulatedDir); } String dataMediaDir = String.format(Locale.ROOT, "/data/media/%d/", userId); if (dataDir.startsWith(dataMediaDir)) { return getExternalInfo(dataDir, dataMediaDir); } Log.i(TAG, "getInfo: Unrecognized path %s, returning true as fallback.", dataDir); return new BackupDataDirectoryInfo(dataDir, true, TYPE_UNKNOWN, TYPE_CUSTOM); } @NonNull private static BackupDataDirectoryInfo getExternalInfo(@NonNull String dataDir, @NonNull String baseDir) { String relativeDir = dataDir.substring(baseDir.length()); // No starting separator int subType; if (relativeDir.startsWith("Android/data/")) { subType = TYPE_ANDROID_DATA; } else if (relativeDir.startsWith("Android/obb/")) { subType = TYPE_ANDROID_OBB; } else if (relativeDir.startsWith("Android/media/")) { subType = TYPE_ANDROID_MEDIA; } else subType = TYPE_CUSTOM; return new BackupDataDirectoryInfo(dataDir, Paths.get(baseDir).isDirectory(), TYPE_EXTERNAL, subType); } @IntDef(value = { TYPE_INTERNAL, TYPE_EXTERNAL, TYPE_UNKNOWN, }) @Retention(RetentionPolicy.SOURCE) public @interface Type { } @IntDef(value = { TYPE_CUSTOM, TYPE_ANDROID_DATA, TYPE_ANDROID_MEDIA, TYPE_ANDROID_OBB, TYPE_CREDENTIAL_PROTECTED, TYPE_DEVICE_PROTECTED, }) @Retention(RetentionPolicy.SOURCE) public @interface SubType { } public static final int TYPE_CUSTOM = 0; public static final int TYPE_ANDROID_DATA = 1; public static final int TYPE_ANDROID_MEDIA = 2; public static final int TYPE_ANDROID_OBB = 3; public static final int TYPE_CREDENTIAL_PROTECTED = 4; public static final int TYPE_DEVICE_PROTECTED = 5; public static final int TYPE_INTERNAL = 1; public static final int TYPE_EXTERNAL = 2; public static final int TYPE_UNKNOWN = 3; @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) public final String rawPath; @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) public final Path path; public final boolean isMounted; @Type public final int type; @SubType public final int subtype; private BackupDataDirectoryInfo(String path, boolean isMounted, @Type int type, @SubType int subtype) { this.rawPath = path; this.path = Paths.get(path); this.isMounted = isMounted; this.type = type; this.subtype = subtype; } public Path getDirectory() { return path; } public boolean isExternal() { return type == TYPE_EXTERNAL; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/backup/BackupException.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.backup; import androidx.annotation.NonNull; public class BackupException extends Throwable { @NonNull private final String mDetailMessage; public BackupException(@NonNull String message) { super(message); mDetailMessage = message; } public BackupException(@NonNull String message, @NonNull Throwable cause) { super(message, cause); mDetailMessage = message; } @NonNull @Override public String getMessage() { return mDetailMessage; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/backup/BackupFlags.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.backup; import static io.github.muntashirakon.AppManager.utils.UIUtils.getSmallerText; import android.content.Context; import android.text.SpannableStringBuilder; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.core.util.Pair; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Objects; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.compat.ManifestCompat; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.users.Users; import io.github.muntashirakon.AppManager.utils.ArrayUtils; public final class BackupFlags { @IntDef(flag = true, value = { BACKUP_NOTHING, BACKUP_CUSTOM_USERS, BACKUP_SOURCE, BACKUP_APK_FILES, BACKUP_INT_DATA, BACKUP_EXT_DATA, BACKUP_ADB_DATA, BACKUP_EXT_OBB_MEDIA, BACKUP_EXCLUDE_CACHE, BACKUP_EXTRAS, BACKUP_CACHE, BACKUP_MULTIPLE, BACKUP_RULES, BACKUP_NO_SIGNATURE_CHECK, }) @Retention(RetentionPolicy.SOURCE) public @interface BackupFlag { } public static final int BACKUP_NOTHING = 0; @SuppressWarnings("PointlessBitwiseExpression") @Deprecated private static final int BACKUP_SOURCE = 1 << 0; public static final int BACKUP_INT_DATA = 1 << 1; public static final int BACKUP_EXT_DATA = 1 << 2; @Deprecated private static final int BACKUP_EXCLUDE_CACHE = 1 << 3; public static final int BACKUP_RULES = 1 << 4; public static final int BACKUP_NO_SIGNATURE_CHECK = 1 << 5; public static final int BACKUP_APK_FILES = 1 << 6; public static final int BACKUP_EXT_OBB_MEDIA = 1 << 7; public static final int BACKUP_CUSTOM_USERS = 1 << 8; public static final int BACKUP_MULTIPLE = 1 << 9; public static final int BACKUP_EXTRAS = 1 << 10; public static final int BACKUP_CACHE = 1 << 11; public static final int BACKUP_ADB_DATA = 1 << 12; private static final LinkedHashMap> sBackupFlagsMap = new LinkedHashMap>() {{ put(BACKUP_APK_FILES, new Pair<>(R.string.backup_apk_files, R.string.backup_apk_files_description)); put(BACKUP_INT_DATA, new Pair<>(R.string.internal_data, R.string.backup_internal_data_description)); put(BACKUP_EXT_DATA, new Pair<>(R.string.external_data, R.string.backup_external_data_description)); put(BACKUP_ADB_DATA, new Pair<>(R.string.adb_data, R.string.adb_data_description)); put(BACKUP_EXT_OBB_MEDIA, new Pair<>(R.string.backup_obb_media, R.string.backup_obb_media_description)); put(BACKUP_CACHE, new Pair<>(R.string.backup_cache, R.string.backup_cache_description)); put(BACKUP_EXTRAS, new Pair<>(R.string.backup_extras, R.string.backup_extras_description)); put(BACKUP_RULES, new Pair<>(R.string.rules, R.string.backup_rules_description)); put(BACKUP_MULTIPLE, new Pair<>(R.string.backup_multiple, R.string.backup_multiple_description)); put(BACKUP_CUSTOM_USERS, new Pair<>(R.string.backup_custom_users, R.string.backup_custom_users_description)); put(BACKUP_NO_SIGNATURE_CHECK, new Pair<>(R.string.skip_signature_checks, R.string.backup_skip_signature_checks_description)); }}; @BackupFlag public static int getSupportedBackupFlags() { List backupFlags = getSupportedBackupFlagsAsArray(); int flags = 0; for (int flag : backupFlags) { flags |= flag; } return flags; } @NonNull public static List getSupportedBackupFlagsAsArray() { List backupFlags = new ArrayList<>(); backupFlags.add(BACKUP_APK_FILES); if (SelfPermissions.canWriteToDataData()) { backupFlags.add(BACKUP_INT_DATA); } backupFlags.add(BACKUP_EXT_DATA); if (SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.BACKUP)) { backupFlags.add(BACKUP_ADB_DATA); } backupFlags.add(BACKUP_EXT_OBB_MEDIA); backupFlags.add(BACKUP_CACHE); backupFlags.add(BACKUP_EXTRAS); backupFlags.add(BACKUP_RULES); backupFlags.add(BACKUP_MULTIPLE); if (Users.getUsersIds().length > 1) { // Display custom users only if multiple users present backupFlags.add(BACKUP_CUSTOM_USERS); } backupFlags.add(BACKUP_NO_SIGNATURE_CHECK); return backupFlags; } @NonNull public static List getBackupFlagsAsArray(@BackupFlag int flags) { flags = migrate(flags); List backupFlags = new ArrayList<>(); if ((flags & BACKUP_APK_FILES) != 0) { backupFlags.add(BACKUP_APK_FILES); } if ((flags & BACKUP_INT_DATA) != 0) { backupFlags.add(BACKUP_INT_DATA); } if ((flags & BACKUP_EXT_DATA) != 0) { backupFlags.add(BACKUP_EXT_DATA); } if ((flags & BACKUP_ADB_DATA) != 0) { backupFlags.add(BACKUP_ADB_DATA); } if ((flags & BACKUP_EXT_OBB_MEDIA) != 0) { backupFlags.add(BACKUP_EXT_OBB_MEDIA); } if ((flags & BACKUP_CACHE) != 0) { backupFlags.add(BACKUP_CACHE); } if ((flags & BACKUP_EXTRAS) != 0) { backupFlags.add(BACKUP_EXTRAS); } if ((flags & BACKUP_RULES) != 0) { backupFlags.add(BACKUP_RULES); } if ((flags & BACKUP_MULTIPLE) != 0) { backupFlags.add(BACKUP_MULTIPLE); } if ((flags & BACKUP_CUSTOM_USERS) != 0) { backupFlags.add(BACKUP_CUSTOM_USERS); } if ((flags & BACKUP_NO_SIGNATURE_CHECK) != 0) { backupFlags.add(BACKUP_NO_SIGNATURE_CHECK); } return backupFlags; } @NonNull public static CharSequence[] getFormattedFlagNames(@NonNull Context context, List backupFlags) { // Reset backup flags CharSequence[] flagNames = new CharSequence[backupFlags.size()]; for (int i = 0; i < flagNames.length; ++i) { Pair flagNamePair = Objects.requireNonNull(sBackupFlagsMap.get(backupFlags.get(i))); flagNames[i] = new SpannableStringBuilder() .append(context.getText(flagNamePair.first)) .append("\n") .append(getSmallerText(context.getText(flagNamePair.second))); } return flagNames; } @BackupFlag private int mFlags; @NonNull public static BackupFlags fromPref() { return new BackupFlags(getSanitizedFlags(Prefs.BackupRestore.getBackupFlags())); } public BackupFlags(@BackupFlag int flags) { mFlags = flags; } @BackupFlag public int getFlags() { return mFlags; } public void addFlag(@BackupFlag int flag) { mFlags |= flag; } public void removeFlag(@BackupFlag int flag) { mFlags &= ~flag; } public void setFlags(int flags) { mFlags = flags; } @NonNull public int[] flagsToCheckedIndexes(@NonNull List enabledFlags) { List indexes = new ArrayList<>(); for (int i = 0; i < enabledFlags.size(); ++i) { int flag = enabledFlags.get(i); if ((mFlags & flag) != 0) { indexes.add(i); } } return ArrayUtils.convertToIntArray(indexes); } public boolean isEmpty() { return mFlags == 0; } public boolean backupApkFiles() { return (mFlags & BACKUP_APK_FILES) != 0; } public boolean backupInternalData() { return (mFlags & BACKUP_INT_DATA) != 0; } public boolean backupExternalData() { return (mFlags & BACKUP_EXT_DATA) != 0; } public boolean backupAdbData() { return (mFlags & BACKUP_ADB_DATA) != 0; } public boolean backupMediaObb() { return (mFlags & BACKUP_EXT_OBB_MEDIA) != 0; } public boolean backupData() { return backupInternalData() || backupExternalData() || backupAdbData() || backupMediaObb(); } public boolean backupRules() { return (mFlags & BACKUP_RULES) != 0; } public boolean backupExtras() { return (mFlags & BACKUP_EXTRAS) != 0; } public boolean backupCache() { return (mFlags & BACKUP_CACHE) != 0; } @SuppressWarnings("BooleanMethodIsAlwaysInverted") public boolean skipSignatureCheck() { return (mFlags & BACKUP_NO_SIGNATURE_CHECK) != 0; } public boolean backupMultiple() { return (mFlags & BACKUP_MULTIPLE) != 0; } public boolean backupCustomUsers() { return (mFlags & BACKUP_CUSTOM_USERS) != 0; } @NonNull public CharSequence toLocalisedString(Context context) { StringBuilder sb = new StringBuilder(); boolean append = false; if (backupApkFiles()) { sb.append("APK"); append = true; } if (backupInternalData()) { sb.append(append ? "+" : "").append("Int"); append = true; } if (backupExternalData()) { sb.append(append ? "+" : "").append("Ext"); append = true; } if (backupAdbData()) { sb.append(append ? "+" : "").append("ADB"); append = true; } if (backupMediaObb()) { sb.append(append ? "+" : "").append("OBB"); append = true; } if (backupRules()) { sb.append(append ? "+" : "").append("Rules"); append = true; } if (backupExtras()) { sb.append(append ? "+" : "").append("Extras"); append = true; } if (backupCache()) { sb.append(append ? "+" : "").append("Caches"); } return sb; } /** * Remove unsupported flags from the given list of flags */ private static int getSanitizedFlags(int flags) { if (!SelfPermissions.canWriteToDataData()) { flags &= ~BACKUP_INT_DATA; } if (Users.getUsersIds().length == 1) { flags &= ~BACKUP_CUSTOM_USERS; } return migrate(flags); } private static int migrate(int flags) { if ((flags & BACKUP_SOURCE) != 0) { // BACKUP_SOURCE is replaced with BACKUP_APK_FILES flags &= ~BACKUP_SOURCE; flags |= BACKUP_APK_FILES; } if ((flags & BACKUP_EXCLUDE_CACHE) != 0) { // BACKUP_EXCLUDE_CACHE is inversely replaced with BACKUP_CACHE flags &= ~BACKUP_EXCLUDE_CACHE; flags &= ~BACKUP_CACHE; } return flags; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/backup/BackupItems.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.backup; import static io.github.muntashirakon.AppManager.backup.BackupManager.KEYSTORE_PREFIX; import android.annotation.UserIdInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.Closeable; import java.io.FileNotFoundException; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.UUID; import java.util.stream.Collectors; import io.github.muntashirakon.AppManager.backup.struct.BackupMetadataV5; import io.github.muntashirakon.AppManager.crypto.Crypto; import io.github.muntashirakon.AppManager.crypto.DummyCrypto; import io.github.muntashirakon.AppManager.db.entity.Backup; import io.github.muntashirakon.AppManager.logcat.helper.SaveLogHelper; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.PathReader; import io.github.muntashirakon.io.PathWriter; import io.github.muntashirakon.io.Paths; public class BackupItems { public static final String BACKUP_DIRECTORY = "backups"; private static final String APK_SAVING_DIRECTORY = "apks"; private static final String ICON_FILE = "icon.png"; private static final String RULES_TSV = "rules.am.tsv"; private static final String MISC_TSV = "misc.am.tsv"; private static final String CHECKSUMS_TXT = "checksums.txt"; private static final String FREEZE = ".freeze"; private static final String NO_MEDIA = ".nomedia"; @NonNull private static Path getBaseDirectory() { return Prefs.Storage.getAppManagerDirectory(); } @NonNull public static BackupItem findBackupItem(@NonNull String relativeDir) throws FileNotFoundException { return new BackupItem(getBaseDirectory().findFile(relativeDir)); } @NonNull public static BackupItem findOrCreateBackupItem(@UserIdInt int userId, @Nullable String backupName, @NonNull String packageName) throws IOException { Path backupPath; List previousBackupItems = null; if (MetadataManager.getCurrentBackupMetaVersion() >= 5) { List previousBackups = BackupUtils.retrieveBackupFromDb(userId, backupName, packageName); if (!previousBackups.isEmpty()) { previousBackupItems = new ArrayList<>(previousBackups.size()); for (Backup backup : previousBackups) { previousBackupItems.add(backup.getItem()); } } String backupUuid = UUID.randomUUID().toString(); backupPath = getBaseDirectory() .findOrCreateDirectory(BACKUP_DIRECTORY) .findOrCreateDirectory(backupUuid); } else { backupPath = getBaseDirectory() .findOrCreateDirectory(packageName) .findOrCreateDirectory(BackupUtils.getV4BackupName(userId, backupName)); } BackupItem backupItem = new BackupItem(backupPath, true); backupItem.setBackupName(BackupUtils.getCompatBackupName(backupName)); backupItem.setPreviousBackups(previousBackupItems); return backupItem; } @NonNull public static BackupItem createBackupItemGracefully(@UserIdInt int userId, @Nullable String backupName, @NonNull String packageName) throws IOException { Path backupPath; if (MetadataManager.getCurrentBackupMetaVersion() >= 5) { String backupUuid = UUID.randomUUID().toString(); backupPath = getBaseDirectory() .findOrCreateDirectory(BACKUP_DIRECTORY) .findOrCreateDirectory(backupUuid); } else { Path baseDir = getBaseDirectory().findOrCreateDirectory(packageName); String backupItemName = BackupUtils.getV4BackupName(userId, backupName); String newBackupName = backupItemName; int i = 0; while (baseDir.hasFile(newBackupName)) { newBackupName = backupItemName + "_" + (++i); } backupPath = baseDir.createNewDirectory(newBackupName); } BackupItem backupItem = new BackupItem(backupPath, true); backupItem.setBackupName(BackupUtils.getCompatBackupName(backupName)); return backupItem; } @NonNull public static List findAllBackupItems() { Path baseDirectory = getBaseDirectory(); Path[] paths = baseDirectory.listFiles(Path::isDirectory); List backupItems = new ArrayList<>(paths.length); for (Path path : paths) { if (SaveLogHelper.SAVED_LOGS_DIR.equals(path.getName())) { continue; } if (APK_SAVING_DIRECTORY.equals(path.getName())) { continue; } if (".tmp".equals(path.getName())) { continue; } // Other backups can store multiple backups per folder backupItems.addAll(Arrays.stream(path.listFiles(Path::isDirectory)) .map(BackupItem::new) .collect(Collectors.toList())); } // We don't need to check further at this stage. // It's the caller's job to check the contents if needed. return backupItems; } @NonNull private static synchronized Path getTemporaryUnencryptedPath(@NonNull String backupName) throws IOException { Path tmpDir = Prefs.Storage.getTempPath(); String newFilename = backupName; int i = 0; while (tmpDir.hasFile(newFilename)) { newFilename = backupName + "_" + (++i); } return tmpDir.findOrCreateDirectory(newFilename); } @NonNull private static synchronized Path getTemporaryBackupPath(@NonNull Path originalBackupPath) throws IOException { Path tmpDir = originalBackupPath.requireParent(); String tmpFilename = "." + originalBackupPath.getName(); String newFilename = tmpFilename; int i = 0; while (tmpDir.hasFile(newFilename)) { newFilename = tmpFilename + "_" + (++i); } return tmpDir.findOrCreateDirectory(newFilename); } @NonNull public static Path getApkBackupDirectory() throws IOException { return getBaseDirectory().findOrCreateDirectory(APK_SAVING_DIRECTORY); } public static void createNoMediaIfNotExists() throws IOException { Path backupDirectory = getBaseDirectory(); if (!backupDirectory.hasFile(NO_MEDIA)) { backupDirectory.createNewFile(NO_MEDIA, null); } } public static class BackupItem { public static final String TAG = BackupItem.class.getSimpleName(); @NonNull private final Path mBackupPath; @NonNull private final Path mTempBackupPath; private final Object mCryptoGuard = new Object(); @Nullable private Crypto mCrypto; @CryptoUtils.Mode private String mCryptoMode = CryptoUtils.MODE_NO_ENCRYPTION; @Nullable private String mBackupName; private boolean mBackupNameSet = false; private boolean mBackupMode; private boolean mBackupSuccess = false; private final List mTemporaryFiles = new ArrayList<>(); private Path mTempUnencyptedPath; @Nullable private List mPreviousBackups; private BackupItem(@NonNull Path backupPath, boolean backupMode) throws IOException { mBackupPath = backupPath; mBackupMode = backupMode; if (mBackupMode) { mBackupPath.mkdirs(); // Create backup path if not exists mTempBackupPath = getTemporaryBackupPath(mBackupPath); } else mTempBackupPath = mBackupPath; } // Read-only instance: the point is not to throw IOException private BackupItem(@NonNull Path backupPath) { mBackupPath = backupPath; mBackupMode = false; mTempBackupPath = mBackupPath; } public void setCrypto(@Nullable Crypto crypto) { if (crypto == null || crypto instanceof DummyCrypto) { mCrypto = null; mCryptoMode = CryptoUtils.MODE_NO_ENCRYPTION; } else { mCrypto = crypto; mCryptoMode = crypto.getModeName(); } } public void setBackupName(@Nullable String backupName) { mBackupName = backupName; mBackupNameSet = true; } @Nullable public String getBackupName() { if (mBackupNameSet) { return mBackupName; } if (mBackupMode) { throw new IllegalStateException("mBackupName must be set in backup mode."); } if (isV5AndUp()) { throw new IllegalStateException("getBackupName() is unavailable in backup v5 and up unless set manually."); } // For v4 or earlier backups, fallback to filename return BackupUtils.getRealBackupName(4, mBackupPath.getName()); } public void setPreviousBackups(@Nullable List previousBackups) { mPreviousBackups = previousBackups; } public String getRelativeDir() { if (isV5AndUp()) { // {AppManagerDir}/backups/{UUID}/ return BackupUtils.getV5RelativeDir(mBackupPath.getName()); } else { // {AppManagerDir}/{packagename}/{userid}[_{backup_name}] String userIdBackupName = mBackupPath.getName(); String packageName = mBackupPath.requireParent().getName(); return BackupUtils.getV4RelativeDir(userIdBackupName, packageName); } } public boolean isBackupMode() { return mBackupMode; } @NonNull public Path getBackupPath() { return mBackupMode ? mTempBackupPath : mBackupPath; } public Path getUnencryptedBackupPath() throws IOException { if (mCrypto == null) { // Use real path for unencrypted backups return getBackupPath(); } else { return requireUnencryptedBackupPath(); } } public Path requireUnencryptedBackupPath() throws IOException { if (mTempUnencyptedPath == null) { // We can only do this once for each BackupItem mTempUnencyptedPath = getTemporaryUnencryptedPath(getBackupPath().getName()); } return mTempUnencyptedPath; } @NonNull public Path[] encrypt(@NonNull Path[] files) throws IOException { // Encrypt the files and delete the originals synchronized (mCryptoGuard) { if (mCrypto == null) { // No encryption enabled return files; } List newFileList = new ArrayList<>(); // Get desired extension String ext = CryptoUtils.getExtension(mCryptoMode); // Create necessary files (1-1 correspondence) for (Path inputFile : files) { Path parent = getBackupPath(); String outputFilename = inputFile.getName() + ext; Path outputPath = parent.createNewFile(outputFilename, null); newFileList.add(outputPath); Log.i(TAG, "Input: %s\nOutput: %s", inputFile, outputPath); } Path[] newFiles = newFileList.toArray(new Path[0]); // Perform actual encryption mCrypto.encrypt(files, newFiles); // Delete unencrypted files for (Path inputFile : files) { if (!inputFile.delete()) { throw new IOException("Couldn't delete old file " + inputFile); } } return newFiles; } } @NonNull public Path[] decrypt(@NonNull Path[] files) throws IOException { // Decrypt the files but do NOT delete the originals synchronized (mCryptoGuard) { if (mCrypto == null) { // No encryption enabled return files; } List newFileList = new ArrayList<>(); // Get desired extension String ext = CryptoUtils.getExtension(mCryptoMode); // Create necessary files (1-1 correspondence) for (Path inputFile : files) { Path parent = getUnencryptedBackupPath(); String filename = inputFile.getName(); String outputFilename = filename.substring(0, filename.lastIndexOf(ext)); Path outputPath = parent.createNewFile(outputFilename, null); newFileList.add(outputPath); Log.i(TAG, "Input: %s\nOutput: %s", inputFile, outputPath); } Path[] newFiles = newFileList.toArray(new Path[0]); // Perform actual decryption mCrypto.decrypt(files, newFiles); mTemporaryFiles.addAll(newFileList); return newFiles; } } @NonNull public Path getIconFile() throws IOException { // Icon is never encrypted if (mBackupMode) { return getBackupPath().findOrCreateFile(ICON_FILE, null); } else return getBackupPath().findFile(ICON_FILE); } public boolean isV5AndUp() { return getBackupPath().hasFile(MetadataManager.INFO_V5_FILE); } public Path getInfoFile() throws IOException { // info_v5.am.json is never encrypted if (mBackupMode) { return getBackupPath().findOrCreateFile(MetadataManager.INFO_V5_FILE, null); } else return getBackupPath().findFile(MetadataManager.INFO_V5_FILE); } public Path getMetadataV5File(boolean decryptIfRequired) throws IOException { if (mBackupMode) { // Needs to be encrypted in backup mode return getBackupPath().findOrCreateFile(MetadataManager.META_V5_FILE, null); } else { // Needs to be decrypted in restore mode Path file = getBackupPath().findFile(MetadataManager.META_V5_FILE + CryptoUtils.getExtension(mCryptoMode)); return decryptIfRequired ? decrypt(new Path[]{file})[0] : file; } } @NonNull public Path getMetadataV2File() throws IOException { // meta_v2.am.json is never encrypted if (mBackupMode) { return getBackupPath().findOrCreateFile(MetadataManager.META_V2_FILE, null); } else return getBackupPath().findFile(MetadataManager.META_V2_FILE); } public BackupMetadataV5.Info getInfo() throws IOException { return MetadataManager.readInfo(this); } public BackupMetadataV5 getMetadata() throws IOException { return MetadataManager.readMetadata(this); } public BackupMetadataV5 getMetadata(BackupMetadataV5.Info backupInfo) throws IOException { return MetadataManager.readMetadata(this, backupInfo); } @NonNull private Path getChecksumFile() throws IOException { if (mBackupMode) { // Needs to be encrypted in backup mode return getUnencryptedBackupPath().findOrCreateFile(CHECKSUMS_TXT, null); } else { // Needs to be decrypted in restore mode Path file = getBackupPath().findFile(CHECKSUMS_TXT + CryptoUtils.getExtension(mCryptoMode)); return decrypt(new Path[]{file})[0]; } } @NonNull public Checksum getChecksum() throws IOException { return new Checksum(getChecksumFile(), mBackupMode ? "w" : "r"); } @NonNull public Path getMiscFile() throws IOException { if (mBackupMode) { // Needs to be encrypted in backup mode return getUnencryptedBackupPath().findOrCreateFile(MISC_TSV, null); } else { // Needs to be decrypted in restore mode return getBackupPath().findFile(MISC_TSV + CryptoUtils.getExtension(mCryptoMode)); } } @NonNull public Path getRulesFile() throws IOException { if (mBackupMode) { // Needs to be encrypted in backup mode return getUnencryptedBackupPath().findOrCreateFile(RULES_TSV, null); } else { // Needs to be decrypted in restore mode return getBackupPath().findFile(RULES_TSV + CryptoUtils.getExtension(mCryptoMode)); } } @NonNull public Path[] getSourceFiles() { String ext = CryptoUtils.getExtension(mCryptoMode); final String sourcePrefix = BackupUtils.getSourceFilePrefix(null); Path[] paths = getBackupPath().listFiles((dir, name) -> name.startsWith(sourcePrefix) && name.endsWith(ext)); return Paths.getSortedPaths(paths); } @NonNull public Path[] getDataFiles(int index) { String ext = CryptoUtils.getExtension(mCryptoMode); final String dataPrefix = BackupUtils.getDataFilePrefix(index, null); // extension can be anything Path[] paths = getBackupPath().listFiles((dir, name) -> name.startsWith(dataPrefix) && name.endsWith(ext)); return Paths.getSortedPaths(paths); } @NonNull public Path[] getKeyStoreFiles() { String ext = CryptoUtils.getExtension(mCryptoMode); Path[] paths = getBackupPath().listFiles((dir, name) -> name.startsWith(KEYSTORE_PREFIX) && name.endsWith(ext)); return Paths.getSortedPaths(paths); } public void freeze() throws IOException { getBackupPath().createNewFile(FREEZE, null); } public void unfreeze() throws FileNotFoundException { getFreezeFile().delete(); } public boolean isFrozen() { try { return getFreezeFile().exists(); } catch (IOException e) { return false; } } public void commit() throws IOException { if (mBackupMode) { if (mBackupSuccess) { // Backup already done return; } if (!delete()) { throw new IOException("Could not delete " + mBackupPath); } if (!mTempBackupPath.moveTo(mBackupPath)) { throw new IOException("Could not move " + mTempBackupPath + " to " + mBackupPath); } if (mPreviousBackups != null) { for (BackupItem previousBackup : mPreviousBackups) { if (!previousBackup.delete()) { Log.w(TAG, "Could not delete %s", previousBackup.mBackupPath); } } } mBackupSuccess = true; // Set backup mode to false to make it read-only mBackupMode = false; } } public void cleanup() { if (mBackupMode) { if (!mBackupSuccess) { // Backup wasn't successful, delete the directory mTempBackupPath.delete(); } } for (Path file : mTemporaryFiles) { Log.d(TAG, "Deleting %s", file); file.delete(); } if (mTempUnencyptedPath != null) { mTempUnencyptedPath.delete(); } if (mCrypto != null) { mCrypto.close(); } } public boolean exists() { return mBackupPath.exists(); } public boolean delete() { if (mBackupPath.exists()) { if (!isV5AndUp()) { // For v4 and earlier, delete parent if it's the last one. Path parent = mBackupPath.requireParent(); if (parent.listFiles().length == 1) { // Also deletes children return parent.delete(); } } return mBackupPath.delete(); } return true; // The backup path doesn't exist anyway } @NonNull private Path getFreezeFile() throws FileNotFoundException { return getBackupPath().findFile(FREEZE); } } public static class Checksum implements Closeable { private PrintWriter mWriter; private final HashMap mChecksums = new HashMap<>(); private final String mMode; private final Path mFile; @NonNull public static String[] getCertChecksums(@NonNull Checksum checksum) { List certChecksums = new ArrayList<>(); synchronized (checksum.mChecksums) { for (String name : checksum.mChecksums.keySet()) { if (name.startsWith(BackupManager.CERT_PREFIX)) { certChecksums.add(checksum.mChecksums.get(name)); } } } return certChecksums.toArray(new String[0]); } Checksum(@NonNull Path checksumFile, String mode) throws IOException { mFile = checksumFile; mMode = mode; if ("w".equals(mode)) { mWriter = new PrintWriter(new BufferedWriter(new PathWriter(checksumFile))); } else if ("r".equals(mode)) { synchronized (mChecksums) { BufferedReader reader = new BufferedReader(new PathReader(checksumFile)); // Get checksums String line; String[] lineSplits; while ((line = reader.readLine()) != null) { lineSplits = line.split("\t", 2); if (lineSplits.length != 2) { throw new RuntimeException("Illegal lines found in the checksum file."); } mChecksums.put(lineSplits[1], lineSplits[0]); } reader.close(); } } else throw new IOException("Unknown mode: " + mode); } public Path getFile() { return mFile; } public void add(@NonNull String fileName, @NonNull String checksum) { synchronized (mChecksums) { if (!"w".equals(mMode)) { throw new IllegalStateException("add is inaccessible in mode " + mMode); } mWriter.println(String.format("%s\t%s", checksum, fileName)); mChecksums.put(fileName, checksum); mWriter.flush(); } } @Nullable String get(String fileName) { synchronized (mChecksums) { return mChecksums.get(fileName); } } @Override public void close() { synchronized (mChecksums) { if (mWriter != null) { mWriter.close(); mWriter = null; } } } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/backup/BackupManager.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.backup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import io.github.muntashirakon.AppManager.backup.struct.BackupMetadataV5; import io.github.muntashirakon.AppManager.backup.struct.BackupOpOptions; import io.github.muntashirakon.AppManager.backup.struct.DeleteOpOptions; import io.github.muntashirakon.AppManager.backup.struct.RestoreOpOptions; import io.github.muntashirakon.AppManager.db.entity.Backup; import io.github.muntashirakon.AppManager.progress.ProgressHandler; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.ExUtils; import io.github.muntashirakon.AppManager.utils.TarUtils; /** * Manage backups for individual package belong to individual user. */ public class BackupManager { public static final String TAG = BackupManager.class.getSimpleName(); /* language=regexp */ static final String[] CACHE_DIRS = new String[]{"cache/.*", "code_cache/.*", "no_backup/.*"}; /* language=regexp */ static final String[] LIB_DIR = new String[]{"lib/"}; public static final String SOURCE_PREFIX = "source"; public static final String DATA_PREFIX = "data"; static final String KEYSTORE_PREFIX = "keystore"; static final int KEYSTORE_PLACEHOLDER = -1000; static final String DATA_BACKUP_SPECIAL_PREFIX = "special:"; static final String DATA_BACKUP_SPECIAL_ADB = DATA_BACKUP_SPECIAL_PREFIX + "adb"; public static final String CERT_PREFIX = "cert_"; static final String MASTER_KEY = ".masterkey"; @NonNull public static String getExt(@TarUtils.TarType String tarType) { if (TarUtils.TAR_BZIP2.equals(tarType)) { return ".tar.bz2"; } else if (TarUtils.TAR_ZSTD.equals(tarType)) { return ".tar.zst"; } else return ".tar.gz"; } private boolean mRequiresRestart; public BackupManager() { ExUtils.exceptionAsIgnored(BackupItems::createNoMediaIfNotExists); } public boolean requiresRestart() { return mRequiresRestart; } public void backup(@NonNull BackupOpOptions options, @Nullable ProgressHandler progressHandler) throws BackupException { if (options.packageName.equals("android")) { throw new BackupException("Android System (android) cannot be backed up."); } if (options.flags.isEmpty()) { throw new BackupException("Backup is requested without any flags."); } BackupItems.BackupItem backupItem; try { if (options.override) { backupItem = BackupItems.findOrCreateBackupItem(options.userId, options.backupName, options.packageName); } else { backupItem = BackupItems.createBackupItemGracefully(options.userId, options.backupName, options.packageName); } } catch (IOException e) { throw new BackupException("Could not create BackupItem.", e); } if (progressHandler != null) { int max = calculateMaxProgress(options.flags); progressHandler.setProgressTextInterface(ProgressHandler.PROGRESS_PERCENT); progressHandler.postUpdate(max, 0f); } try (BackupOp backupOp = new BackupOp(options.packageName, options.flags, backupItem, options.userId)) { backupOp.runBackup(progressHandler); BackupUtils.putBackupToDbAndBroadcast(ContextUtils.getContext(), backupOp.getMetadata()); } } /** * Restore a single backup for a given package belonging to the given package */ public void restore(@NonNull RestoreOpOptions options, @Nullable ProgressHandler progressHandler) throws BackupException { if (options.packageName.equals("android")) { throw new BackupException("Android System (android) cannot be restored."); } if (options.flags.isEmpty()) { throw new BackupException("Restore is requested without any flags."); } BackupItems.BackupItem backupItem; try { if (options.relativeDir != null) { backupItem = BackupItems.findBackupItem(options.relativeDir); } else { // Use base backup Backup baseBackup = BackupUtils.retrieveBaseBackupFromDb(options.userId, options.packageName); if (baseBackup != null) { backupItem = baseBackup.getItem(); } else { throw new BackupException("No base backup found."); } } } catch (IOException e) { throw new BackupException("Could not get backup files.", e); } if (progressHandler != null) { int max = calculateMaxProgress(options.flags); progressHandler.setProgressTextInterface(ProgressHandler.PROGRESS_PERCENT); progressHandler.postUpdate(max, 0f); } try (RestoreOp restoreOp = new RestoreOp(options.packageName, options.flags, backupItem, options.userId)) { restoreOp.runRestore(progressHandler); mRequiresRestart |= restoreOp.requiresRestart(); } } public void deleteBackup(@NonNull DeleteOpOptions options) throws BackupException { List backupItemList; if (options.relativeDirs == null) { // Delete base backup Backup baseBackup = BackupUtils.retrieveBaseBackupFromDb(options.userId, options.packageName); if (baseBackup != null) { try { backupItemList = Collections.singletonList(baseBackup.getItem()); } catch (IOException e) { throw new BackupException("Could not get backup files.", e); } } else backupItemList = Collections.emptyList(); } else { backupItemList = new ArrayList<>(options.relativeDirs.length); for (String relativeDir : options.relativeDirs) { try { backupItemList.add(BackupItems.findBackupItem(relativeDir)); } catch (IOException e) { throw new BackupException("Could not get backup files.", e); } } } for (BackupItems.BackupItem backupItem : backupItemList) { BackupMetadataV5 metadata; try { metadata = backupItem.getMetadata(); } catch (IOException e) { throw new BackupException("Could not retrieve metadata from backup.", e); } if (!backupItem.isFrozen() && !backupItem.delete()) { throw new BackupException("Could not delete the selected backups"); } BackupUtils.deleteBackupToDbAndBroadcast(ContextUtils.getContext(), metadata); } } public void verify(@NonNull String relativeDir) throws BackupException { BackupItems.BackupItem backupItem; try { backupItem = BackupItems.findBackupItem(relativeDir); } catch (IOException e) { throw new BackupException("Could not get backup files.", e); } try (VerifyOp restoreOp = new VerifyOp(backupItem)) { restoreOp.verify(); } } private static int calculateMaxProgress(@NonNull BackupFlags backupFlags) { int tasks = 1; if (backupFlags.backupApkFiles()) ++tasks; if (backupFlags.backupData()) ++tasks; if (backupFlags.backupExtras()) ++tasks; if (backupFlags.backupRules()) ++tasks; return tasks; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/backup/BackupOp.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.backup; import static io.github.muntashirakon.AppManager.backup.BackupManager.CERT_PREFIX; import static io.github.muntashirakon.AppManager.backup.BackupManager.KEYSTORE_PLACEHOLDER; import static io.github.muntashirakon.AppManager.backup.BackupManager.KEYSTORE_PREFIX; import static io.github.muntashirakon.AppManager.backup.BackupManager.MASTER_KEY; import static io.github.muntashirakon.AppManager.backup.BackupManager.getExt; import static io.github.muntashirakon.AppManager.backup.BackupUtils.TAR_TYPES; import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.GET_SIGNING_CERTIFICATES; import android.annotation.UserIdInt; import android.app.INotificationManager; import android.content.ComponentName; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PermissionInfo; import android.graphics.Bitmap; import android.os.Build; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import androidx.core.content.pm.PackageInfoCompat; import androidx.core.content.pm.PermissionInfoCompat; import java.io.Closeable; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.regex.Pattern; import io.github.muntashirakon.AppManager.apk.ApkFile; import io.github.muntashirakon.AppManager.apk.ApkSource; import io.github.muntashirakon.AppManager.backup.struct.BackupMetadataV5; import io.github.muntashirakon.AppManager.compat.AppOpsManagerCompat; import io.github.muntashirakon.AppManager.compat.BackupCompat; import io.github.muntashirakon.AppManager.compat.DeviceIdleManagerCompat; import io.github.muntashirakon.AppManager.compat.ManifestCompat; import io.github.muntashirakon.AppManager.compat.NetworkPolicyManagerCompat; import io.github.muntashirakon.AppManager.compat.PackageManagerCompat; import io.github.muntashirakon.AppManager.compat.PermissionCompat; import io.github.muntashirakon.AppManager.crypto.CryptoException; import io.github.muntashirakon.AppManager.ipc.ProxyBinder; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.magisk.MagiskDenyList; import io.github.muntashirakon.AppManager.magisk.MagiskHide; import io.github.muntashirakon.AppManager.magisk.MagiskProcess; import io.github.muntashirakon.AppManager.misc.OsEnvironment; import io.github.muntashirakon.AppManager.progress.ProgressHandler; import io.github.muntashirakon.AppManager.rules.PseudoRules; import io.github.muntashirakon.AppManager.rules.compontents.ComponentUtils; import io.github.muntashirakon.AppManager.rules.compontents.ComponentsBlocker; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.ssaid.SsaidSettings; import io.github.muntashirakon.AppManager.uri.UriManager; import io.github.muntashirakon.AppManager.utils.ArrayUtils; import io.github.muntashirakon.AppManager.utils.BitmapRandomizer; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.DigestUtils; import io.github.muntashirakon.AppManager.utils.ExUtils; import io.github.muntashirakon.AppManager.utils.FileUtils; import io.github.muntashirakon.AppManager.utils.FreezeUtils; import io.github.muntashirakon.AppManager.utils.KeyStoreUtils; import io.github.muntashirakon.AppManager.utils.PackageUtils; import io.github.muntashirakon.AppManager.utils.ParcelFileDescriptorUtil; import io.github.muntashirakon.AppManager.utils.TarUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.AppManager.utils.Utils; import io.github.muntashirakon.io.IoUtils; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; @WorkerThread class BackupOp implements Closeable { static final String TAG = BackupOp.class.getSimpleName(); @NonNull private final String mPackageName; @NonNull private final BackupItems.BackupItem mBackupItem; @NonNull private final BackupFlags mBackupFlags; @NonNull private final BackupMetadataV5 mMetadata; @NonNull private final PackageInfo mPackageInfo; @NonNull private final ApplicationInfo mApplicationInfo; @UserIdInt private final int mUserId; @NonNull private final BackupItems.Checksum mChecksum; // We don't need privileged package manager here @NonNull private final PackageManager mPm; BackupOp(@NonNull String packageName, @NonNull BackupFlags backupFlags, @NonNull BackupItems.BackupItem backupItem, @UserIdInt int userId) throws BackupException { mPackageName = packageName; mBackupItem = backupItem; mUserId = userId; mBackupFlags = backupFlags; mPm = ContextUtils.getContext().getPackageManager(); try { mPackageInfo = PackageManagerCompat.getPackageInfo(mPackageName, PackageManager.GET_META_DATA | GET_SIGNING_CERTIFICATES | PackageManager.GET_PERMISSIONS | PackageManagerCompat.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES, userId); Objects.requireNonNull(mPackageInfo); mApplicationInfo = Objects.requireNonNull(mPackageInfo.applicationInfo); // Override existing metadata mMetadata = setupMetadataAndCrypto(); } catch (Throwable e) { mBackupItem.cleanup(); throw new BackupException("Failed to setup metadata.", e); } try { mChecksum = mBackupItem.getChecksum(); String[] certChecksums = PackageUtils.getSigningCertChecksums(mMetadata.info.checksumAlgo, mPackageInfo, false); for (int i = 0; i < certChecksums.length; ++i) { mChecksum.add(CERT_PREFIX + i, certChecksums[i]); } } catch (Throwable e) { mBackupItem.cleanup(); throw new BackupException("Failed to create checksum file.", e); } } @Override public void close() { mBackupItem.cleanup(); } @NonNull public BackupMetadataV5 getMetadata() { return mMetadata; } void runBackup(@Nullable ProgressHandler progressHandler) throws BackupException { try { // Fail backup if the app has items in Android KeyStore and backup isn't enabled if (mBackupFlags.backupData() && mMetadata.metadata.keyStore && !Prefs.BackupRestore.backupAppsWithKeyStore()) { throw new BackupException("The app has keystore items and KeyStore backup isn't enabled."); } incrementProgress(progressHandler); // Backup icon backupIcon(); // Backup source if (mBackupFlags.backupApkFiles()) { backupApkFiles(); incrementProgress(progressHandler); } // Backup data if (mBackupFlags.backupData()) { backupData(); // Backup KeyStore if (mMetadata.metadata.keyStore) { backupKeyStore(); } incrementProgress(progressHandler); } // Backup extras if (mBackupFlags.backupExtras()) { backupExtras(); incrementProgress(progressHandler); } // Export rules if (mMetadata.metadata.hasRules) { backupRules(); incrementProgress(progressHandler); } // Write modified metadata try { Map filenameChecksumMap = MetadataManager.writeMetadata(mMetadata, mBackupItem); for (Map.Entry entry : filenameChecksumMap.entrySet()) { mChecksum.add(entry.getKey(), entry.getValue()); } } catch (IOException e) { throw new BackupException("Failed to write metadata.", e); } mChecksum.close(); // Encrypt checksum try { mBackupItem.encrypt(new Path[]{mChecksum.getFile()}); } catch (IOException e) { throw new BackupException("Failed to write checksums.txt", e); } // Replace current backup try { mBackupItem.commit(); } catch (IOException e) { throw new BackupException("Could not finalise backup.", e); } } catch (BackupException e) { throw e; } catch (Throwable th) { throw new BackupException("Unknown error occurred.", th); } } private static void incrementProgress(@Nullable ProgressHandler progressHandler) { if (progressHandler == null) { return; } float current = progressHandler.getLastProgress() + 1; progressHandler.postUpdate(current); } public BackupMetadataV5 setupMetadataAndCrypto() throws CryptoException { // We don't need to backup custom users or multiple backup flags mBackupFlags.removeFlag(BackupFlags.BACKUP_CUSTOM_USERS | BackupFlags.BACKUP_MULTIPLE); String backupName = mBackupItem.getBackupName(); long backupTime = System.currentTimeMillis(); String tarType = Prefs.BackupRestore.getCompressionMethod(); // Verify tar type if (ArrayUtils.indexOf(TAR_TYPES, tarType) == -1) { // Unknown tar type, set default tarType = TarUtils.TAR_GZIP; } String crypto = CryptoUtils.getMode(); BackupCryptSetupHelper cryptoHelper = new BackupCryptSetupHelper(crypto, MetadataManager.getCurrentBackupMetaVersion()); mBackupItem.setCrypto(cryptoHelper.crypto); BackupMetadataV5.Info backupInfo = new BackupMetadataV5.Info(backupTime, mBackupFlags, mUserId, tarType, DigestUtils.SHA_256, crypto, cryptoHelper.getIv(), cryptoHelper.getAes(), cryptoHelper.getKeyIds()); backupInfo.setBackupItem(mBackupItem); BackupMetadataV5.Metadata metadata = new BackupMetadataV5.Metadata(backupName); metadata.keyStore = KeyStoreUtils.hasKeyStore(mApplicationInfo.uid); metadata.label = mApplicationInfo.loadLabel(mPm).toString(); metadata.packageName = mPackageName; metadata.versionName = mPackageInfo.versionName; metadata.versionCode = PackageInfoCompat.getLongVersionCode(mPackageInfo); metadata.apkName = new File(mApplicationInfo.sourceDir).getName(); String[] dataDirs = null; if (mBackupFlags.backupAdbData()) { if (BackupCompat.isAppEligibleForBackupForUser(mUserId, mPackageName)) { mBackupFlags.removeFlag(BackupFlags.BACKUP_INT_DATA); mBackupFlags.removeFlag(BackupFlags.BACKUP_EXT_DATA); List defaultDirs = BackupUtils.getDataDirectories(mApplicationInfo, false, false, mBackupFlags.backupMediaObb()); dataDirs = new String[defaultDirs.size() + 1]; for (int i = 0; i < defaultDirs.size(); ++i) { dataDirs[i] = defaultDirs.get(i); } dataDirs[defaultDirs.size()] = BackupManager.DATA_BACKUP_SPECIAL_ADB; } else { // ADB backup cannot be used. mBackupFlags.removeFlag(BackupFlags.BACKUP_ADB_DATA); mBackupFlags.addFlag(BackupFlags.BACKUP_INT_DATA); mBackupFlags.addFlag(BackupFlags.BACKUP_EXT_DATA); } } if (dataDirs == null) { // Non-ADB backup: default dataDirs = BackupUtils.getDataDirectories(mApplicationInfo, mBackupFlags.backupInternalData(), mBackupFlags.backupExternalData(), mBackupFlags.backupMediaObb()).toArray(new String[0]); } metadata.dataDirs = dataDirs; metadata.isSystem = (mApplicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; metadata.isSplitApk = false; try (ApkFile apkFile = ApkSource.getApkSource(mApplicationInfo).resolve()) { if (apkFile.isSplit()) { List apkEntries = apkFile.getEntries(); int splitCount = apkEntries.size() - 1; metadata.isSplitApk = splitCount > 0; metadata.splitConfigs = new String[splitCount]; for (int i = 0; i < splitCount; ++i) { metadata.splitConfigs[i] = apkEntries.get(i + 1).getFileName(); } } } catch (ApkFile.ApkFileException e) { e.printStackTrace(); } metadata.splitConfigs = ArrayUtils.defeatNullable(metadata.splitConfigs); metadata.hasRules = false; if (mBackupFlags.backupRules()) { try (ComponentsBlocker cb = ComponentsBlocker.getInstance(mPackageInfo.packageName, mUserId, false)) { metadata.hasRules = cb.entryCount() > 0; } } metadata.installer = PackageManagerCompat.getInstallerPackageName(mPackageInfo.packageName, mUserId); return new BackupMetadataV5(backupInfo, metadata); } private void backupIcon() { try { Path iconFile = mBackupItem.getIconFile(); try (OutputStream outputStream = iconFile.openOutputStream()) { Bitmap bitmap = UIUtils.getMutableBitmapFromDrawable(mApplicationInfo.loadIcon(mPm)); BitmapRandomizer.randomizePixel(bitmap); bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream); } } catch (IOException e) { Log.w(TAG, "Could not back up icon."); } } private void backupApkFiles() throws BackupException { Path dataAppPath = OsEnvironment.getDataAppDirectory(); final String sourceBackupFilePrefix = BackupUtils.getSourceFilePrefix(getExt(mMetadata.info.tarType)); Path sourceDir = Paths.get(PackageUtils.getSourceDir(mApplicationInfo)); if (dataAppPath.equals(sourceDir)) { // APK located inside /data/app directory // Backup only the apk file (no split apk support for this type of apk) try { sourceDir = sourceDir.findFile(mMetadata.metadata.apkName); } catch (FileNotFoundException e) { throw new BackupException(mMetadata.metadata.apkName + " not found at " + sourceDir); } } Path[] sourceFiles; try { sourceFiles = TarUtils.create(mMetadata.info.tarType, sourceDir, mBackupItem.getUnencryptedBackupPath(), sourceBackupFilePrefix, /* language=regexp */ new String[]{".*\\.apk"}, null, null, false).toArray(new Path[0]); } catch (Throwable th) { throw new BackupException("APK files backup is requested but no source directory has been backed up.", th); } try { sourceFiles = mBackupItem.encrypt(sourceFiles); } catch (IOException e) { throw new BackupException("Failed to encrypt " + Arrays.toString(sourceFiles), e); } for (Path file : sourceFiles) { mChecksum.add(file.getName(), DigestUtils.getHexDigest(mMetadata.info.checksumAlgo, file)); } } private void backupData() throws BackupException { for (int i = 0; i < mMetadata.metadata.dataDirs.length; ++i) { Path[] dataFiles; String backupDataDir = mMetadata.metadata.dataDirs[i]; if (backupDataDir.equals(BackupManager.DATA_BACKUP_SPECIAL_ADB)) { // ADB backup dataFiles = backupAdb(i); } else { // Regular directory backup dataFiles = backupDirectory(backupDataDir, i); } try { dataFiles = mBackupItem.encrypt(dataFiles); } catch (IOException e) { throw new BackupException("Failed to encrypt " + Arrays.toString(dataFiles)); } for (Path file : dataFiles) { mChecksum.add(file.getName(), DigestUtils.getHexDigest(mMetadata.info.checksumAlgo, file)); } } } @NonNull private Path[] backupDirectory(@NonNull String dir, int index) throws BackupException { String filePrefix = BackupUtils.getDataFilePrefix(index, getExt(mMetadata.info.tarType)); try { return TarUtils.create(mMetadata.info.tarType, Paths.get(dir), mBackupItem.getUnencryptedBackupPath(), filePrefix, null, null, BackupUtils.getExcludeDirs(!mBackupFlags.backupCache()), false) .toArray(new Path[0]); } catch (Throwable th) { throw new BackupException("Failed to backup data directory at " + dir, th); } } @NonNull private Path[] backupAdb(int index) throws BackupException { try { String filePrefix = BackupUtils.getDataFilePrefix(index, ".ab"); Path abFile = mBackupItem.getUnencryptedBackupPath().createNewFile(filePrefix, null); try (OutputStream os = abFile.openOutputStream()) { ParcelFileDescriptor fd = ParcelFileDescriptorUtil.pipeTo(os); BackupCompat.adbBackup(mUserId, fd, false, false, false, false, false, false, false, true, new String[]{mPackageName}); } return new Path[]{abFile}; } catch (Throwable th) { throw new BackupException("Failed to backup ADB data.", th); } } private void backupKeyStore() throws BackupException { // Called only when the app has an keystore item if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // keystore v2 is not supported. Log.w(TAG, "Ignoring KeyStore backups for %s", mPackageName); return; } Path keyStorePath = KeyStoreUtils.getKeyStorePath(mUserId); try { Path masterKeyFile = KeyStoreUtils.getMasterKey(mUserId); // Master key exists, so take its checksum to verify it during the restore mChecksum.add(MASTER_KEY, DigestUtils.getHexDigest(mMetadata.info.checksumAlgo, masterKeyFile.getContentAsString().getBytes())); } catch (FileNotFoundException ignore) { } // Store the KeyStore files Path cachePath = Paths.get(FileUtils.getCachePath()); List cachedKeyStoreFileNames = new ArrayList<>(); List keyStoreFilters = new ArrayList<>(); for (String keyStoreFileName : KeyStoreUtils.getKeyStoreFiles(mApplicationInfo.uid, mUserId)) { try { String newFileName = Utils.replaceOnce(keyStoreFileName, String.valueOf(mApplicationInfo.uid), String.valueOf(KEYSTORE_PLACEHOLDER)); IoUtils.copy(keyStorePath.findFile(keyStoreFileName), cachePath.findOrCreateFile(newFileName, null)); cachedKeyStoreFileNames.add(newFileName); keyStoreFilters.add(Pattern.quote(newFileName)); } catch (Throwable e) { throw new BackupException("Could not cache " + keyStoreFileName, e); } } if (cachedKeyStoreFileNames.isEmpty()) { throw new BackupException("There were some KeyStore items but they couldn't be cached before taking a backup."); } String keyStorePrefix = KEYSTORE_PREFIX + getExt(mMetadata.info.tarType); Path[] backedUpKeyStoreFiles; try { backedUpKeyStoreFiles = TarUtils.create(mMetadata.info.tarType, cachePath, mBackupItem.getUnencryptedBackupPath(), keyStorePrefix, keyStoreFilters.toArray(new String[0]), null, null, false) .toArray(new Path[0]); } catch (Throwable th) { throw new BackupException("Could not backup KeyStore item.", th); } // Remove cache for (String name : cachedKeyStoreFileNames) { try { cachePath.findFile(name).delete(); } catch (FileNotFoundException ignore) { } } try { backedUpKeyStoreFiles = mBackupItem.encrypt(backedUpKeyStoreFiles); } catch (IOException e) { throw new BackupException("Failed to encrypt " + Arrays.toString(backedUpKeyStoreFiles), e); } for (Path file : backedUpKeyStoreFiles) { mChecksum.add(file.getName(), DigestUtils.getHexDigest(mMetadata.info.checksumAlgo, file)); } } private void backupExtras() throws BackupException { PseudoRules rules = new PseudoRules(mPackageName, mUserId); Path miscFile; try { miscFile = mBackupItem.getMiscFile(); } catch (IOException e) { throw new BackupException("Couldn't get misc.am.tsv", e); } // Backup permissions @NonNull String[] permissions = ArrayUtils.defeatNullable(mPackageInfo.requestedPermissions); int[] permissionFlags = ArrayUtils.defeatNullable(mPackageInfo.requestedPermissionsFlags); List opEntries = new ArrayList<>(); try { List packageOpsList = new AppOpsManagerCompat() .getOpsForPackage(mApplicationInfo.uid, mPackageName, null); if (packageOpsList.size() == 1) opEntries.addAll(packageOpsList.get(0).getOps()); } catch (Exception ignore) { } PermissionInfo info; int basePermissionType; int protectionLevels; for (int i = 0; i < permissions.length; ++i) { try { info = mPm.getPermissionInfo(permissions[i], 0); basePermissionType = PermissionInfoCompat.getProtection(info); protectionLevels = PermissionInfoCompat.getProtectionFlags(info); if (basePermissionType != PermissionInfo.PROTECTION_DANGEROUS && (protectionLevels & PermissionInfo.PROTECTION_FLAG_DEVELOPMENT) == 0) { // Don't include permissions that are neither dangerous nor development continue; } boolean isGranted = (permissionFlags[i] & PackageInfo.REQUESTED_PERMISSION_GRANTED) != 0; int permFlags; if (SelfPermissions.checkGetGrantRevokeRuntimePermissions()) { permFlags = PermissionCompat.getPermissionFlags(info.name, mPackageName, mUserId); } else permFlags = PermissionCompat.FLAG_PERMISSION_NONE; rules.setPermission(permissions[i], isGranted, permFlags); } catch (PackageManager.NameNotFoundException ignore) { } } // Backup app ops for (AppOpsManagerCompat.OpEntry entry : opEntries) { rules.setAppOp(entry.getOp(), entry.getMode()); } // Backup MagiskHide data Collection magiskHiddenProcesses = MagiskHide.getProcesses(mPackageInfo); for (MagiskProcess magiskProcess : magiskHiddenProcesses) { if (magiskProcess.isEnabled()) { rules.setMagiskHide(magiskProcess); } } // Backup Magisk DenyList data Collection magiskDeniedProcesses = MagiskDenyList.getProcesses(mPackageInfo); for (MagiskProcess magiskProcess : magiskDeniedProcesses) { if (magiskProcess.isEnabled()) { rules.setMagiskDenyList(magiskProcess); } } // Backup allowed notification listeners aka BIND_NOTIFICATION_LISTENER_SERVICE if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 && SelfPermissions.checkNotificationListenerAccess()) { try { INotificationManager notificationManager = INotificationManager.Stub.asInterface(ProxyBinder.getService(Context.NOTIFICATION_SERVICE)); List notificationComponents = notificationManager.getEnabledNotificationListeners(mUserId); List componentsForThisPkg = new ArrayList<>(); for (ComponentName componentName : notificationComponents) { if (mPackageName.equals(componentName.getPackageName())) { componentsForThisPkg.add(componentName.getClassName()); } } for (String component : componentsForThisPkg) { rules.setNotificationListener(component, true); } } catch (RemoteException e) { e.printStackTrace(); } } // Backup battery optimization boolean batteryOptimized = DeviceIdleManagerCompat.isBatteryOptimizedApp(mPackageName); if (!batteryOptimized) { rules.setBatteryOptimization(false); } // Backup net policy if (SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.MANAGE_NETWORK_POLICY)) { int policies = ExUtils.requireNonNullElse(() -> NetworkPolicyManagerCompat.getUidPolicy(mApplicationInfo.uid), 0); if (policies > 0) { // Store only if there is a policy rules.setNetPolicy(policies); } } // Backup URI grants List uriGrants = new UriManager().getGrantedUris(mPackageName); if (uriGrants != null) { for (UriManager.UriGrant uriGrant : uriGrants) { if (uriGrant.targetUserId == mUserId) { rules.setUriGrant(uriGrant); } } } // Backup SSAID if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { try { String ssaid = new SsaidSettings(mUserId).getSsaid(mPackageName, mApplicationInfo.uid); if (ssaid != null) rules.setSsaid(ssaid); } catch (IOException e) { // Ignore exception Log.e(TAG, e); } } // Backup freezeType Integer freezeType = FreezeUtils.loadFreezeMethod(mPackageName); if (freezeType != null) { rules.setFreezeType(freezeType); } // Commit rules.commitExternal(miscFile); if (!miscFile.exists()) return; try { miscFile = mBackupItem.encrypt(new Path[]{miscFile})[0]; // Store checksum mChecksum.add(miscFile.getName(), DigestUtils.getHexDigest(mMetadata.info.checksumAlgo, miscFile)); } catch (IOException | IndexOutOfBoundsException e) { throw new BackupException("Couldn't get misc.am.tsv for generating checksum", e); } } private void backupRules() throws BackupException { try { Path rulesFile = mBackupItem.getRulesFile(); try (OutputStream outputStream = rulesFile.openOutputStream(); ComponentsBlocker cb = ComponentsBlocker.getInstance(mPackageName, mUserId)) { ComponentUtils.storeRules(outputStream, cb.getAll(), true); } if (!rulesFile.exists()) return; rulesFile = mBackupItem.encrypt(new Path[]{rulesFile})[0]; // Store checksum mChecksum.add(rulesFile.getName(), DigestUtils.getHexDigest(mMetadata.info.checksumAlgo, rulesFile)); } catch (IOException | IndexOutOfBoundsException e) { throw new BackupException("Rules backup is requested but encountered an error during fetching rules.", e); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/backup/BackupUtils.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.backup; import android.annotation.SuppressLint; import android.annotation.UserIdInt; import android.content.Context; import android.content.pm.ApplicationInfo; import android.os.Build; import android.os.UserHandleHidden; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.annotation.WorkerThread; import org.jetbrains.annotations.Contract; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.regex.Pattern; import io.github.muntashirakon.AppManager.backup.struct.BackupMetadataV5; import io.github.muntashirakon.AppManager.db.entity.Backup; import io.github.muntashirakon.AppManager.db.utils.AppDb; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.misc.OsEnvironment; import io.github.muntashirakon.AppManager.users.Users; import io.github.muntashirakon.AppManager.utils.ArrayUtils; import io.github.muntashirakon.AppManager.utils.BroadcastUtils; import io.github.muntashirakon.AppManager.utils.TarUtils; import io.github.muntashirakon.AppManager.utils.Utils; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; public final class BackupUtils { public static final String TAG = BackupUtils.class.getSimpleName(); public static final String[] TAR_TYPES = new String[]{TarUtils.TAR_GZIP, TarUtils.TAR_BZIP2, TarUtils.TAR_ZSTD}; public static final String[] TAR_TYPES_READABLE = new String[]{"GZip", "BZip2", "Zstandard"}; private static final Pattern UUID_PATTERN = Pattern.compile("[a-f\\d]{8}(-[a-f\\d]{4}){3}-[a-f\\d]{12}"); public static boolean isUuid(@NonNull String name) { return UUID_PATTERN.matcher(name).matches(); } @Nullable @Contract("!null -> !null") public static String getCompatBackupName(@Nullable String backupName) { if (MetadataManager.getCurrentBackupMetaVersion() >= 5) { return backupName; } return getV4SanitizedBackupName(backupName); } @NonNull public static String getV4BackupName(@UserIdInt int userId, @Nullable String backupName) { if (backupName == null) { return String.valueOf(userId); } // For v4 and earlier, backup name is used as a filename. So, necessary sanitization may be // required. return userId + "_" + getV4SanitizedBackupName(backupName); } @Nullable @Contract("!null -> !null") public static String getV4SanitizedBackupName(@Nullable String backupName) { if (backupName == null) { return null; } // [\\/:?"<>|\s] return backupName.trim().replaceAll("[\\\\/:?\"<>|\\s]+", "_"); } @NonNull public static String getV5RelativeDir(@NonNull String backupUuid) { // backups/{backupUuid} return BackupItems.BACKUP_DIRECTORY + File.separator + backupUuid; } @NonNull public static String getV4RelativeDir(@NonNull String backupNameWithUser, @NonNull String packageName) { // Relative directory needs to be inferred: {packageName}/{backupNameWithUser} // where backupNameWithUser = {userid}[_{backup_name}] return packageName + File.separator + backupNameWithUser; } @NonNull public static String getV4RelativeDir(@UserIdInt int userId, @Nullable String backupName, @NonNull String packageName) { // Relative directory needs to be inferred: {packageName}/{backupName} // where backupName = {userid}[_{backup_name}] return packageName + File.separator + getV4BackupName(userId, backupName); } @Nullable public static String getRealBackupName(int backupVersion, @Nullable String backupNameWithUserId) { if (backupVersion >= 5) { return backupNameWithUserId; } else { // v4 or earlier backup: {userid}[_{backup_name}] if (backupNameWithUserId == null || TextUtils.isDigitsOnly(backupNameWithUserId)) { // It's only a user ID return null; } else { int firstUnderscore = backupNameWithUserId.indexOf('_'); if (firstUnderscore != -1) { // Found an underscore String userHandle = backupNameWithUserId.substring(0, firstUnderscore); if (TextUtils.isDigitsOnly(userHandle)) { return backupNameWithUserId.substring(firstUnderscore + 1); } } throw new IllegalArgumentException("Invalid backup name " + backupNameWithUserId); } } } public static String getReadableTarType(@TarUtils.TarType String tarType) { int i = ArrayUtils.indexOf(TAR_TYPES, tarType); if (i == -1) { return "GZip"; } return TAR_TYPES_READABLE[i]; } @WorkerThread @NonNull public static HashMap storeAllAndGetLatestBackupMetadata() { AppDb appDb = new AppDb(); HashMap backupMetadata = new HashMap<>(); HashMap> allBackupMetadata = getAllMetadata(); List backups = new ArrayList<>(); for (List metadataList : allBackupMetadata.values()) { if (metadataList.isEmpty()) continue; Backup latestBackup = null; Backup backup; for (BackupMetadataV5 metadataV5 : metadataList) { backup = Backup.fromBackupMetadataV5(metadataV5); backups.add(backup); if (latestBackup == null || backup.backupTime > latestBackup.backupTime) { latestBackup = backup; } } backupMetadata.put(latestBackup.packageName, latestBackup); } appDb.deleteAllBackups(); appDb.insertBackups(backups); return backupMetadata; } @WorkerThread @NonNull public static HashMap getAllLatestBackupMetadataFromDb() { HashMap backupMetadata = new HashMap<>(); for (Backup backup : new AppDb().getAllBackups()) { Backup latestBackup = backupMetadata.get(backup.packageName); if (latestBackup == null || backup.backupTime > latestBackup.backupTime) { backupMetadata.put(backup.packageName, backup); } } return backupMetadata; } public static void putBackupToDbAndBroadcast(@NonNull Context context, @NonNull BackupMetadataV5 metadata) { if (Utils.isRoboUnitTest()) { return; } AppDb appDb = new AppDb(); appDb.insert(Backup.fromBackupMetadataV5(metadata)); appDb.updateApplication(context, metadata.metadata.packageName); BroadcastUtils.sendDbPackageAltered(context, new String[]{metadata.metadata.packageName}); } public static void deleteBackupToDbAndBroadcast(@NonNull Context context, @NonNull BackupMetadataV5 metadata) { AppDb appDb = new AppDb(); appDb.deleteBackup(Backup.fromBackupMetadataV5(metadata)); appDb.updateApplication(context, metadata.metadata.packageName); BroadcastUtils.sendDbPackageAltered(context, new String[]{metadata.metadata.packageName}); } @WorkerThread @NonNull public static List getBackupMetadataFromDbNoLockValidate(@NonNull String packageName) { List backups = new AppDb().getAllBackupsNoLock(packageName); List validatedBackups = new ArrayList<>(backups.size()); for (Backup backup : backups) { try { if (backup.getItem().exists()) { validatedBackups.add(backup); } } catch (IOException e) { e.printStackTrace(); } } return validatedBackups; } @NonNull public static List retrieveBackupFromDb(@UserIdInt int userId, @Nullable String backupName, @NonNull String packageName) { List backups = getBackupMetadataFromDbNoLockValidate(packageName); if (backupName == null) { backupName = ""; } backupName = getV4SanitizedBackupName(backupName); List backupList = new ArrayList<>(); for (Backup backup : backups) { if (backup.userId != userId) { continue; } if (!Objects.equals(backupName, getV4SanitizedBackupName(backup.backupName))) { continue; } backupList.add(backup); } return backupList; } @Nullable public static Backup retrieveLatestBackupFromDb(@UserIdInt int userId, @Nullable String backupName, @NonNull String packageName) { List backups = getBackupMetadataFromDbNoLockValidate(packageName); if (backupName == null) { backupName = ""; } backupName = getV4SanitizedBackupName(backupName); for (Backup backup : backups) { if (backup.userId == userId && Objects.equals(backupName, getV4SanitizedBackupName(backup.backupName))) { return backup; } } return null; } @Nullable public static Backup retrieveBaseBackupFromDb(@UserIdInt int userId, @NonNull String packageName) { List backups = getBackupMetadataFromDbNoLockValidate(packageName); for (Backup backup : backups) { if (backup.userId == userId && TextUtils.isEmpty(backup.backupName)) { return backup; } } return null; } @WorkerThread @Nullable public static Backup getLatestBackupMetadataFromDbNoLockValidate(@NonNull String packageName) { List backups = getBackupMetadataFromDbNoLockValidate(packageName); Backup latestBackup = null; for (Backup backup : backups) { if (latestBackup == null || backup.backupTime > latestBackup.backupTime) { latestBackup = backup; } } return latestBackup; } /** * Retrieves all metadata for all packages */ @WorkerThread @NonNull private static HashMap> getAllMetadata() { HashMap> backupMetadata = new HashMap<>(); List backupPaths = BackupItems.findAllBackupItems(); for (BackupItems.BackupItem backupItem : backupPaths) { try { BackupMetadataV5 metadataV5 = backupItem.getMetadata(); BackupMetadataV5.Metadata metadata = metadataV5.metadata; if (!backupMetadata.containsKey(metadata.packageName)) { backupMetadata.put(metadata.packageName, new ArrayList<>()); } //noinspection ConstantConditions backupMetadata.get(metadata.packageName).add(metadataV5); } catch (IOException e) { Log.w(TAG, "Invalid backup: %s", e, backupItem.getRelativeDir()); } } return backupMetadata; } @NonNull public static String getSourceFilePrefix(@Nullable String fullExtension) { if (fullExtension == null) { return BackupManager.SOURCE_PREFIX; } return BackupManager.SOURCE_PREFIX + fullExtension; } @NonNull public static String getDataFilePrefix(int index, @Nullable String fullExtension) { if (fullExtension == null) { return BackupManager.DATA_PREFIX + index; } return BackupManager.DATA_PREFIX + index + fullExtension; } @NonNull static String[] getExcludeDirs(boolean includeCache, @Nullable String... others) { // Lib dirs has to be ignored by default List excludeDirs = new ArrayList<>(Arrays.asList(BackupManager.LIB_DIR)); if (includeCache) { excludeDirs.addAll(Arrays.asList(BackupManager.CACHE_DIRS)); } if (others != null) { excludeDirs.addAll(Arrays.asList(others)); } return excludeDirs.toArray(new String[0]); } @SuppressLint("SdCardPath") @NonNull static List getDataDirectories(@NonNull ApplicationInfo applicationInfo, boolean loadInternal, boolean loadExternal, boolean loadMediaObb) { // Data directories *must* be readable and non-empty ArrayList dataDirs = new ArrayList<>(); if (applicationInfo.dataDir == null) { throw new IllegalArgumentException("Data directory cannot be empty."); } int userId = UserHandleHidden.getUserId(applicationInfo.uid); if (loadInternal) { String dataDir = applicationInfo.dataDir; if (dataDir.startsWith("/data/data/")) { dataDir = Utils.replaceOnce(dataDir, "/data/data/", String.format(Locale.ROOT, "/data/user/%d/", userId)); } dataDirs.add(dataDir); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && applicationInfo.deviceProtectedDataDir != null) { // /data/user_de/{userId} dataDirs.add(applicationInfo.deviceProtectedDataDir); } } // External directories could be /sdcard, /storage/sdcard, /storage/emulated/{userId} OsEnvironment.UserEnvironment ue = OsEnvironment.getUserEnvironment(userId); if (loadExternal) { Path[] externalFiles = ue.buildExternalStorageAppDataDirs(applicationInfo.packageName); for (Path externalFile : externalFiles) { // Replace /storage/emulated/{!myUserId} with /data/media/{!myUserId} externalFile = Paths.getAccessiblePath(externalFile); if (externalFile.listFiles().length > 0) { dataDirs.add(externalFile.getFilePath()); } } } if (loadMediaObb) { List externalFiles = new ArrayList<>(); externalFiles.addAll(Arrays.asList(ue.buildExternalStorageAppMediaDirs(applicationInfo.packageName))); externalFiles.addAll(Arrays.asList(ue.buildExternalStorageAppObbDirs(applicationInfo.packageName))); for (Path externalFile : externalFiles) { // Replace /storage/emulated/{!myUserId} with /data/media/{!myUserId} externalFile = Paths.getAccessiblePath(externalFile); if (externalFile.listFiles().length > 0) { dataDirs.add(externalFile.getFilePath()); } } } return dataDirs; } /** * Get a writable data directory from the given directory. This is useful for restoring a backup. */ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) @SuppressLint("SdCardPath") static String getWritableDataDirectory(@NonNull String dataDir, @UserIdInt int oldUserId, @UserIdInt int newUserId) { if (dataDir.startsWith("/data/data/")) { // /data/data/ -> /data/user/{newUserId}/ return Utils.replaceOnce(dataDir, "/data/data/", String.format(Locale.ROOT, "/data/user/%d/", newUserId)); } String dataUserDir = String.format(Locale.ROOT, "/data/user/%d/", oldUserId); if (dataDir.startsWith(dataUserDir)) { // /data/user/{oldUserId} -> /data/user/{newUserId}/ return Utils.replaceOnce(dataDir, dataUserDir, String.format(Locale.ROOT, "/data/user/%d/", newUserId)); } String dataUserDeDir = String.format(Locale.ROOT, "/data/user_de/%d/", oldUserId); if (dataDir.startsWith(dataUserDeDir)) { // /data/user_de/{oldUserId} -> /data/user_de/{newUserId}/ return Utils.replaceOnce(dataDir, dataUserDeDir, String.format(Locale.ROOT, "/data/user_de/%d/", newUserId)); } if (dataDir.startsWith("/sdcard/")) { // /sdcard/ -> /storage/emulated/{newUserId}/ or /data/media/{newUserId}/ in a multiuser system return getExternalStorage(dataDir, "/sdcard/", newUserId); } if (dataDir.startsWith("/storage/sdcard/")) { // /storage/sdcard/ -> /storage/emulated/{newUserId}/ or /data/media/{newUserId}/ in a multiuser system, otherwise /sdcard/ return getExternalStorage(dataDir, "/storage/sdcard/", newUserId); } if (dataDir.startsWith("/storage/sdcard0/")) { // /storage/sdcard0/ -> /storage/emulated/{newUserId}/ or /data/media/{newUserId}/ in a multiuser system, otherwise /sdcard/ return getExternalStorage(dataDir, "/storage/sdcard0/", newUserId); } String oldStorageEmulatedDir = String.format(Locale.ROOT, "/storage/emulated/%d/", oldUserId); if (dataDir.startsWith(oldStorageEmulatedDir)) { // /storage/emulated/{oldUserId}/ -> /storage/emulated/{newUserId}/ or /data/media/{newUserId}/ in a multiuser system, otherwise /sdcard/ return getExternalStorage(dataDir, oldStorageEmulatedDir, newUserId); } String oldDataMediaDir = String.format(Locale.ROOT, "/data/media/%d/", oldUserId); if (dataDir.startsWith(oldDataMediaDir)) { // /data/media/{oldUserId}/ -> /storage/emulated/{newUserId}/ or /data/media/{newUserId}/ in a multiuser system, otherwise /sdcard/ return getExternalStorage(dataDir, oldDataMediaDir, newUserId); } Log.i(TAG, "getWritableDataDirectory: Unrecognized path %s, using as is.", dataDir); return dataDir; } @SuppressLint("SdCardPath") @NonNull private static String getExternalStorage(@NonNull String dataDir, @NonNull String match, @UserIdInt int userId) { if (Users.getAllUsers().size() > 1) { // Multiuser system, use either /storage/emulated/{userId} or /data/media/{userId} String storageEmulatedDir = String.format(Locale.ROOT, "/storage/emulated/%d/", userId); if (userId == UserHandleHidden.myUserId() && Paths.get(storageEmulatedDir).canRead()) { return Utils.replaceOnce(dataDir, match, storageEmulatedDir); } return Utils.replaceOnce(dataDir, match, String.format(Locale.ROOT, "/data/media/%d/", userId)); } // Otherwise, use /sdcard return Utils.replaceOnce(dataDir, match, "/sdcard/"); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/backup/CryptoUtils.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.backup; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.StringDef; import androidx.annotation.WorkerThread; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import io.github.muntashirakon.AppManager.backup.struct.BackupMetadataV2; import io.github.muntashirakon.AppManager.crypto.AESCrypto; import io.github.muntashirakon.AppManager.crypto.Crypto; import io.github.muntashirakon.AppManager.crypto.CryptoException; import io.github.muntashirakon.AppManager.crypto.ECCCrypto; import io.github.muntashirakon.AppManager.crypto.OpenPGPCrypto; import io.github.muntashirakon.AppManager.crypto.RSACrypto; import io.github.muntashirakon.AppManager.crypto.ks.KeyStoreManager; import io.github.muntashirakon.AppManager.settings.Prefs; public class CryptoUtils { @StringDef(value = { MODE_NO_ENCRYPTION, MODE_AES, MODE_RSA, MODE_ECC, MODE_OPEN_PGP, }) @Retention(RetentionPolicy.SOURCE) public @interface Mode { } public static final String MODE_NO_ENCRYPTION = "none"; public static final String MODE_AES = "aes"; public static final String MODE_RSA = "rsa"; public static final String MODE_ECC = "ecc"; public static final String MODE_OPEN_PGP = "pgp"; @Mode public static String getMode() { String currentMode = Prefs.Encryption.getEncryptionMode(); if (isAvailable(currentMode)) return currentMode; // Fallback to no encryption if none of the modes are available. return MODE_NO_ENCRYPTION; } public static String getExtension(@NonNull @Mode String mode) { switch (mode) { case MODE_OPEN_PGP: return OpenPGPCrypto.GPG_EXT; case MODE_AES: return AESCrypto.AES_EXT; case MODE_RSA: return RSACrypto.RSA_EXT; case MODE_ECC: return ECCCrypto.ECC_EXT; case MODE_NO_ENCRYPTION: default: return ""; } } /** * Get file name with appropriate extension */ @NonNull public static String getAppropriateFilename(String filename, @NonNull @Mode String mode) { return filename + getExtension(mode); } @WorkerThread public static Crypto setupCrypto(@NonNull BackupMetadataV2 metadata) throws CryptoException { BackupCryptSetupHelper cryptoHelper = new BackupCryptSetupHelper(metadata.crypto, metadata.version); metadata.keyIds = cryptoHelper.getKeyIds(); metadata.aes = cryptoHelper.getAes(); metadata.iv = cryptoHelper.getIv(); return cryptoHelper.crypto; } @WorkerThread public static boolean isAvailable(@NonNull @Mode String mode) { switch (mode) { case MODE_OPEN_PGP: String keyIds = Prefs.Encryption.getOpenPgpKeyIds(); // FIXME(1/10/20): Check for the availability of the provider return !TextUtils.isEmpty(keyIds); case MODE_AES: try { return KeyStoreManager.getInstance().containsKey(AESCrypto.AES_KEY_ALIAS); } catch (Exception e) { return false; } case MODE_RSA: try { return KeyStoreManager.getInstance().containsKey(RSACrypto.RSA_KEY_ALIAS); } catch (Exception e) { return false; } case MODE_ECC: try { return KeyStoreManager.getInstance().containsKey(ECCCrypto.ECC_KEY_ALIAS); } catch (Exception e) { return false; } case MODE_NO_ENCRYPTION: return true; default: return false; } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/backup/MetadataManager.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.backup; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.annotation.WorkerThread; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.io.OutputStream; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import io.github.muntashirakon.AppManager.backup.struct.BackupMetadataV5; import io.github.muntashirakon.AppManager.crypto.CryptoException; import io.github.muntashirakon.AppManager.utils.DigestUtils; import io.github.muntashirakon.AppManager.utils.JSONUtils; import io.github.muntashirakon.io.Path; public final class MetadataManager { public static final String TAG = MetadataManager.class.getSimpleName(); private static int currentBackupMetaVersion = 5; public static final String META_V2_FILE = "meta_v2.am.json"; // New scheme public static final String INFO_V5_FILE = "info_v5.am.json"; // unencrypted public static final String META_V5_FILE = "meta_v5.am.json"; // encrypted private MetadataManager() { } @VisibleForTesting public static void setCurrentBackupMetaVersion(int version) { currentBackupMetaVersion = version; } public static int getCurrentBackupMetaVersion() { return currentBackupMetaVersion; } @NonNull @WorkerThread public static BackupMetadataV5.Info readInfo(@NonNull BackupItems.BackupItem backupItem) throws IOException { boolean v5AndUp = backupItem.isV5AndUp(); Path infoFile = v5AndUp ? backupItem.getInfoFile() : backupItem.getMetadataV2File(); String infoString = infoFile.getContentAsString(); JSONObject jsonObject; if (TextUtils.isEmpty(infoString)) { throw new IOException("Empty JSON string for path " + infoFile); } try { jsonObject = new JSONObject(infoString); BackupMetadataV5.Info info = new BackupMetadataV5.Info(jsonObject); info.setBackupItem(backupItem); return info; } catch (JSONException e) { throw new IOException(e.getMessage() + " for path " + infoFile, e); } } @NonNull @WorkerThread public static BackupMetadataV5 readMetadata(@NonNull BackupItems.BackupItem backupItem) throws IOException { BackupMetadataV5.Info info = readInfo(backupItem); return readMetadata(backupItem, info); } @NonNull @WorkerThread public static BackupMetadataV5 readMetadata(@NonNull BackupItems.BackupItem backupItem, @NonNull BackupMetadataV5.Info backupInfo) throws IOException { boolean v5AndUp = backupItem.isV5AndUp(); if (v5AndUp) { // Need to setup crypto in order to decrypt meta_v5.am.json setCrypto(backupItem, backupInfo); } Path metadataFile = v5AndUp ? backupItem.getMetadataV5File(true) : backupItem.getMetadataV2File(); String metadataString = metadataFile.getContentAsString(); JSONObject jsonObject; if (TextUtils.isEmpty(metadataString)) { throw new IOException("Empty JSON string for path " + metadataFile); } try { jsonObject = new JSONObject(metadataString); if (!v5AndUp) { // Meta is a subset of meta_v2.am.json except for backup_name jsonObject.put("backup_name", backupItem.getBackupName()); } BackupMetadataV5.Metadata metadata = new BackupMetadataV5.Metadata(jsonObject); return new BackupMetadataV5(backupInfo, metadata); } catch (JSONException e) { throw new IOException(e.getMessage() + " for path " + metadataFile, e); } } @WorkerThread @NonNull public static Map writeMetadata(@NonNull BackupMetadataV5 metadata, @NonNull BackupItems.BackupItem backupFile) throws IOException { if (!backupFile.isBackupMode()) { throw new IOException("Backup is in read-only mode."); } if (metadata.info.version >= 5) { // v5 and up return writeMetadataV5(metadata, backupFile); } else { // Old style backup return writeMetadataV2(metadata, backupFile); } } @WorkerThread @NonNull private static Map writeMetadataV2(@NonNull BackupMetadataV5 metadata, @NonNull BackupItems.BackupItem backupFile) throws IOException { Path metadataFile = backupFile.getMetadataV2File(); try (OutputStream outputStream = metadataFile.openOutputStream()) { JSONObject metadataObject = metadata.info.serializeToJson(); JSONUtils.putAll(metadataObject, metadata.metadata.serializeToJson()); // Info is a subset of meta_v2.am.json except for backup_name metadataObject.remove("backup_name"); outputStream.write(metadataObject.toString(4).getBytes()); return Collections.singletonMap(metadataFile.getName(), DigestUtils.getHexDigest( metadata.info.checksumAlgo, metadataFile)); } catch (JSONException e) { throw new IOException(e.getMessage() + " for path " + backupFile.getBackupPath(), e); } } @WorkerThread @NonNull private static Map writeMetadataV5(@NonNull BackupMetadataV5 metadata, @NonNull BackupItems.BackupItem backupFile) throws IOException { Map filenameChecksumMap = new LinkedHashMap<>(2); Path metadataFile = backupFile.getMetadataV5File(true); try (OutputStream outputStream = metadataFile.openOutputStream()) { outputStream.write(metadata.metadata.serializeToJson().toString(4).getBytes()); } catch (JSONException e) { throw new IOException(e.getMessage() + " for file " + metadataFile, e); } // Encrypt the metadata Path encryptedMetadataFile = backupFile.encrypt(new Path[]{metadataFile})[0]; filenameChecksumMap.put(encryptedMetadataFile.getName(), DigestUtils.getHexDigest( metadata.info.checksumAlgo, encryptedMetadataFile)); Path infoFile = backupFile.getInfoFile(); try (OutputStream outputStream = infoFile.openOutputStream()) { outputStream.write(metadata.info.serializeToJson().toString(4).getBytes()); filenameChecksumMap.put(infoFile.getName(), DigestUtils.getHexDigest( metadata.info.checksumAlgo, infoFile)); } catch (JSONException e) { throw new IOException(e.getMessage() + " for file " + infoFile, e); } return filenameChecksumMap; } private static void setCrypto(@NonNull BackupItems.BackupItem backupItem, @NonNull BackupMetadataV5.Info backupInfo) throws IOException { if (!CryptoUtils.isAvailable(backupInfo.crypto)) { throw new IOException("Mode " + backupInfo.crypto + " is currently unavailable."); } try { backupItem.setCrypto(backupInfo.getCrypto()); } catch (CryptoException e) { throw new IOException("Failed to get crypto " + backupInfo.crypto, e); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/backup/RestoreOp.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.backup; import static io.github.muntashirakon.AppManager.backup.BackupManager.KEYSTORE_PLACEHOLDER; import static io.github.muntashirakon.AppManager.backup.BackupManager.MASTER_KEY; import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.GET_SIGNING_CERTIFICATES; import android.app.AppOpsManager; import android.app.INotificationManager; import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Build; import android.os.ParcelFileDescriptor; import android.system.ErrnoException; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import java.io.Closeable; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; import io.github.muntashirakon.AppManager.apk.ApkFile; import io.github.muntashirakon.AppManager.apk.installer.InstallerOptions; import io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat; import io.github.muntashirakon.AppManager.backup.struct.BackupMetadataV5; import io.github.muntashirakon.AppManager.compat.AppOpsManagerCompat; import io.github.muntashirakon.AppManager.compat.BackupCompat; import io.github.muntashirakon.AppManager.compat.DeviceIdleManagerCompat; import io.github.muntashirakon.AppManager.compat.ManifestCompat; import io.github.muntashirakon.AppManager.compat.NetworkPolicyManagerCompat; import io.github.muntashirakon.AppManager.compat.PackageManagerCompat; import io.github.muntashirakon.AppManager.crypto.CryptoException; import io.github.muntashirakon.AppManager.ipc.ProxyBinder; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.magisk.MagiskDenyList; import io.github.muntashirakon.AppManager.magisk.MagiskHide; import io.github.muntashirakon.AppManager.permission.PermUtils; import io.github.muntashirakon.AppManager.permission.Permission; import io.github.muntashirakon.AppManager.progress.ProgressHandler; import io.github.muntashirakon.AppManager.rules.PseudoRules; import io.github.muntashirakon.AppManager.rules.RuleType; import io.github.muntashirakon.AppManager.rules.RulesImporter; import io.github.muntashirakon.AppManager.rules.struct.AppOpRule; import io.github.muntashirakon.AppManager.rules.struct.FreezeRule; import io.github.muntashirakon.AppManager.rules.struct.MagiskDenyListRule; import io.github.muntashirakon.AppManager.rules.struct.MagiskHideRule; import io.github.muntashirakon.AppManager.rules.struct.NetPolicyRule; import io.github.muntashirakon.AppManager.rules.struct.PermissionRule; import io.github.muntashirakon.AppManager.rules.struct.RuleEntry; import io.github.muntashirakon.AppManager.rules.struct.SsaidRule; import io.github.muntashirakon.AppManager.rules.struct.UriGrantRule; import io.github.muntashirakon.AppManager.runner.Runner; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.ssaid.SsaidSettings; import io.github.muntashirakon.AppManager.uri.UriManager; import io.github.muntashirakon.AppManager.utils.DigestUtils; import io.github.muntashirakon.AppManager.utils.FreezeUtils; import io.github.muntashirakon.AppManager.utils.KeyStoreUtils; import io.github.muntashirakon.AppManager.utils.PackageUtils; import io.github.muntashirakon.AppManager.utils.ParcelFileDescriptorUtil; import io.github.muntashirakon.AppManager.utils.TarUtils; import io.github.muntashirakon.AppManager.utils.Utils; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; import io.github.muntashirakon.io.UidGidPair; @WorkerThread class RestoreOp implements Closeable { static final String TAG = RestoreOp.class.getSimpleName(); private static final Object sLock = new Object(); @NonNull private final String mPackageName; @NonNull private final BackupFlags mBackupFlags; @NonNull private final BackupFlags mRequestedFlags; @NonNull private final BackupMetadataV5.Info mBackupInfo; @NonNull private final BackupMetadataV5.Metadata mBackupMetadata; @NonNull private final BackupItems.BackupItem mBackupItem; @Nullable private PackageInfo mPackageInfo; private int mUid; @NonNull private final BackupItems.Checksum mChecksum; private final int mUserId; private boolean mIsInstalled; private boolean mRequiresRestart; RestoreOp(@NonNull String packageName, @NonNull BackupFlags requestedFlags, @NonNull BackupItems.BackupItem backupItem, int userId) throws BackupException { mPackageName = packageName; mRequestedFlags = requestedFlags; mBackupItem = backupItem; mUserId = userId; try { mBackupInfo = mBackupItem.getInfo(); mBackupFlags = mBackupInfo.flags; } catch (IOException e) { mBackupItem.cleanup(); throw new BackupException("Could not read backup info. Possibly due to a malformed json file.", e); } // Setup crypto if (!CryptoUtils.isAvailable(mBackupInfo.crypto)) { mBackupItem.cleanup(); throw new BackupException("Mode " + mBackupInfo.crypto + " is currently unavailable."); } try { mBackupItem.setCrypto(mBackupInfo.getCrypto()); } catch (CryptoException e) { mBackupItem.cleanup(); throw new BackupException("Could not get crypto " + mBackupInfo.crypto, e); } try { mBackupMetadata = mBackupItem.getMetadata(mBackupInfo).metadata; } catch (IOException e) { mBackupItem.cleanup(); throw new BackupException("Could not read backup metadata. Possibly due to a malformed json file.", e); } // Get checksums try { mChecksum = mBackupItem.getChecksum(); } catch (Throwable e) { mBackupItem.cleanup(); throw new BackupException("Failed to get checksums.", e); } // Verify metadata if (!requestedFlags.skipSignatureCheck()) { try { verifyMetadata(); } catch (BackupException e) { mBackupItem.cleanup(); throw e; } } // Check user handle if (mBackupInfo.userId != userId) { Log.w(TAG, "Using different user handle."); } // Get package info mPackageInfo = null; try { mPackageInfo = PackageManagerCompat.getPackageInfo(packageName, GET_SIGNING_CERTIFICATES | PackageManagerCompat.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES, userId); mUid = Objects.requireNonNull(mPackageInfo.applicationInfo).uid; } catch (Exception ignore) { } mIsInstalled = mPackageInfo != null; } @Override public void close() { Log.d(TAG, "Close called"); mChecksum.close(); mBackupItem.cleanup(); } void runRestore(@Nullable ProgressHandler progressHandler) throws BackupException { try { if (mRequestedFlags.backupData() && mBackupMetadata.keyStore && !mRequestedFlags.skipSignatureCheck()) { // Check checksum of master key first checkMasterKey(); } incrementProgress(progressHandler); if (mRequestedFlags.backupApkFiles()) { restoreApkFiles(); incrementProgress(progressHandler); } if (mRequestedFlags.backupData()) { restoreData(); if (mBackupMetadata.keyStore) { restoreKeyStore(); } incrementProgress(progressHandler); } if (mRequestedFlags.backupExtras()) { restoreExtras(); incrementProgress(progressHandler); } if (mRequestedFlags.backupRules()) { restoreRules(); incrementProgress(progressHandler); } } catch (BackupException e) { throw e; } catch (Throwable th) { throw new BackupException("Unknown error occurred", th); } } private static void incrementProgress(@Nullable ProgressHandler progressHandler) { if (progressHandler == null) { return; } float current = progressHandler.getLastProgress() + 1; progressHandler.postUpdate(current); } public boolean requiresRestart() { return mRequiresRestart; } private void verifyMetadata() throws BackupException { boolean isV5AndUp = mBackupItem.isV5AndUp(); if (isV5AndUp) { Path infoFile; try { infoFile = mBackupItem.getInfoFile(); } catch (IOException e) { throw new BackupException("Could not get metadata file.", e); } String checksum = DigestUtils.getHexDigest(mBackupInfo.checksumAlgo, infoFile); if (!checksum.equals(mChecksum.get(infoFile.getName()))) { throw new BackupException("Couldn't verify metadata file." + "\nFile: " + infoFile + "\nFound: " + checksum + "\nRequired: " + mChecksum.get(infoFile.getName())); } } Path metadataFile; try { metadataFile = isV5AndUp ? mBackupItem.getMetadataV5File(false) : mBackupItem.getMetadataV2File(); } catch (IOException e) { throw new BackupException("Could not get metadata file.", e); } String checksum = DigestUtils.getHexDigest(mBackupInfo.checksumAlgo, metadataFile); if (!checksum.equals(mChecksum.get(metadataFile.getName()))) { throw new BackupException("Couldn't verify metadata file." + "\nFile: " + metadataFile + "\nFound: " + checksum + "\nRequired: " + mChecksum.get(metadataFile.getName())); } } private void checkMasterKey() throws BackupException { if (true) { // TODO: 6/2/22 MasterKey may not actually be necessary. return; } String oldChecksum = mChecksum.get(MASTER_KEY); Path masterKey; try { masterKey = KeyStoreUtils.getMasterKey(mUserId); } catch (FileNotFoundException e) { if (oldChecksum == null) return; else throw new BackupException("Master key existed when the checksum was made but now it doesn't."); } if (oldChecksum == null) { throw new BackupException("Master key exists but it didn't exist when the backup was made."); } String newChecksum = DigestUtils.getHexDigest(mBackupInfo.checksumAlgo, masterKey.getContentAsString().getBytes()); if (!newChecksum.equals(oldChecksum)) { throw new BackupException("Checksums for master key did not match."); } } private void restoreApkFiles() throws BackupException { if (!mBackupFlags.backupApkFiles()) { throw new BackupException("APK restore is requested but backup doesn't contain any source files."); } Path[] backupSourceFiles = mBackupItem.getSourceFiles(); if (backupSourceFiles.length == 0) { // No source backup found throw new BackupException("Source restore is requested but there are no source files."); } boolean isVerified = true; if (mPackageInfo != null) { // Check signature of the installed app List certChecksumList = Arrays.asList(PackageUtils.getSigningCertChecksums(mBackupInfo.checksumAlgo, mPackageInfo, false)); String[] certChecksums = BackupItems.Checksum.getCertChecksums(mChecksum); for (String checksum : certChecksums) { if (certChecksumList.contains(checksum)) continue; isVerified = false; if (!mRequestedFlags.skipSignatureCheck()) { throw new BackupException("Signing info verification failed." + "\nInstalled: " + certChecksumList + "\nBackup: " + Arrays.toString(certChecksums)); } } } if (!mRequestedFlags.skipSignatureCheck()) { String checksum; for (Path file : backupSourceFiles) { checksum = DigestUtils.getHexDigest(mBackupInfo.checksumAlgo, file); if (!checksum.equals(mChecksum.get(file.getName()))) { throw new BackupException("Source file verification failed." + "\nFile: " + file + "\nFound: " + checksum + "\nRequired: " + mChecksum.get(file.getName())); } } } if (!isVerified) { // Signature verification failed but still here because signature check is disabled. // The only way to restore is to reinstall the app synchronized (sLock) { PackageInstallerCompat installer = PackageInstallerCompat.getNewInstance(); if (installer.uninstall(mPackageName, mUserId, false)) { throw new BackupException("An uninstallation was necessary but couldn't perform it."); } } } // Setup package staging directory Path packageStagingDirectory = Paths.get(PackageUtils.PACKAGE_STAGING_DIRECTORY); try { synchronized (sLock) { PackageUtils.ensurePackageStagingDirectoryPrivileged(); } } catch (Exception ignore) { } try { if (!packageStagingDirectory.canWrite()) { packageStagingDirectory = mBackupItem.getUnencryptedBackupPath(); } } catch (IOException e) { throw new BackupException("Could not create package staging directory", e); } synchronized (sLock) { // Setup apk files, including split apk final int splitCount = mBackupMetadata.splitConfigs.length; String[] allApkNames = new String[splitCount + 1]; Path[] allApks = new Path[splitCount + 1]; try { Path baseApk = packageStagingDirectory.createNewFile(mBackupMetadata.apkName, null); allApks[0] = baseApk; allApkNames[0] = mBackupMetadata.apkName; for (int i = 1; i < allApkNames.length; ++i) { allApkNames[i] = mBackupMetadata.splitConfigs[i - 1]; allApks[i] = packageStagingDirectory.createNewFile(allApkNames[i], null); } } catch (IOException e) { throw new BackupException("Could not create staging files", e); } // Decrypt sources try { backupSourceFiles = mBackupItem.decrypt(backupSourceFiles); } catch (IOException e) { throw new BackupException("Failed to decrypt " + Arrays.toString(backupSourceFiles), e); } // Extract apk files to the package staging directory try { TarUtils.extract(mBackupInfo.tarType, backupSourceFiles, packageStagingDirectory, allApkNames, null, null); } catch (Throwable th) { throw new BackupException("Failed to extract the apk file(s).", th); } // A normal update will do it now InstallerOptions options = InstallerOptions.getDefault(); options.setInstallerName(mBackupMetadata.installer); options.setUserId(mUserId); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { options.setInstallScenario(PackageManager.INSTALL_SCENARIO_BULK); } AtomicReference status = new AtomicReference<>(); PackageInstallerCompat packageInstaller = PackageInstallerCompat.getNewInstance(); packageInstaller.setOnInstallListener(new PackageInstallerCompat.OnInstallListener() { @Override public void onStartInstall(int sessionId, String packageName) { } // MIUI-begin: MIUI 12.5+ workaround @Override public void onAnotherAttemptInMiui(@Nullable ApkFile apkFile) { // This works because the parent install method still remains active until a final status is // received after all the attempts are finished, which is, then, returned to the parent. packageInstaller.install(allApks, mPackageName, options); } // MIUI-end // HyperOS-begin: HyperOS 2.0+ workaround @Override public void onSecondAttemptInHyperOsWithoutInstaller(@Nullable ApkFile apkFile) { // This works because the parent install method still remains active until a final status is // received after all the attempts are finished, which is, then, returned to the parent. options.setInstallerName("com.android.shell"); packageInstaller.install(allApks, mPackageName, options); } // HyperOS-end @Override public void onFinishedInstall(int sessionId, String packageName, int result, @Nullable String blockingPackage, @Nullable String statusMessage) { status.set(statusMessage); } }); try { if (!packageInstaller.install(allApks, mPackageName, options)) { String statusMessage; if (!isVerified) { // Previously installed app was uninstalled. statusMessage = "Couldn't perform a re-installation"; } else { statusMessage = "Couldn't perform an installation"; } if (status.get() != null) { statusMessage += ": " + status.get(); } else statusMessage += "."; throw new BackupException(statusMessage); } } finally { deleteFiles(allApks); // Clean up apk files } // Get package info, again try { mPackageInfo = PackageManagerCompat.getPackageInfo(mPackageName, GET_SIGNING_CERTIFICATES | PackageManagerCompat.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES, mUserId); mUid = Objects.requireNonNull(mPackageInfo.applicationInfo).uid; mIsInstalled = true; } catch (Exception e) { throw new BackupException("Apparently the install wasn't complete in the previous section.", e); } } } private void restoreKeyStore() throws BackupException { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // keystore v2 is not supported. Log.w(TAG, "Ignoring KeyStore backups for %s", mPackageName); return; } if (mPackageInfo == null) { throw new BackupException("KeyStore restore is requested but the app isn't installed."); } Path[] keyStoreFiles = mBackupItem.getKeyStoreFiles(); if (keyStoreFiles.length == 0) { throw new BackupException("KeyStore files should've existed but they didn't"); } if (!mRequestedFlags.skipSignatureCheck()) { String checksum; for (Path file : keyStoreFiles) { checksum = DigestUtils.getHexDigest(mBackupInfo.checksumAlgo, file); if (!checksum.equals(mChecksum.get(file.getName()))) { throw new BackupException("KeyStore file verification failed." + "\nFile: " + file + "\nFound: " + checksum + "\nRequired: " + mChecksum.get(file.getName())); } } } // Decrypt sources try { keyStoreFiles = mBackupItem.decrypt(keyStoreFiles); } catch (IOException e) { throw new BackupException("Failed to decrypt " + Arrays.toString(keyStoreFiles), e); } // Restore KeyStore files to the /data/misc/keystore folder Path keyStorePath = KeyStoreUtils.getKeyStorePath(mUserId); // Note down UID/GID UidGidPair uidGidPair; int mode; try { uidGidPair = Objects.requireNonNull(keyStorePath.getFile()).getUidGid(); mode = keyStorePath.getFile().getMode(); } catch (ErrnoException e) { throw new BackupException("Failed to access properties of the KeyStore folder.", e); } try { TarUtils.extract(mBackupInfo.tarType, keyStoreFiles, keyStorePath, null, null, null); // Restore folder permission Paths.chown(keyStorePath, uidGidPair.uid, uidGidPair.gid); //noinspection OctalInteger Paths.chmod(keyStorePath, mode & 0777); } catch (Throwable th) { throw new BackupException("Failed to restore the KeyStore files.", th); } // Rename files List keyStoreFileNames = KeyStoreUtils.getKeyStoreFiles(KEYSTORE_PLACEHOLDER, mUserId); for (String keyStoreFileName : keyStoreFileNames) { try { String newFilename = Utils.replaceOnce(keyStoreFileName, String.valueOf(KEYSTORE_PLACEHOLDER), String.valueOf(mUid)); keyStorePath.findFile(keyStoreFileName).renameTo(newFilename); Path targetFile = keyStorePath.findFile(newFilename); // Restore file permission Paths.chown(targetFile, uidGidPair.uid, uidGidPair.gid); //noinspection OctalInteger Paths.chmod(targetFile, 0600); } catch (IOException | ErrnoException e) { throw new BackupException("Failed to rename KeyStore files", e); } } Runner.runCommand(new String[]{"restorecon", "-R", keyStorePath.getFilePath()}); } private void restoreData() throws BackupException { // Data restore is requested: Data restore is only possible if the app is actually // installed. So, check if it's installed first. if (mPackageInfo == null) { throw new BackupException("Data restore is requested but the app isn't installed."); } if (!mRequestedFlags.skipSignatureCheck()) { // Verify integrity of the data backups String checksum; for (int i = 0; i < mBackupMetadata.dataDirs.length; ++i) { Path[] dataFiles = mBackupItem.getDataFiles(i); if (dataFiles.length == 0) { throw new BackupException("Data restore is requested but there are no data files for index " + i + "."); } for (Path file : dataFiles) { checksum = DigestUtils.getHexDigest(mBackupInfo.checksumAlgo, file); if (!checksum.equals(mChecksum.get(file.getName()))) { throw new BackupException("Data file verification failed for index " + i + "." + "\nFile: " + file + "\nFound: " + checksum + "\nRequired: " + mChecksum.get(file.getName())); } } } } // Force-stop and clear app data PackageManagerCompat.clearApplicationUserData(mPackageName, mUserId); // Restore backups for (int i = 0; i < mBackupMetadata.dataDirs.length; ++i) { String backupDataDir = mBackupMetadata.dataDirs[i]; if (backupDataDir.equals(BackupManager.DATA_BACKUP_SPECIAL_ADB)) { // Adb backup restore restoreAdb(i); } else { // Regular directory restore restoreDirectory(mBackupMetadata.dataDirs[i], i); } } } private void restoreDirectory(@NonNull String dir, int index) throws BackupException { String dataSource = BackupUtils.getWritableDataDirectory(dir, mBackupInfo.userId, mUserId); BackupDataDirectoryInfo dataDirectoryInfo = BackupDataDirectoryInfo.getInfo(dataSource, mUserId); Path dataSourceFile = dataDirectoryInfo.getDirectory(); Path[] dataFiles = mBackupItem.getDataFiles(index); if (dataFiles.length == 0) { throw new BackupException("Data restore is requested but there are no data files for index " + index + "."); } UidGidPair uidGidPair = dataSourceFile.getUidGid(); if (uidGidPair == null) { // Fallback to app UID uidGidPair = new UidGidPair(mUid, mUid); } if (dataDirectoryInfo.isExternal()) { // Skip if external data restore is not requested switch (dataDirectoryInfo.subtype) { case BackupDataDirectoryInfo.TYPE_ANDROID_DATA: // Skip restoring Android/data directory if not requested if (!mRequestedFlags.backupExternalData()) { return; } break; case BackupDataDirectoryInfo.TYPE_ANDROID_OBB: case BackupDataDirectoryInfo.TYPE_ANDROID_MEDIA: // Skip restoring Android/data or Android/media if media/obb restore not requested if (!mRequestedFlags.backupMediaObb()) { return; } break; case BackupDataDirectoryInfo.TYPE_CREDENTIAL_PROTECTED: case BackupDataDirectoryInfo.TYPE_CUSTOM: case BackupDataDirectoryInfo.TYPE_DEVICE_PROTECTED: // NOP break; } } else { // Skip if internal data restore is not requested. if (!mRequestedFlags.backupInternalData()) { return; } } // Create data folder if not exists if (!dataSourceFile.exists()) { if (dataDirectoryInfo.isExternal() && !dataDirectoryInfo.isMounted) { if (!Utils.isRoboUnitTest()) { throw new BackupException("External directory containing " + dataSource + " is not mounted."); } // else Skip checking for mounted partition for robolectric tests } if (!dataSourceFile.mkdirs()) { throw new BackupException("Could not create directory " + dataSourceFile); } if (!dataDirectoryInfo.isExternal()) { // Restore UID, GID dataSourceFile.setUidGid(uidGidPair); } } // Decrypt data try { dataFiles = mBackupItem.decrypt(dataFiles); } catch (IOException e) { throw new BackupException("Failed to decrypt " + Arrays.toString(dataFiles), e); } // Extract data to the data directory try { String publicSourceDir = new File(Objects.requireNonNull(mPackageInfo.applicationInfo).publicSourceDir).getParent(); TarUtils.extract(mBackupInfo.tarType, dataFiles, dataSourceFile, null, BackupUtils .getExcludeDirs(!mRequestedFlags.backupCache(), null), publicSourceDir); } catch (Throwable th) { throw new BackupException("Failed to restore data files for index " + index + ".", th); } // Restore UID and GID if (!Runner.runCommand(String.format(Locale.ROOT, "chown -R %d:%d \"%s\"", uidGidPair.uid, uidGidPair.gid, dataSourceFile.getFilePath())).isSuccessful()) { if (!Utils.isRoboUnitTest()) { throw new BackupException("Failed to restore ownership info for index " + index + "."); } // else Don't care about permissions } // Restore context if (!dataDirectoryInfo.isExternal()) { Runner.runCommand(new String[]{"restorecon", "-R", dataSourceFile.getFilePath()}); } } private void restoreAdb(int index) throws BackupException { Path[] dataFiles = mBackupItem.getDataFiles(index); if (dataFiles.length != 1) { throw new BackupException("ADB restore is requested but there are no .ab files."); } // Decrypt data try { dataFiles = mBackupItem.decrypt(dataFiles); } catch (IOException e) { throw new BackupException("Failed to decrypt " + Arrays.toString(dataFiles), e); } // Restore data try (InputStream is = dataFiles[0].openInputStream()) { ParcelFileDescriptor fd = ParcelFileDescriptorUtil.pipeFrom(is); BackupCompat.adbRestore(mUserId, fd); } catch (Throwable th) { throw new BackupException("Failed to restore ADB data", th); } } private synchronized void restoreExtras() throws BackupException { if (!mIsInstalled) { throw new BackupException("Misc restore is requested but the app isn't installed."); } PseudoRules rules = new PseudoRules(mPackageName, mUserId); // Backward compatibility for restoring permissions loadMiscRules(rules); // Apply rules List entries = rules.getAll(); AppOpsManagerCompat appOpsManager = new AppOpsManagerCompat(); INotificationManager notificationManager = INotificationManager.Stub.asInterface(ProxyBinder.getService(Context.NOTIFICATION_SERVICE)); boolean magiskHideAvailable = MagiskHide.available(); boolean canModifyAppOpMode = SelfPermissions.canModifyAppOpMode(); boolean canChangeNetPolicy = SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.MANAGE_NETWORK_POLICY); for (RuleEntry entry : entries) { try { switch (entry.type) { case APP_OP: if (canModifyAppOpMode) { appOpsManager.setMode(Integer.parseInt(entry.name), mUid, mPackageName, ((AppOpRule) entry).getMode()); } break; case NET_POLICY: if (canChangeNetPolicy) { NetworkPolicyManagerCompat.setUidPolicy(mUid, ((NetPolicyRule) entry).getPolicies()); } break; case PERMISSION: { PermissionRule permissionRule = (PermissionRule) entry; Permission permission = permissionRule.getPermission(true); permission.setAppOpAllowed(permission.getAppOp() != AppOpsManagerCompat.OP_NONE && appOpsManager .checkOperation(permission.getAppOp(), mUid, mPackageName) == AppOpsManager.MODE_ALLOWED); if (permissionRule.isGranted()) { PermUtils.grantPermission(mPackageInfo, permission, appOpsManager, true, true); } else { PermUtils.revokePermission(mPackageInfo, permission, appOpsManager, true); } break; } case BATTERY_OPT: if (SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.DEVICE_POWER)) { DeviceIdleManagerCompat.disableBatteryOptimization(mPackageName); } break; case MAGISK_HIDE: { MagiskHideRule magiskHideRule = (MagiskHideRule) entry; if (magiskHideAvailable) { MagiskHide.apply(magiskHideRule.getMagiskProcess(), false); } else { // Fall-back to Magisk DenyList MagiskDenyList.apply(magiskHideRule.getMagiskProcess(), false); } break; } case MAGISK_DENY_LIST: { MagiskDenyList.apply(((MagiskDenyListRule) entry).getMagiskProcess(), false); break; } case NOTIFICATION: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 && SelfPermissions.checkNotificationListenerAccess()) { notificationManager.setNotificationListenerAccessGrantedForUser( new ComponentName(mPackageName, entry.name), mUserId, true); } break; case URI_GRANT: UriManager.UriGrant uriGrant = ((UriGrantRule) entry).getUriGrant(); UriManager.UriGrant newUriGrant = new UriManager.UriGrant( uriGrant.sourceUserId, mUserId, uriGrant.userHandle, uriGrant.sourcePkg, uriGrant.targetPkg, uriGrant.uri, uriGrant.prefix, uriGrant.modeFlags, uriGrant.createdTime); UriManager uriManager = new UriManager(); uriManager.grantUri(newUriGrant); uriManager.writeGrantedUriPermissions(); mRequiresRestart = true; break; case SSAID: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { new SsaidSettings(mUserId).setSsaid(mPackageName, mUid, ((SsaidRule) entry).getSsaid()); mRequiresRestart = true; } break; case FREEZE: int freezeType = ((FreezeRule) entry).getFreezeType(); FreezeUtils.storeFreezeMethod(mPackageName, freezeType); break; } } catch (Throwable e) { // There are several reason restoring these things go wrong, especially when // downgrading from an Android to another. It's better to simply suppress these // exceptions instead of causing a failure or worse, a crash Log.e(TAG, e); } } } private void loadMiscRules(final PseudoRules rules) throws BackupException { Path miscFile; try { miscFile = mBackupItem.getMiscFile(); } catch (IOException e) { // There are no permissions, just skip return; } if (!mRequestedFlags.skipSignatureCheck()) { String checksum = DigestUtils.getHexDigest(mBackupInfo.checksumAlgo, miscFile); if (!checksum.equals(mChecksum.get(miscFile.getName()))) { throw new BackupException("Couldn't verify misc file." + "\nFile: " + miscFile + "\nFound: " + checksum + "\nRequired: " + mChecksum.get(miscFile.getName())); } } // Decrypt permission file try { miscFile = mBackupItem.decrypt(new Path[]{miscFile})[0]; } catch (IOException | IndexOutOfBoundsException e) { throw new BackupException("Failed to decrypt " + miscFile.getName(), e); } try { rules.loadExternalEntries(miscFile); } catch (Throwable e) { throw new BackupException("Failed to load rules from misc.", e); } } private void restoreRules() throws BackupException { // Apply rules if (!mIsInstalled) { throw new BackupException("Rules restore is requested but the app isn't installed."); } Path rulesFile; try { rulesFile = mBackupItem.getRulesFile(); } catch (IOException e) { if (mBackupMetadata.hasRules) { throw new BackupException("Rules file is missing.", e); } else { // There are no rules, just skip return; } } if (!mRequestedFlags.skipSignatureCheck()) { String checksum = DigestUtils.getHexDigest(mBackupInfo.checksumAlgo, rulesFile); if (!checksum.equals(mChecksum.get(rulesFile.getName()))) { throw new BackupException("Couldn't verify permission file." + "\nFile: " + rulesFile + "\nFound: " + checksum + "\nRequired: " + mChecksum.get(rulesFile.getName())); } } // Decrypt rules file try { rulesFile = mBackupItem.decrypt(new Path[]{rulesFile})[0]; } catch (IOException | IndexOutOfBoundsException e) { throw new BackupException("Failed to decrypt " + rulesFile.getName(), e); } try (RulesImporter importer = new RulesImporter(Arrays.asList(RuleType.values()), new int[]{mUserId})) { importer.addRulesFromPath(rulesFile); importer.setPackagesToImport(Collections.singletonList(mPackageName)); importer.applyRules(true); } catch (IOException e) { throw new BackupException("Failed to restore rules file.", e); } } private void deleteFiles(@NonNull Path[] files) { for (Path file : files) { file.delete(); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/backup/VerifyOp.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.backup; import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; import java.io.Closeable; import java.io.IOException; import io.github.muntashirakon.AppManager.backup.struct.BackupMetadataV5; import io.github.muntashirakon.AppManager.crypto.CryptoException; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.utils.DigestUtils; import io.github.muntashirakon.io.Path; @WorkerThread class VerifyOp implements Closeable { static final String TAG = VerifyOp.class.getSimpleName(); @NonNull private final BackupFlags mBackupFlags; @NonNull private final BackupMetadataV5.Info mBackupInfo; @NonNull private final BackupMetadataV5.Metadata mBackupMetadata; @NonNull private final BackupItems.BackupItem mBackupItem; @NonNull private final BackupItems.Checksum mChecksum; VerifyOp(@NonNull BackupItems.BackupItem backupItem) throws BackupException { mBackupItem = backupItem; try { mBackupInfo = mBackupItem.getInfo(); mBackupFlags = mBackupInfo.flags; } catch (IOException e) { mBackupItem.cleanup(); throw new BackupException("Could not read backup info. Possibly due to a malformed json file.", e); } // Setup crypto if (!CryptoUtils.isAvailable(mBackupInfo.crypto)) { mBackupItem.cleanup(); throw new BackupException("Mode " + mBackupInfo.crypto + " is currently unavailable."); } try { mBackupItem.setCrypto(mBackupInfo.getCrypto()); } catch (CryptoException e) { mBackupItem.cleanup(); throw new BackupException("Could not get crypto " + mBackupInfo.crypto, e); } try { mBackupMetadata = mBackupItem.getMetadata(mBackupInfo).metadata; } catch (IOException e) { mBackupItem.cleanup(); throw new BackupException("Could not read backup metadata. Possibly due to a malformed json file.", e); } // Get checksums try { mChecksum = mBackupItem.getChecksum(); } catch (Throwable e) { mBackupItem.cleanup(); throw new BackupException("Could not get checksums.", e); } // Verify metadata try { verifyMetadata(); } catch (BackupException e) { mBackupItem.cleanup(); throw e; } } @Override public void close() { Log.d(TAG, "Close called"); mChecksum.close(); mBackupItem.cleanup(); } void verify() throws BackupException { try { // No need to check master key as it varies from device to device and APK signing key checksum as it would // remain intact if the APK files are not modified. if (mBackupFlags.backupApkFiles()) { verifyApkFiles(); } if (mBackupFlags.backupData()) { verifyData(); if (mBackupMetadata.keyStore) { verifyKeyStore(); } } if (mBackupFlags.backupExtras()) { verifyExtras(); } if (mBackupFlags.backupRules()) { verifyRules(); } } catch (BackupException e) { throw e; } catch (Throwable th) { throw new BackupException("Unknown error occurred", th); } } private void verifyMetadata() throws BackupException { boolean isV5AndUp = mBackupItem.isV5AndUp(); if (isV5AndUp) { Path infoFile; try { infoFile = mBackupItem.getInfoFile(); } catch (IOException e) { throw new BackupException("Could not get metadata file.", e); } String checksum = DigestUtils.getHexDigest(mBackupInfo.checksumAlgo, infoFile); if (!checksum.equals(mChecksum.get(infoFile.getName()))) { throw new BackupException("Couldn't verify metadata file." + "\nFile: " + infoFile + "\nFound: " + checksum + "\nRequired: " + mChecksum.get(infoFile.getName())); } } Path metadataFile; try { metadataFile = isV5AndUp ? mBackupItem.getMetadataV5File(false) : mBackupItem.getMetadataV2File(); } catch (IOException e) { throw new BackupException("Could not get metadata file.", e); } String checksum = DigestUtils.getHexDigest(mBackupInfo.checksumAlgo, metadataFile); if (!checksum.equals(mChecksum.get(metadataFile.getName()))) { throw new BackupException("Couldn't verify metadata file." + "\nFile: " + metadataFile + "\nFound: " + checksum + "\nRequired: " + mChecksum.get(metadataFile.getName())); } } private void verifyApkFiles() throws BackupException { Path[] backupSourceFiles = mBackupItem.getSourceFiles(); if (backupSourceFiles.length == 0) { // No APK files found throw new BackupException("Backup does not contain any APK files."); } String checksum; for (Path file : backupSourceFiles) { checksum = DigestUtils.getHexDigest(mBackupInfo.checksumAlgo, file); if (!checksum.equals(mChecksum.get(file.getName()))) { throw new BackupException("Could not verify APK files." + "\nFile: " + file.getName() + "\nFound: " + checksum + "\nRequired: " + mChecksum.get(file.getName())); } } } private void verifyKeyStore() throws BackupException { Path[] keyStoreFiles = mBackupItem.getKeyStoreFiles(); if (keyStoreFiles.length == 0) { // Not having KeyStore backups is fine. return; } String checksum; for (Path file : keyStoreFiles) { checksum = DigestUtils.getHexDigest(mBackupInfo.checksumAlgo, file); if (!checksum.equals(mChecksum.get(file.getName()))) { throw new BackupException("Could not verify KeyStore files." + "\nFile: " + file.getName() + "\nFound: " + checksum + "\nRequired: " + mChecksum.get(file.getName())); } } } private void verifyData() throws BackupException { Path[] dataFiles; String checksum; for (int i = 0; i < mBackupMetadata.dataDirs.length; ++i) { dataFiles = mBackupItem.getDataFiles(i); if (dataFiles.length == 0) { throw new BackupException("No data files at index " + i + "."); } for (Path file : dataFiles) { checksum = DigestUtils.getHexDigest(mBackupInfo.checksumAlgo, file); if (!checksum.equals(mChecksum.get(file.getName()))) { throw new BackupException("Could not verify data files at index " + i + "." + "\nFile: " + file.getName() + "\nFound: " + checksum + "\nRequired: " + mChecksum.get(file.getName())); } } } } private void verifyExtras() throws BackupException { Path miscFile; try { miscFile = mBackupItem.getMiscFile(); } catch (IOException ignore) { // There are no permissions, just skip return; } String checksum = DigestUtils.getHexDigest(mBackupInfo.checksumAlgo, miscFile); if (!checksum.equals(mChecksum.get(miscFile.getName()))) { throw new BackupException("Could not verify extras." + "\nFile: " + miscFile.getName() + "\nFound: " + checksum + "\nRequired: " + mChecksum.get(miscFile.getName())); } } private void verifyRules() throws BackupException { Path rulesFile; try { rulesFile = mBackupItem.getRulesFile(); } catch (IOException e) { if (mBackupMetadata.hasRules) { throw new BackupException("Rules file is missing.", e); } else { // There are no rules, just skip return; } } String checksum = DigestUtils.getHexDigest(mBackupInfo.checksumAlgo, rulesFile); if (!checksum.equals(mChecksum.get(rulesFile.getName()))) { throw new BackupException("Could not verify rules file." + "\nFile: " + rulesFile.getName() + "\nFound: " + checksum + "\nRequired: " + mChecksum.get(rulesFile.getName())); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/backup/adb/AndroidBackupCreator.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.backup.adb; import static io.github.muntashirakon.AppManager.backup.adb.BackupCategories.CAT_EXT; import static io.github.muntashirakon.AppManager.backup.adb.BackupCategories.CAT_INT_CE; import static io.github.muntashirakon.AppManager.backup.adb.BackupCategories.CAT_INT_DE; import static io.github.muntashirakon.AppManager.backup.adb.BackupCategories.CAT_OBB; import static io.github.muntashirakon.AppManager.backup.adb.BackupCategories.CAT_SRC; import android.content.pm.PackageInfo; import android.content.pm.Signature; import android.os.Build; import android.util.StringBuilderPrinter; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.pm.PackageInfoCompat; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; import org.apache.commons.compress.archivers.tar.TarConstants; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import io.github.muntashirakon.AppManager.apk.signing.SignerInfo; import io.github.muntashirakon.AppManager.backup.BackupUtils; import io.github.muntashirakon.AppManager.utils.ExUtils; import io.github.muntashirakon.AppManager.utils.PackageUtils; import io.github.muntashirakon.AppManager.utils.TarUtils; import io.github.muntashirakon.io.IoUtils; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.SplitInputStream; public class AndroidBackupCreator implements AutoCloseable { public static final String TAG = AndroidBackupCreator.class.getSimpleName(); public static void fromTar(@NonNull Path tarSource, @NonNull Path abDest, @Nullable char[] password, int api, boolean compress) throws IOException { int backupFileVersion = Constants.getBackupFileVersionFromApi(api); AndroidBackupHeader header = new AndroidBackupHeader(backupFileVersion, compress, password); try (InputStream is = tarSource.openInputStream(); OutputStream realOs = header.write(abDest.openOutputStream())) { IoUtils.copy(is, realOs); } catch (IOException e) { throw e; } catch (Exception e) { ExUtils.rethrowAsIOException(e); } } @NonNull private final Path mWorkingDir; @NonNull private final String mPackageName; @NonNull private final PackageInfo mPackageInfo; @Nullable private final String mInstallerPackage; private final Map> mCategoryFilesMap; @NonNull @TarUtils.TarType private final String mTarType; private final List mFilesToBeDeleted = new ArrayList<>(); public AndroidBackupCreator(@NonNull Map> categoryFilesMap, @NonNull Path temporaryDir, @NonNull PackageInfo packageInfo, @Nullable String installerPackage, @NonNull @TarUtils.TarType String tarType) { mCategoryFilesMap = new HashMap<>(categoryFilesMap); mWorkingDir = temporaryDir; mPackageInfo = packageInfo; mPackageName = packageInfo.packageName; mInstallerPackage = installerPackage; mTarType = tarType; } @Override public void close() { for (Path file : mFilesToBeDeleted) { file.delete(); } } public Path getBackupFile(int dataIndex) throws IOException { // Create temporary merged TAR file String backupFilename = BackupUtils.getDataFilePrefix(dataIndex, null); Path tempTarFile = mWorkingDir.createNewFile(backupFilename + ".tar", null); mFilesToBeDeleted.add(tempTarFile); Path backupFile = mWorkingDir.createNewFile(backupFilename + ".ab", null); // Merge all category files into a single TAR mergeCategoryFilesIntoTar(tempTarFile); // Convert to AB file fromTar(tempTarFile, backupFile, null, Build.VERSION.SDK_INT, true); return backupFile; } private void mergeCategoryFilesIntoTar(@NonNull Path outputTarFile) throws IOException { try (OutputStream fos = outputTarFile.openOutputStream(); BufferedOutputStream bos = new BufferedOutputStream(fos); TarArchiveOutputStream taos = new TarArchiveOutputStream(bos)) { taos.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); taos.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_POSIX); // Add manifest file first addManifestEntry(taos); // Process each category for (Map.Entry> entry : mCategoryFilesMap.entrySet()) { int category = entry.getKey(); List files = entry.getValue(); if (files != null && !files.isEmpty()) { processCategoryFiles(taos, category, files); } } taos.finish(); } } private void addManifestEntry(@NonNull TarArchiveOutputStream taos) throws IOException { String manifestPath = Constants.APPS_PREFIX + mPackageName + File.separator + Constants.BACKUP_MANIFEST_FILENAME; byte[] manifestContent = getManifestBytes(mCategoryFilesMap.get(CAT_SRC) != null); TarArchiveEntry manifestEntry = new TarArchiveEntry(manifestPath); manifestEntry.setSize(manifestContent.length); manifestEntry.setMode(0600); // rw------- manifestEntry.setModTime(0); // See AppMetadataBackupWriter.java taos.putArchiveEntry(manifestEntry); taos.write(manifestContent); taos.closeArchiveEntry(); } // See AppMetadataBackupWriter#getManifestBytes(PackageInfo, boolean) @NonNull private byte[] getManifestBytes(boolean withApk) { StringBuilder builder = new StringBuilder(4096); StringBuilderPrinter printer = new StringBuilderPrinter(builder); printer.println(Integer.toString(Constants.BACKUP_MANIFEST_VERSION)); printer.println(mPackageName); printer.println(Long.toString(PackageInfoCompat.getLongVersionCode(mPackageInfo))); printer.println(Integer.toString(Build.VERSION.SDK_INT)); printer.println((mInstallerPackage != null) ? mInstallerPackage : ""); printer.println(withApk ? "1" : "0"); // Write the signature block. SignerInfo signerInfo = PackageUtils.getSignerInfo(mPackageInfo, true); if (signerInfo == null || signerInfo.getCurrentSignerCerts() == null) { printer.println("0"); } else { // Retrieve the newest signatures to write. try { X509Certificate[] signerCerts = signerInfo.getCurrentSignerCerts(); Signature[] signatures = new Signature[signerCerts.length]; for (int i = 0; i < signatures.length; ++i) { signatures[i] = new Signature(signerCerts[i].getEncoded()); } printer.println(Integer.toString(signerCerts.length)); for (Signature sig : signatures) { printer.println(sig.toCharsString()); } } catch (CertificateEncodingException e) { // Fall back to 0 printer.println("0"); } } return builder.toString().getBytes(); } private void processCategoryFiles(@NonNull TarArchiveOutputStream taos, int category, @NonNull List files) throws IOException { try (SplitInputStream sis = new SplitInputStream(files); BufferedInputStream bis = new BufferedInputStream(sis); InputStream decompressedStream = TarUtils.createDecompressedStream(bis, mTarType); TarArchiveInputStream tis = new TarArchiveInputStream(decompressedStream)) { TarArchiveEntry entry; while ((entry = tis.getNextTarEntry()) != null) { String transformedPath = transformEntryPath(entry.getName(), category); byte linkFlag; if (entry.isSymbolicLink()) { linkFlag = TarConstants.LF_SYMLINK; } else if (entry.isDirectory()) { linkFlag = TarConstants.LF_DIR; } else linkFlag = TarConstants.LF_NORMAL; // Create new entry with transformed path TarArchiveEntry newEntry = new TarArchiveEntry(transformedPath, linkFlag); newEntry.setSize(entry.getSize()); newEntry.setMode(entry.getMode()); newEntry.setModTime(entry.getModTime()); newEntry.setUserId(entry.getUserId()); newEntry.setGroupId(entry.getGroupId()); newEntry.setUserName(entry.getUserName()); newEntry.setGroupName(entry.getGroupName()); if (entry.isSymbolicLink()) { newEntry.setLinkName(entry.getLinkName()); } taos.putArchiveEntry(newEntry); if (linkFlag == TarConstants.LF_NORMAL) { IoUtils.copy(tis, taos); } taos.closeArchiveEntry(); } } } @NonNull private String transformEntryPath(@NonNull String originalPath, int category) { String basePath = Constants.APPS_PREFIX + mPackageName + File.separator; if (originalPath.endsWith(File.separator)) { // AB expects no trailing slashes originalPath = originalPath.substring(0, originalPath.length() - 1); } switch (category) { case CAT_SRC: return basePath + Constants.APK_TREE_TOKEN + File.separator + originalPath; case CAT_INT_CE: return transformInternalPath(basePath, originalPath, false); case CAT_INT_DE: return transformInternalPath(basePath, originalPath, true); case CAT_EXT: return basePath + Constants.MANAGED_EXTERNAL_TREE_TOKEN + File.separator + originalPath; case CAT_OBB: return basePath + Constants.OBB_TREE_TOKEN + File.separator + originalPath; default: throw new IllegalArgumentException("Invalid category: " + category); } } @NonNull private String transformInternalPath(@NonNull String basePath, @NonNull String path, boolean isDE) { String prefix; if (path.startsWith("files/")) { prefix = isDE ? Constants.DEVICE_FILES_TREE_TOKEN : Constants.FILES_TREE_TOKEN; path = path.substring(6); // Remove "files/" } else if (path.startsWith("databases/")) { prefix = isDE ? Constants.DEVICE_DATABASE_TREE_TOKEN : Constants.DATABASE_TREE_TOKEN; path = path.substring(10); // Remove "databases/" } else if (path.startsWith("shared_prefs/")) { prefix = isDE ? Constants.DEVICE_SHAREDPREFS_TREE_TOKEN : Constants.SHAREDPREFS_TREE_TOKEN; path = path.substring(13); // Remove "shared_prefs/" } else if (path.startsWith("no_backup/")) { prefix = isDE ? Constants.DEVICE_NO_BACKUP_TREE_TOKEN : Constants.NO_BACKUP_TREE_TOKEN; path = path.substring(10); // Remove "no_backup/" } else if (path.startsWith("caches/")) { prefix = isDE ? Constants.DEVICE_CACHE_TREE_TOKEN : Constants.CACHE_TREE_TOKEN; path = path.substring(7); // Remove "caches/" } else prefix = isDE ? Constants.DEVICE_ROOT_TREE_TOKEN : Constants.ROOT_TREE_TOKEN; return basePath + prefix + File.separator + path; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/backup/adb/AndroidBackupExtractor.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.backup.adb; import static io.github.muntashirakon.AppManager.backup.adb.BackupCategories.CAT_EXT; import static io.github.muntashirakon.AppManager.backup.adb.BackupCategories.CAT_INT_CE; import static io.github.muntashirakon.AppManager.backup.adb.BackupCategories.CAT_INT_DE; import static io.github.muntashirakon.AppManager.backup.adb.BackupCategories.CAT_OBB; import static io.github.muntashirakon.AppManager.backup.adb.BackupCategories.CAT_SRC; import static io.github.muntashirakon.AppManager.backup.adb.BackupCategories.CAT_UNK; import static io.github.muntashirakon.AppManager.utils.TarUtils.DEFAULT_SPLIT_SIZE; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; import org.apache.commons.compress.archivers.tar.TarConstants; import java.io.BufferedOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import io.github.muntashirakon.AppManager.backup.BackupUtils; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.utils.ExUtils; import io.github.muntashirakon.AppManager.utils.TarUtils; import io.github.muntashirakon.io.IoUtils; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; import io.github.muntashirakon.io.SplitOutputStream; public class AndroidBackupExtractor implements AutoCloseable { public static final String TAG = AndroidBackupExtractor.class.getSimpleName(); public static void toTar(@NonNull Path abSource, @NonNull Path tarDest, @Nullable char[] password) throws IOException { AndroidBackupHeader header = new AndroidBackupHeader(password); try (OutputStream os = tarDest.openOutputStream(); InputStream realIs = header.read(abSource.openInputStream())) { IoUtils.copy(realIs, os); } catch (Exception e) { ExUtils.rethrowAsIOException(e); } } private final Path mWorkingDir; private final Map> mCategoryTargetEntriesMap = new HashMap<>(); private final List mFilesToBeDeleted = new ArrayList<>(); public AndroidBackupExtractor(@NonNull Path abFile, @NonNull Path temporaryDir, @NonNull String packageName) throws IOException { mWorkingDir = temporaryDir; String relativeDirInAb = Constants.APPS_PREFIX + packageName + File.separator; String abFilename = Paths.trimPathExtension(abFile.getName()); Path tarFile = temporaryDir.createNewFile(abFilename + ".tar", null); mFilesToBeDeleted.add(tarFile); Path dest = temporaryDir.createNewDirectory(abFilename); mFilesToBeDeleted.add(dest); toTar(abFile, tarFile, null); try (InputStream fis = tarFile.openInputStream(); TarArchiveInputStream tis = new TarArchiveInputStream(fis)) { String realDestPath = dest.getRealFilePath(); int relDirSize = relativeDirInAb.length(); TarArchiveEntry entry; while ((entry = tis.getNextTarEntry()) != null) { String filename = Paths.normalize(entry.getName()); // Early zip slip vulnerability check to avoid creating any files at all if (filename == null || filename.startsWith("../")) { throw new IOException("Zip slip vulnerability detected!" + "\nExpected dest: " + new File(realDestPath, entry.getName()) + "\nActual path: " + (filename != null ? new File(realDestPath, filename) : realDestPath)); } if (!filename.startsWith(relativeDirInAb)) { throw new IOException("Unsupported file in AB: " + filename); } // Remove apps/{packageName}/ part filename = filename.substring(relDirSize); Path file; if (entry.isDirectory()) { file = dest.createDirectoriesIfRequired(filename); } else file = dest.createNewArbitraryFile(filename, null); // Check if the given entry is a link. if (entry.isSymbolicLink() && file.getFilePath() != null) { String linkName = entry.getLinkName(); file.delete(); file.createNewSymbolicLink(linkName); } else { // Zip slip vulnerability might still be present String realFilePath = file.getRealFilePath(); if (realDestPath != null && realFilePath != null && !realFilePath.startsWith(realDestPath)) { throw new IOException("Zip slip vulnerability detected!" + "\nExpected dest: " + new File(realDestPath, entry.getName()) + "\nActual path: " + realFilePath); } if (!entry.isDirectory()) { try (OutputStream os = file.openOutputStream()) { IoUtils.copy(tis, os); } } } // Categorize and build TarArchiveEntry int category = getCategory(filename); if (category == CAT_UNK && filename.equals(Constants.BACKUP_MANIFEST_FILENAME)) { // Ignore manifest file continue; } TarArchiveEntry targetEntry = getTargetArchiveEntry(entry, filename); List targetTarEntries = mCategoryTargetEntriesMap.get(category); if (targetTarEntries == null) { targetTarEntries = new ArrayList<>(); mCategoryTargetEntriesMap.put(category, targetTarEntries); } targetTarEntries.add(new TargetTarEntry(file, filename, category, targetEntry)); } } // Validate UNK entries if (mCategoryTargetEntriesMap.get(CAT_UNK) != null) { Log.w(TAG, "Unknown entries: " + mCategoryTargetEntriesMap.get(CAT_UNK)); throw new IOException("Unknown/unsupported entries detected."); } } @Override public void close() { for (Path file : mFilesToBeDeleted) { file.delete(); } } @Nullable public Path[] getSourceFiles(@NonNull String extension, @TarUtils.TarType String tarType) throws IOException { return getFiles(CAT_SRC, 0, extension, tarType); } @Nullable public Path[] getInternalCeDataFiles(int dataIndex, @NonNull String extension, @TarUtils.TarType String tarType) throws IOException { return getFiles(CAT_INT_CE, dataIndex, extension, tarType); } @Nullable public Path[] getInternalDeDataFiles(int dataIndex, @NonNull String extension, @TarUtils.TarType String tarType) throws IOException { return getFiles(CAT_INT_DE, dataIndex, extension, tarType); } @Nullable public Path[] getExternalDataFiles(int dataIndex, @NonNull String extension, @TarUtils.TarType String tarType) throws IOException { return getFiles(CAT_EXT, dataIndex, extension, tarType); } @Nullable public Path[] getObbFiles(int dataIndex, @NonNull String extension, @TarUtils.TarType String tarType) throws IOException { return getFiles(CAT_OBB, dataIndex, extension, tarType); } @Nullable public Path[] getFiles(int category, int dataIndex, @NonNull String extension, @TarUtils.TarType String tarType) throws IOException { if (category >= CAT_UNK) { throw new IllegalArgumentException("Invalid category: " + category); } List targetTarEntries = mCategoryTargetEntriesMap.get(category); if (targetTarEntries == null) { return null; } String filePrefix = category == CAT_SRC ? BackupUtils.getSourceFilePrefix(extension) : BackupUtils.getDataFilePrefix(dataIndex, extension); try (SplitOutputStream sos = new SplitOutputStream(mWorkingDir, filePrefix, DEFAULT_SPLIT_SIZE); BufferedOutputStream bos = new BufferedOutputStream(sos); OutputStream os = TarUtils.createCompressedStream(bos, tarType)) { try (TarArchiveOutputStream tos = new TarArchiveOutputStream(os)) { tos.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); tos.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_POSIX); for (TargetTarEntry entry : targetTarEntries) { if (entry.targetEntry.isSymbolicLink()) { // Add the link as is tos.putArchiveEntry(entry.targetEntry); } else { tos.putArchiveEntry(entry.targetEntry); if (!entry.targetEntry.isDirectory()) { try (InputStream is = entry.sourceFile.openInputStream()) { IoUtils.copy(is, tos); } } } tos.closeArchiveEntry(); } tos.finish(); } finally { os.close(); } return Paths.getSortedPaths(sos.getFiles().toArray(new Path[0])); } } private int getCategory(@NonNull String filename) { //noinspection SuspiciousRegexArgument Not on Windows String firstPart = filename.split(File.separator, 2)[0]; switch (firstPart) { case Constants.APK_TREE_TOKEN: return CAT_SRC; case Constants.OBB_TREE_TOKEN: return CAT_OBB; case Constants.MANAGED_EXTERNAL_TREE_TOKEN: return CAT_EXT; case Constants.ROOT_TREE_TOKEN: case Constants.FILES_TREE_TOKEN: case Constants.NO_BACKUP_TREE_TOKEN: case Constants.DATABASE_TREE_TOKEN: case Constants.SHAREDPREFS_TREE_TOKEN: case Constants.CACHE_TREE_TOKEN: return CAT_INT_CE; case Constants.DEVICE_ROOT_TREE_TOKEN: case Constants.DEVICE_FILES_TREE_TOKEN: case Constants.DEVICE_NO_BACKUP_TREE_TOKEN: case Constants.DEVICE_DATABASE_TREE_TOKEN: case Constants.DEVICE_SHAREDPREFS_TREE_TOKEN: case Constants.DEVICE_CACHE_TREE_TOKEN: return CAT_INT_DE; default: return CAT_UNK; } } @NonNull private TarArchiveEntry getTargetArchiveEntry(@NonNull TarArchiveEntry src, @NonNull String filename) { String realFilename = getRealFilename(filename); if (src.isSymbolicLink()) { TarArchiveEntry dst = new TarArchiveEntry(realFilename, TarConstants.LF_SYMLINK); dst.setLinkName(src.getLinkName()); return dst; } // Regular file/folder byte flag = src.isDirectory() ? TarConstants.LF_DIR : TarConstants.LF_NORMAL; TarArchiveEntry dst = new TarArchiveEntry(realFilename, flag); dst.setSize(src.getSize()); dst.setMode(src.getMode()); dst.setModTime(src.getModTime()); dst.setUserId(src.getUserId()); dst.setGroupId(src.getGroupId()); dst.setUserName(src.getUserName()); dst.setGroupName(src.getGroupName()); return dst; } @NonNull private String getRealFilename(@NonNull String filename) { //noinspection SuspiciousRegexArgument Not on Windows String[] parts = filename.split(File.separator, 2); String firstPart = parts[0]; String secondPart = parts[1]; switch (firstPart) { case Constants.FILES_TREE_TOKEN: case Constants.DEVICE_FILES_TREE_TOKEN: return "files/" + secondPart; case Constants.NO_BACKUP_TREE_TOKEN: case Constants.DEVICE_NO_BACKUP_TREE_TOKEN: return "no_backup/" + secondPart; case Constants.DATABASE_TREE_TOKEN: case Constants.DEVICE_DATABASE_TREE_TOKEN: return "databases/" + secondPart; case Constants.SHAREDPREFS_TREE_TOKEN: case Constants.DEVICE_SHAREDPREFS_TREE_TOKEN: return "shared_prefs/" + secondPart; case Constants.CACHE_TREE_TOKEN: case Constants.DEVICE_CACHE_TREE_TOKEN: return "caches/" + secondPart; default: return secondPart; } } private static class TargetTarEntry { @NonNull public final Path sourceFile; @NonNull public final String sourceFilename; public final int category; @NonNull public final TarArchiveEntry targetEntry; private TargetTarEntry(@NonNull Path sourceFile, @NonNull String sourceFilename, int category, @NonNull TarArchiveEntry targetEntry) { this.sourceFile = sourceFile; this.sourceFilename = sourceFilename; this.category = category; this.targetEntry = targetEntry; } @NonNull @Override public String toString() { return sourceFilename; } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/backup/adb/AndroidBackupHeader.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.backup.adb; import static io.github.muntashirakon.AppManager.backup.adb.Constants.BACKUP_FILE_HEADER_MAGIC; import static io.github.muntashirakon.AppManager.backup.adb.Constants.BACKUP_FILE_VERSION; import static io.github.muntashirakon.AppManager.backup.adb.Constants.ENCRYPTION_ALGORITHM_NAME; import static io.github.muntashirakon.AppManager.backup.adb.Constants.PBKDF2_HASH_ROUNDS; import static io.github.muntashirakon.AppManager.backup.adb.Constants.PBKDF2_KEY_SIZE; import static io.github.muntashirakon.AppManager.backup.adb.Constants.PBKDF2_SALT_SIZE; import static io.github.muntashirakon.AppManager.backup.adb.Constants.PBKDF_CURRENT; import static io.github.muntashirakon.AppManager.backup.adb.Constants.PBKDF_FALLBACK; import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.security.Key; import java.security.MessageDigest; import java.security.SecureRandom; import java.security.spec.KeySpec; import java.util.Arrays; import java.util.zip.Deflater; import java.util.zip.DeflaterOutputStream; import java.util.zip.InflaterInputStream; import javax.crypto.Cipher; import javax.crypto.CipherInputStream; import javax.crypto.CipherOutputStream; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.SecretKeySpec; import aosp.libcore.util.HexEncoding; final class AndroidBackupHeader { // NOTE: (CWE-326) Vulnerable to padding oracle attacks, but there's no way to fix it as it's used by Android. private static final String TRANSFORMATION = "AES/CBC/PKCS5Padding"; private final SecureRandom mRng = new SecureRandom(); private int mBackupFileVersion; private boolean mCompress; @Nullable private final char[] mPassword; public AndroidBackupHeader(int backupFileVersion, boolean compress, @Nullable char[] password) { mBackupFileVersion = backupFileVersion; mCompress = compress; mPassword = password; } public AndroidBackupHeader(@Nullable char[] password) { mBackupFileVersion = Constants.getBackupFileVersionFromApi(Build.VERSION.SDK_INT); mCompress = true; mPassword = password; } @NonNull public InputStream read(@NonNull InputStream backupStream) throws Exception { // First, parse out the unencrypted/uncompressed header InputStream preCompressStream = backupStream; final int headerLen = BACKUP_FILE_HEADER_MAGIC.length(); byte[] streamHeader = new byte[headerLen]; readFullyOrThrow(backupStream, streamHeader); byte[] magicBytes = BACKUP_FILE_HEADER_MAGIC.getBytes(StandardCharsets.UTF_8); if (Arrays.equals(magicBytes, streamHeader)) { // okay, header looks good. now parse out the rest of the fields. String s = readHeaderLine(backupStream); mBackupFileVersion = Integer.parseInt(s); if (mBackupFileVersion <= BACKUP_FILE_VERSION) { // okay, it's a version we recognize. if it's version 1, we may need // to try two different PBKDF2 regimes to compare checksums. final boolean pbkdf2Fallback = (mBackupFileVersion == 1); s = readHeaderLine(backupStream); mCompress = (Integer.parseInt(s) != 0); s = readHeaderLine(backupStream); if (s.equals("none")) { // no more header to parse; we're good to go } else if (mPassword != null && mPassword.length > 0) { // AES-256 preCompressStream = decodeAesHeaderAndInitialize(mPassword, s, pbkdf2Fallback, backupStream); } else { throw new IOException("Archive is encrypted but no password given"); } } else { throw new IOException("Wrong header version: " + s); } } else { throw new IOException("Didn't read the right header magic"); } // okay, use the right stream layer based on compression return mCompress ? new InflaterInputStream(preCompressStream) : preCompressStream; } @NonNull public OutputStream write(@NonNull OutputStream backupStream) throws Exception { // Write the global file header. All strings are UTF-8 encoded; lines end // with a '\n' byte. Actual backup data begins immediately following the // final '\n'. // // line 1: "ANDROID BACKUP" // line 2: backup file format version, currently "5" // line 3: compressed? "0" if not compressed, "1" if compressed. // line 4: name of encryption algorithm [currently only "none" or "AES-256"] // // When line 4 is not "none", then additional header data follows: // // line 5: user password salt [hex] // line 6: encryption key checksum salt [hex] // line 7: number of PBKDF2 rounds to use (same for user & encryption key) [decimal] // line 8: IV of the user key [hex] // line 9: encryption key blob [hex] // IV of the encryption key, encryption key itself, encryption key checksum hash // // The encryption key checksum is the encryption key plus its checksum salt, run through // 10k rounds of PBKDF2. This is used to verify that the user has supplied the // correct password for decrypting the archive: the encryption key decrypted from // the archive using the user-supplied password is also run through PBKDF2 in // this way, and if the result does not match the checksum as stored in the // archive, then we know that the user-supplied password does not match the // archive's. StringBuilder headerbuf = new StringBuilder(1024); headerbuf.append(BACKUP_FILE_HEADER_MAGIC); headerbuf.append(mBackupFileVersion); // integer, no trailing \n headerbuf.append(mCompress ? "\n1\n" : "\n0\n"); OutputStream finalOutput = backupStream; // Set up the encryption stage if appropriate, and emit the correct header if (mPassword != null) { finalOutput = emitAesBackupHeader(headerbuf, backupStream); } else { headerbuf.append("none\n"); } byte[] header = headerbuf.toString().getBytes(StandardCharsets.UTF_8); backupStream.write(header); // Set up the compression stage feeding into the encryption stage (if any) if (mCompress) { Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION); finalOutput = new DeflaterOutputStream(finalOutput, deflater, true); } return finalOutput; } @NonNull private OutputStream emitAesBackupHeader(@NonNull StringBuilder headerbuf, @NonNull OutputStream ofstream) throws Exception { // User key will be used to encrypt the encryption key. byte[] newUserSalt = randomBytes(PBKDF2_SALT_SIZE); SecretKey userKey = buildCharArrayKey(PBKDF_CURRENT, mPassword, newUserSalt, PBKDF2_HASH_ROUNDS); // the encryption key is random for each backup byte[] encryptionKey = new byte[256 / 8]; mRng.nextBytes(encryptionKey); byte[] checksumSalt = randomBytes(PBKDF2_SALT_SIZE); // primary encryption of the datastream with the encryption key Cipher c = Cipher.getInstance(TRANSFORMATION); SecretKeySpec encryptionKeySpec = new SecretKeySpec(encryptionKey, "AES"); c.init(Cipher.ENCRYPT_MODE, encryptionKeySpec); OutputStream finalOutput = new CipherOutputStream(ofstream, c); // line 4: name of encryption algorithm headerbuf.append(ENCRYPTION_ALGORITHM_NAME); headerbuf.append('\n'); // line 5: user password salt [hex] headerbuf.append(byteArrayToHex(newUserSalt)); headerbuf.append('\n'); // line 6: encryption key checksum salt [hex] headerbuf.append(byteArrayToHex(checksumSalt)); headerbuf.append('\n'); // line 7: number of PBKDF2 rounds used [decimal] headerbuf.append(PBKDF2_HASH_ROUNDS); headerbuf.append('\n'); // line 8: IV of the user key [hex] Cipher mkC = Cipher.getInstance(TRANSFORMATION); mkC.init(Cipher.ENCRYPT_MODE, userKey); byte[] IV = mkC.getIV(); headerbuf.append(byteArrayToHex(IV)); headerbuf.append('\n'); // line 9: encryption IV + key blob, encrypted by the user key [hex]. Blob format: // [byte] IV length = Niv // [array of Niv bytes] IV itself // [byte] encryption key length = Nek // [array of Nek bytes] encryption key itself // [byte] encryption key checksum hash length = Nck // [array of Nck bytes] encryption key checksum hash // // The checksum is the (encryption key + checksum salt), run through the // stated number of PBKDF2 rounds IV = c.getIV(); byte[] mk = encryptionKeySpec.getEncoded(); byte[] checksum = makeKeyChecksum(PBKDF_CURRENT, encryptionKeySpec.getEncoded(), checksumSalt, PBKDF2_HASH_ROUNDS); ByteArrayOutputStream blob = new ByteArrayOutputStream(IV.length + mk.length + checksum.length + 3); DataOutputStream mkOut = new DataOutputStream(blob); mkOut.writeByte(IV.length); mkOut.write(IV); mkOut.writeByte(mk.length); mkOut.write(mk); mkOut.writeByte(checksum.length); mkOut.write(checksum); mkOut.flush(); byte[] encryptedMk = mkC.doFinal(blob.toByteArray()); headerbuf.append(byteArrayToHex(encryptedMk)); headerbuf.append('\n'); return finalOutput; } @NonNull private static InputStream decodeAesHeaderAndInitialize(char[] decryptPassword, @NonNull String encryptionName, boolean pbkdf2Fallback, @NonNull InputStream rawInStream) throws Exception { if (!encryptionName.equals(ENCRYPTION_ALGORITHM_NAME)) { throw new IOException("Unsupported encryption method: " + encryptionName); } String userSaltHex = readHeaderLine(rawInStream); // 5 byte[] userSalt = hexToByteArray(userSaltHex); String ckSaltHex = readHeaderLine(rawInStream); // 6 byte[] ckSalt = hexToByteArray(ckSaltHex); int rounds = Integer.parseInt(readHeaderLine(rawInStream)); // 7 String userIvHex = readHeaderLine(rawInStream); // 8 String encryptionKeyBlobHex = readHeaderLine(rawInStream); // 9 // decrypt the encryption key blob try { return attemptEncryptionKeyDecryption(decryptPassword, PBKDF_CURRENT, userSalt, ckSalt, rounds, userIvHex, encryptionKeyBlobHex, rawInStream); } catch (Exception e) { if (pbkdf2Fallback) { return attemptEncryptionKeyDecryption(decryptPassword, PBKDF_FALLBACK, userSalt, ckSalt, rounds, userIvHex, encryptionKeyBlobHex, rawInStream); } throw e; } } @NonNull private static InputStream attemptEncryptionKeyDecryption(char[] decryptPassword, String algorithm, byte[] userSalt, byte[] ckSalt, int rounds, String userIvHex, String encryptionKeyBlobHex, InputStream rawInStream) throws Exception { InputStream result; Cipher c = Cipher.getInstance(TRANSFORMATION); SecretKey userKey = buildCharArrayKey(algorithm, decryptPassword, userSalt, rounds); byte[] IV = hexToByteArray(userIvHex); IvParameterSpec ivSpec = new IvParameterSpec(IV); c.init(Cipher.DECRYPT_MODE, new SecretKeySpec(userKey.getEncoded(), "AES"), ivSpec); byte[] mkCipher = hexToByteArray(encryptionKeyBlobHex); byte[] mkBlob = c.doFinal(mkCipher); // first, the encryption key IV int offset = 0; int len = mkBlob[offset++]; IV = Arrays.copyOfRange(mkBlob, offset, offset + len); offset += len; // then the encryption key itself len = mkBlob[offset++]; byte[] encryptionKey = Arrays.copyOfRange(mkBlob, offset, offset + len); offset += len; // and finally the encryption key checksum hash len = mkBlob[offset++]; byte[] mkChecksum = Arrays.copyOfRange(mkBlob, offset, offset + len); // now validate the decrypted encryption key against the checksum byte[] calculatedCk = makeKeyChecksum(algorithm, encryptionKey, ckSalt, rounds); if (MessageDigest.isEqual(calculatedCk, mkChecksum)) { ivSpec = new IvParameterSpec(IV); c.init(Cipher.DECRYPT_MODE, new SecretKeySpec(encryptionKey, "AES"), ivSpec); // Only if all of the above worked properly will 'result' be assigned result = new CipherInputStream(rawInStream, c); } else { throw new IOException("Incorrect password"); } return result; } /** * Used for generating random salts or passwords. */ public byte[] randomBytes(int bits) { byte[] array = new byte[bits / 8]; mRng.nextBytes(array); return array; } @NonNull private static String readHeaderLine(@NonNull InputStream in) throws IOException { int c; StringBuilder buffer = new StringBuilder(80); while ((c = in.read()) >= 0) { if (c == '\n') { break; // consume and discard the newlines } buffer.append((char) c); } return buffer.toString(); } private static void readFullyOrThrow(InputStream in, byte[] buffer) throws IOException { int offset = 0; while (offset < buffer.length) { int bytesRead = in.read(buffer, offset, buffer.length - offset); if (bytesRead <= 0) { throw new IOException("Couldn't fully read data"); } offset += bytesRead; } } /** * Generates {@link SecretKey} instance from given parameters and returns it's checksum. *

* Current implementation returns the key in its primary encoding format. * * @param algorithm - key generation algorithm. * @param pwBytes - password. * @param salt - salt. * @param rounds - number of rounds to run in key generation. * @return Hex representation of the generated key, or null if generation failed. */ @NonNull public static byte[] makeKeyChecksum(String algorithm, byte[] pwBytes, byte[] salt, int rounds) throws Exception { char[] mkAsChar = new char[pwBytes.length]; for (int i = 0; i < pwBytes.length; i++) { mkAsChar[i] = (char) pwBytes[i]; } Key checksum = buildCharArrayKey(algorithm, mkAsChar, salt, rounds); return checksum.getEncoded(); } /** * Creates {@link SecretKey} instance from given parameters. * * @param algorithm key generation algorithm. * @param pwArray password. * @param salt salt. * @param rounds number of rounds to run in key generation. * @return {@link SecretKey} instance or null in case of an error. */ @NonNull private static SecretKey buildCharArrayKey(String algorithm, char[] pwArray, byte[] salt, int rounds) throws Exception { // FIXME: 18/2/23 May not work for backup file version 1 SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(algorithm); KeySpec ks = new PBEKeySpec(pwArray, salt, rounds, PBKDF2_KEY_SIZE); return keyFactory.generateSecret(ks); } /** * Creates hex string representation of the byte array. */ public static String byteArrayToHex(byte[] data) { return HexEncoding.encodeToString(data, true); } /** * Creates byte array from it's hex string representation. */ public static byte[] hexToByteArray(String digits) { final int bytes = digits.length() / 2; if (2 * bytes != digits.length()) { throw new IllegalArgumentException("Hex string must have an even number of digits"); } byte[] result = new byte[bytes]; for (int i = 0; i < digits.length(); i += 2) { result[i / 2] = (byte) Integer.parseInt(digits.substring(i, i + 2), 16); } return result; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/backup/adb/BackupCategories.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.backup.adb; import androidx.annotation.IntDef; @IntDef({BackupCategories.CAT_SRC, BackupCategories.CAT_INT_CE, BackupCategories.CAT_INT_DE, BackupCategories.CAT_EXT, BackupCategories.CAT_OBB, BackupCategories.CAT_UNK}) public @interface BackupCategories { int CAT_SRC = 0; int CAT_INT_CE = 1; int CAT_INT_DE = 2; int CAT_EXT = 3; int CAT_OBB = 4; int CAT_UNK = 5; } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/backup/adb/Constants.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.backup.adb; import android.app.backup.BackupAgent; import android.os.Build; /** * Constants taken from {@code android.app.backup.FullBackup}, {@code com.android.server.backup.BackupManagerService}, * {@code com.android.server.backup.utils.PasswordUtils}, {@code com.android.server.backup.BackupPasswordManager} */ final class Constants { /** * Path containing APK files i.e. /apps/{package}/a */ public static final String APK_TREE_TOKEN = "a"; /** * Path containing OBB files i.e. /apps/{package}/obb */ public static final String OBB_TREE_TOKEN = "obb"; /** * Path containing key-value data. Maximum 5 MB. */ public static final String KEY_VALUE_DATA_TOKEN = "k"; /** * Path containing internal data from CE context i.e. /apps/{package}/r excluding files, no_backup, cache, * code_cache and shared_prefs directories. This corresponds to /data/user/{userId}/{package}. */ public static final String ROOT_TREE_TOKEN = "r"; /** * Path containing files data from CE context i.e. /apps/{package}/f. This corresponds to /data/user/{userId}/{package}/files. * But this is not always correct. {@link BackupAgent} is capable of fetching the exact location since it runs in * the app's context. */ public static final String FILES_TREE_TOKEN = "f"; /** * Path containing no_backup data from CE context i.e. /apps/{package}/nb. This path is never used as of Android 13. */ public static final String NO_BACKUP_TREE_TOKEN = "nb"; /** * Path containing databases from CE context i.e. /apps/{package}/db. This corresponds to /data/user/{userId}/{package}/databases. * But this is not always correct. {@link BackupAgent} is capable of fetching the exact location since it runs in * the app's context. */ public static final String DATABASE_TREE_TOKEN = "db"; /** * Path containing shared preferences from CE context i.e. /apps/{package}/db. This corresponds to /data/user/{userId}/{package}/shared_prefs. * But this is not always correct. {@link BackupAgent} is capable of fetching the exact location since it runs in * the app's context. */ public static final String SHAREDPREFS_TREE_TOKEN = "sp"; /** * Path containing caches data from CE context i.e. /apps/{package}/c. This path is never used as of Android 13. */ public static final String CACHE_TREE_TOKEN = "c"; /** * Same as {@link #ROOT_TREE_TOKEN} but for DE context. */ public static final String DEVICE_ROOT_TREE_TOKEN = "d_r"; /** * Same as {@link #FILES_TREE_TOKEN} but for DE context. */ public static final String DEVICE_FILES_TREE_TOKEN = "d_f"; /** * Same as {@link #NO_BACKUP_TREE_TOKEN} but for DE context. This path is never used as of Android 13. */ public static final String DEVICE_NO_BACKUP_TREE_TOKEN = "d_nb"; /** * Same as {@link #DATABASE_TREE_TOKEN} but for DE context. */ public static final String DEVICE_DATABASE_TREE_TOKEN = "d_db"; /** * Same as {@link #SHAREDPREFS_TREE_TOKEN} but for DE context. */ public static final String DEVICE_SHAREDPREFS_TREE_TOKEN = "d_sp"; /** * Same as {@link #CACHE_TREE_TOKEN} but for DE context. This path is never used as of Android 13. */ public static final String DEVICE_CACHE_TREE_TOKEN = "d_c"; /** * Files containing in the external storage directory returned by {@link android.content.Context#getExternalFilesDir(String)}. * {@link BackupAgent} is capable of fetching this directory as it runs in the app's context. Location is /apps/{package}/ef. */ public static final String MANAGED_EXTERNAL_TREE_TOKEN = "ef"; /** * Path containing files from the external storages. This path is never used as of Android 13. */ public static final String SHARED_STORAGE_TOKEN = "shared"; /** * All apps are stored inside this directory in the backup file. The immediate children are the package names. */ public static final String APPS_PREFIX = "apps/"; /** * Shared storages are stored in this directory in the backup file. The immediate children are the volume names. * This is never really used as of Android 13. */ public static final String SHARED_PREFIX = SHARED_STORAGE_TOKEN + "/"; // Name and current contents version of the full-backup manifest file // // Manifest version history: // // 1 : initial release public static final String BACKUP_MANIFEST_FILENAME = "_manifest"; public static final int BACKUP_MANIFEST_VERSION = 1; // External archive format version history: // // 1 : initial release (4+) // 2 : no format change per se; version bump to facilitate PBKDF2 version skew detection (unused) // 3 : introduced "_meta" metadata file; no other format change per se (5+) // 4 : added support for new device-encrypted storage locations (7+) // 5 : added support for key-value packages (8+) public static final int BACKUP_FILE_VERSION = 5; public static final String BACKUP_FILE_HEADER_MAGIC = "ANDROID BACKUP\n"; public static final String BACKUP_METADATA_FILENAME = "_meta"; public static final int BACKUP_METADATA_VERSION = 1; public static final int BACKUP_WIDGET_METADATA_TOKEN = 0x01FFED01; // Configuration of PBKDF2 that we use for generating pw hashes and intermediate keys public static final int PBKDF2_HASH_ROUNDS = 10000; public static final int PBKDF2_KEY_SIZE = 256; // bits public static final int PBKDF2_SALT_SIZE = 512; // bits public static final String ENCRYPTION_ALGORITHM_NAME = "AES-256"; public static final String PBKDF_CURRENT = "PBKDF2WithHmacSHA1"; public static final String PBKDF_FALLBACK = "PBKDF2WithHmacSHA1And8bit"; private Constants() { } public static int getBackupFileVersionFromApi(int api) { if (api <= 0) { api = Build.VERSION.SDK_INT; } if (api >= Build.VERSION_CODES.O) { return BACKUP_FILE_VERSION; } if (api >= Build.VERSION_CODES.N) { return 4; } if (api >= Build.VERSION_CODES.LOLLIPOP) { return 3; } if (api >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { return 1; } throw new IllegalArgumentException("Invalid/unsupported api " + api); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/backup/convert/ConvertUtils.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.backup.convert; import android.annotation.SuppressLint; import android.os.Build; import androidx.annotation.NonNull; import com.android.apksig.ApkVerifier; import com.android.apksig.apk.ApkFormatException; import com.android.apksig.util.DataSource; import com.android.apksig.util.DataSources; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.channels.FileChannel; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.List; import io.github.muntashirakon.AppManager.backup.BackupCryptSetupHelper; import io.github.muntashirakon.AppManager.backup.BackupItems; import io.github.muntashirakon.AppManager.backup.CryptoUtils; import io.github.muntashirakon.AppManager.backup.MetadataManager; import io.github.muntashirakon.AppManager.backup.struct.BackupMetadataV2; import io.github.muntashirakon.AppManager.backup.struct.BackupMetadataV5; import io.github.muntashirakon.AppManager.crypto.Crypto; import io.github.muntashirakon.AppManager.crypto.CryptoException; import io.github.muntashirakon.AppManager.crypto.DummyCrypto; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.self.filecache.FileCache; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.utils.DigestUtils; import io.github.muntashirakon.io.FileSystemManager; import io.github.muntashirakon.io.Path; public final class ConvertUtils { public static final String TAG = ConvertUtils.class.getSimpleName(); @NonNull public static BackupMetadataV5 getV5Metadata(@NonNull BackupMetadataV2 metadataV2, @NonNull BackupItems.BackupItem backupItem) throws CryptoException { // Here we don't care about the crypto we had for metdataV2, because the crypto that the // imported backups use may be different from the one configured for this app String compressionMethod = Prefs.BackupRestore.getCompressionMethod(); String crypto = CryptoUtils.getMode(); BackupCryptSetupHelper cryptoHelper = new BackupCryptSetupHelper(crypto, MetadataManager.getCurrentBackupMetaVersion()); BackupMetadataV5.Info info = new BackupMetadataV5.Info(metadataV2.backupTime, metadataV2.flags, metadataV2.userId, compressionMethod, DigestUtils.SHA_256, crypto, cryptoHelper.getIv(), cryptoHelper.getAes(), cryptoHelper.getKeyIds()); info.setBackupItem(backupItem); BackupMetadataV5.Metadata metadata = new BackupMetadataV5.Metadata(backupItem.getBackupName()); metadata.hasRules = metadataV2.hasRules; metadata.label = metadataV2.label; metadata.packageName = metadataV2.packageName; metadata.versionName = metadataV2.versionName; metadata.versionCode = metadataV2.versionCode; if (metadataV2.dataDirs != null) { metadata.dataDirs = metadataV2.dataDirs.clone(); } metadata.isSystem = metadataV2.isSystem; metadata.isSplitApk = metadataV2.isSplitApk; if (metadataV2.splitConfigs != null) { metadata.splitConfigs = metadataV2.splitConfigs.clone(); } metadata.apkName = metadataV2.apkName; metadata.instructionSet = metadataV2.instructionSet; metadata.keyStore = metadataV2.keyStore; metadata.installer = metadataV2.installer; return new BackupMetadataV5(info, metadata); } @NonNull public static Path[] decryptSourceFiles(@NonNull Path[] files, @NonNull Crypto crypto, @NonNull String cryptoMode, @NonNull BackupItems.BackupItem backupItem) throws IOException { if (crypto instanceof DummyCrypto) { return files; } List newFileList = new ArrayList<>(); // Get desired extension String ext = CryptoUtils.getExtension(cryptoMode); // Create necessary files (1-1 correspondence) for (Path inputFile : files) { Path parent = backupItem.requireUnencryptedBackupPath(); String filename = inputFile.getName(); String outputFilename = filename.substring(0, filename.lastIndexOf(ext)); Path outputPath = parent.createNewFile(outputFilename, null); newFileList.add(outputPath); Log.i(TAG, "Input: %s\nOutput: %s", inputFile, outputPath); } Path[] newFiles = newFileList.toArray(new Path[0]); // Perform actual decryption crypto.decrypt(files, newFiles); return newFiles; } @NonNull public static Converter getConversionUtil(@ImportType int backupType, Path file) { switch (backupType) { case ImportType.OAndBackup: return new OABConverter(file); case ImportType.TitaniumBackup: return new TBConverter(file); case ImportType.SwiftBackup: return new SBConverter(file); default: throw new IllegalArgumentException("Unsupported import type " + backupType); } } @NonNull public static Path[] getRelevantImportFiles(@NonNull Path baseLocation, @ImportType int backupType) { switch (backupType) { case ImportType.OAndBackup: // Package directories return baseLocation.listFiles(Path::isDirectory); case ImportType.TitaniumBackup: // Properties files return baseLocation.listFiles((dir, name) -> name.endsWith(".properties")); case ImportType.SwiftBackup: // XML files return baseLocation.listFiles((dir, name) -> name.endsWith(".xml")); default: throw new IllegalArgumentException("Unsupported import type " + backupType); } } @SuppressLint("SdCardPath") @NonNull static String[] getDataDirs(String packageName, int userHandle, boolean hasInternal, boolean hasExternal, boolean hasObb) { List dataDirs = new ArrayList<>(2); if (hasInternal) { dataDirs.add("/data/user/" + userHandle + "/" + packageName); } if (hasExternal) { dataDirs.add("/storage/emulated/" + userHandle + "/Android/data/" + packageName); } if (hasObb) { dataDirs.add("/storage/emulated/" + userHandle + "/Android/obb/" + packageName); } return dataDirs.toArray(new String[0]); } @NonNull static String[] getChecksumsFromApk(@NonNull Path apkFile, @DigestUtils.Algorithm String algo) throws IOException, ApkFormatException, NoSuchAlgorithmException, CertificateEncodingException { // Since we can't directly work with ProxyFile, we need to cache it and read the signature FileChannel fileChannel; try { fileChannel = apkFile.openFileChannel(FileSystemManager.MODE_READ_ONLY); } catch (IOException e) { File cachedFile = FileCache.getGlobalFileCache().getCachedFile(apkFile); fileChannel = new RandomAccessFile(cachedFile, "r").getChannel(); } DataSource dataSource = DataSources.asDataSource(fileChannel); List checksums = new ArrayList<>(1); ApkVerifier verifier = new ApkVerifier.Builder(dataSource) .setMaxCheckedPlatformVersion(Build.VERSION.SDK_INT) .build(); ApkVerifier.Result apkVerifierResult = verifier.verify(); // Get signer certificates List certificates = apkVerifierResult.getSignerCertificates(); if (certificates != null && !certificates.isEmpty()) { for (X509Certificate certificate : certificates) { checksums.add(DigestUtils.getHexDigest(algo, certificate.getEncoded())); } } return checksums.toArray(new String[0]); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/backup/convert/Converter.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.backup.convert; import io.github.muntashirakon.AppManager.backup.BackupException; public abstract class Converter { public abstract void convert() throws BackupException; public abstract void cleanup(); public abstract String getPackageName(); } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/backup/convert/ImportType.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.backup.convert; public @interface ImportType { int OAndBackup = 0; int TitaniumBackup = 1; int SwiftBackup = 2; } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/backup/convert/OABConverter.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.backup.convert; import static io.github.muntashirakon.AppManager.backup.BackupManager.CERT_PREFIX; import static io.github.muntashirakon.AppManager.backup.BackupManager.getExt; import static io.github.muntashirakon.AppManager.utils.TarUtils.DEFAULT_SPLIT_SIZE; import android.annotation.UserIdInt; import android.os.UserHandleHidden; import android.text.TextUtils; import androidx.annotation.NonNull; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; import org.json.JSONException; import org.json.JSONObject; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import io.github.muntashirakon.AppManager.backup.BackupException; import io.github.muntashirakon.AppManager.backup.BackupItems; import io.github.muntashirakon.AppManager.backup.BackupFlags; import io.github.muntashirakon.AppManager.backup.BackupUtils; import io.github.muntashirakon.AppManager.backup.CryptoUtils; import io.github.muntashirakon.AppManager.backup.MetadataManager; import io.github.muntashirakon.AppManager.backup.struct.BackupMetadataV2; import io.github.muntashirakon.AppManager.backup.struct.BackupMetadataV5; import io.github.muntashirakon.AppManager.crypto.Crypto; import io.github.muntashirakon.AppManager.crypto.CryptoException; import io.github.muntashirakon.AppManager.self.filecache.FileCache; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.utils.ArrayUtils; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.DigestUtils; import io.github.muntashirakon.AppManager.utils.ExUtils; import io.github.muntashirakon.AppManager.utils.TarUtils; import io.github.muntashirakon.io.IoUtils; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.SplitOutputStream; /** * A documentation about OAndBackup is located at * GH#371. */ public class OABConverter extends Converter { public static final String TAG = OABConverter.class.getSimpleName(); public static final String PATH_SUFFIX = "oandbackups"; private static final List SPECIAL_BACKUPS = new ArrayList() { { add("accounts"); add("appwidgets"); add("bluetooth"); add("data.usage.policy"); add("wallpaper"); add("wifi.access.points"); } }; private static final int MODE_UNSET = 0; private static final int MODE_APK = 1; private static final int MODE_DATA = 2; private static final int MODE_BOTH = 3; private static final String EXTERNAL_FILES = "external_files"; private final Path mBackupLocation; private final String mPackageName; @UserIdInt private final int mUserId; private BackupItems.Checksum mChecksum; private BackupMetadataV2 mSourceMetadata; private String mSourceCryptoMode; private Crypto mSourceCrypto; private BackupMetadataV5 mDestMetadata; private BackupItems.BackupItem mBackupItem; /** * @param backupLocation E.g. {@code /sdcard/oandbackups/package.name} */ public OABConverter(@NonNull Path backupLocation) { mBackupLocation = backupLocation; // Last path component is the package name mPackageName = backupLocation.getName(); mUserId = UserHandleHidden.myUserId(); } @Override public void convert() throws BackupException { if (SPECIAL_BACKUPS.contains(mPackageName)) { throw new BackupException("Cannot convert special backup " + mPackageName); } // Source metadata mSourceMetadata = readLogFile(); // Simulate a backup creation try { mBackupItem = BackupItems.createBackupItemGracefully(mUserId, "OAndBackup", mPackageName); } catch (IOException e) { throw new BackupException("Could not get backup files.", e); } boolean backupSuccess = false; try { try { // Destination metadata mDestMetadata = ConvertUtils.getV5Metadata(mSourceMetadata, mBackupItem); } catch (CryptoException e) { throw new BackupException("Failed to get crypto " + mDestMetadata.info.crypto, e); } try { mChecksum = mBackupItem.getChecksum(); } catch (IOException e) { throw new BackupException("Failed to create checksum file.", e); } if (mDestMetadata.info.flags.backupApkFiles()) { backupApkFile(); } if (mDestMetadata.info.flags.backupData()) { backupData(); } // Write modified metadata try { Map filenameChecksumMap = MetadataManager.writeMetadata(mDestMetadata, mBackupItem); for (Map.Entry filenameChecksumPair : filenameChecksumMap.entrySet()) { mChecksum.add(filenameChecksumPair.getKey(), filenameChecksumPair.getValue()); } } catch (IOException e) { throw new BackupException("Failed to write metadata.", e); } // Store checksum for metadata mChecksum.close(); // Encrypt checksum try { mBackupItem.encrypt(new Path[]{mChecksum.getFile()}); } catch (IOException e) { throw new BackupException("Failed to encrypt checksums.txt", e); } // Replace current backup try { mBackupItem.commit(); } catch (IOException e) { throw new BackupException("Could not finalise backup.", e); } backupSuccess = true; } catch (BackupException e) { throw e; } catch (Throwable th) { throw new BackupException("Unknown error occurred.", th); } finally { mBackupItem.cleanup(); if (backupSuccess) { BackupUtils.putBackupToDbAndBroadcast(ContextUtils.getContext(), mDestMetadata); } } } @Override public void cleanup() { mSourceCrypto.close(); mBackupLocation.delete(); } @Override public String getPackageName() { return mPackageName; } private BackupMetadataV2 readLogFile() throws BackupException { try { BackupMetadataV2 metadataV2 = new BackupMetadataV2(); Path logFile = mBackupLocation.findFile(mPackageName + ".log"); String jsonString = logFile.getContentAsString(); if (TextUtils.isEmpty(jsonString)) throw new JSONException("Empty JSON string."); JSONObject jsonObject = new JSONObject(jsonString); metadataV2.label = jsonObject.getString("label"); metadataV2.packageName = jsonObject.getString("packageName"); metadataV2.versionName = jsonObject.getString("versionName"); metadataV2.versionCode = jsonObject.getInt("versionCode"); metadataV2.isSystem = jsonObject.optBoolean("isSystem"); metadataV2.isSplitApk = false; metadataV2.splitConfigs = ArrayUtils.emptyArray(String.class); metadataV2.hasRules = false; metadataV2.backupTime = jsonObject.getLong("lastBackupMillis"); metadataV2.crypto = jsonObject.optBoolean("isEncrypted") ? CryptoUtils.MODE_OPEN_PGP : CryptoUtils.MODE_NO_ENCRYPTION; mSourceCryptoMode = metadataV2.crypto; mSourceCrypto = CryptoUtils.setupCrypto(metadataV2); metadataV2.apkName = new File(jsonObject.getString("sourceDir")).getName(); // Flags metadataV2.flags = new BackupFlags(BackupFlags.BACKUP_MULTIPLE); int backupMode = jsonObject.optInt("backupMode", MODE_UNSET); if (backupMode == MODE_UNSET) { throw new BackupException("Destination doesn't contain any backup."); } if (backupMode == MODE_APK || backupMode == MODE_BOTH) { if (mBackupLocation.hasFile(CryptoUtils.getAppropriateFilename(metadataV2.apkName, mSourceCryptoMode))) { metadataV2.flags.addFlag(BackupFlags.BACKUP_APK_FILES); } else { throw new BackupException("Destination doesn't contain any APK files."); } } if (backupMode == MODE_DATA || backupMode == MODE_BOTH) { boolean hasBackup = false; if (mBackupLocation.hasFile(CryptoUtils.getAppropriateFilename(mPackageName + ".zip", mSourceCryptoMode))) { metadataV2.flags.addFlag(BackupFlags.BACKUP_INT_DATA); hasBackup = true; } if (mBackupLocation.hasFile(EXTERNAL_FILES) && mBackupLocation.findFile(EXTERNAL_FILES).hasFile( CryptoUtils.getAppropriateFilename(mPackageName + ".zip", mSourceCryptoMode))) { metadataV2.flags.addFlag(BackupFlags.BACKUP_EXT_DATA); hasBackup = true; } if (!hasBackup) { throw new BackupException("Destination doesn't contain any data files."); } metadataV2.flags.addFlag(BackupFlags.BACKUP_CACHE); } metadataV2.userId = UserHandleHidden.myUserId(); metadataV2.dataDirs = ConvertUtils.getDataDirs(mPackageName, mUserId, metadataV2.flags .backupInternalData(), metadataV2.flags.backupExternalData(), false); metadataV2.tarType = Prefs.BackupRestore.getCompressionMethod(); metadataV2.keyStore = false; metadataV2.installer = Prefs.Installer.getInstallerPackageName(); metadataV2.version = 2; // Old version is used so that we know that it needs permission fixes return metadataV2; } catch (JSONException | IOException | CryptoException e) { return ExUtils.rethrowAsBackupException("Could not parse JSON file.", e); } } private void backupApkFile() throws BackupException { Path[] baseApkFiles; try { baseApkFiles = new Path[]{mBackupLocation.findFile(CryptoUtils.getAppropriateFilename( mSourceMetadata.apkName, mSourceCryptoMode))}; } catch (FileNotFoundException e) { throw new BackupException("Could not get base.apk file.", e); } // Decrypt APK file if needed try { baseApkFiles = ConvertUtils.decryptSourceFiles(baseApkFiles, mSourceCrypto, mSourceCryptoMode, mBackupItem); } catch (IOException e) { throw new BackupException("Failed to decrypt " + Arrays.toString(baseApkFiles), e); } // baseApkFiles should be a singleton array if (baseApkFiles.length != 1) { throw new BackupException("Incorrect number of APK files: " + baseApkFiles.length); } Path baseApkFile = baseApkFiles[0]; // Get certificate checksums try { String[] checksums = ConvertUtils.getChecksumsFromApk(baseApkFile, mDestMetadata.info.checksumAlgo); for (int i = 0; i < checksums.length; ++i) { mChecksum.add(CERT_PREFIX + i, checksums[i]); } } catch (Exception ignore) { } // Backup APK file String sourceBackupFilePrefix = BackupUtils.getSourceFilePrefix(getExt(mDestMetadata.info.tarType)); Path[] sourceFiles; try { sourceFiles = TarUtils.create(mDestMetadata.info.tarType, baseApkFile, mBackupItem.getUnencryptedBackupPath(), sourceBackupFilePrefix, /* language=regexp */ new String[]{".*\\.apk"}, null, null, false) .toArray(new Path[0]); } catch (Throwable th) { throw new BackupException("APK files backup is requested but no APK files have been backed up.", th); } // Overwrite with the new files try { sourceFiles = mBackupItem.encrypt(sourceFiles); } catch (IOException e) { throw new BackupException("Failed to encrypt " + Arrays.toString(sourceFiles), e); } for (Path file : sourceFiles) { mChecksum.add(file.getName(), DigestUtils.getHexDigest(mDestMetadata.info.checksumAlgo, file)); } } private void backupData() throws BackupException { List dataFiles = new ArrayList<>(2); if (mDestMetadata.info.flags.backupInternalData()) { try { dataFiles.add(mBackupLocation.findFile(CryptoUtils.getAppropriateFilename(mPackageName + ".zip", mSourceCryptoMode))); } catch (FileNotFoundException e) { throw new BackupException("Could not get internal data backup.", e); } } if (mDestMetadata.info.flags.backupExternalData()) { try { dataFiles.add(mBackupLocation.findFile(EXTERNAL_FILES).findFile(CryptoUtils.getAppropriateFilename( mPackageName + ".zip", mSourceCryptoMode))); } catch (FileNotFoundException e) { throw new BackupException("Could not get external data backup.", e); } } String tarType = mDestMetadata.info.tarType; int i = 0; Path[] files; for (Path dataFile : dataFiles) { files = new Path[]{dataFile}; // Decrypt APK file if needed try { files = ConvertUtils.decryptSourceFiles(files, mSourceCrypto, mSourceCryptoMode, mBackupItem); } catch (IOException e) { throw new BackupException("Failed to decrypt " + Arrays.toString(files), e); } // baseApkFiles should be a singleton array if (files.length != 1) { throw new BackupException("Incorrect number of APK files: " + files.length); } String dataBackupFilePrefix = BackupUtils.getDataFilePrefix(i++, getExt(tarType)); try (ZipInputStream zis = new ZipInputStream(new BufferedInputStream(files[0].openInputStream())); SplitOutputStream sos = new SplitOutputStream(mBackupItem.getUnencryptedBackupPath(), dataBackupFilePrefix, DEFAULT_SPLIT_SIZE); BufferedOutputStream bos = new BufferedOutputStream(sos); OutputStream os = TarUtils.createCompressedStream(bos, tarType)) { try (TarArchiveOutputStream tos = new TarArchiveOutputStream(os)) { tos.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); tos.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_POSIX); ZipEntry zipEntry; while ((zipEntry = zis.getNextEntry()) != null) { File tmpFile = null; if (!zipEntry.isDirectory()) { // We need to use a temporary file tmpFile = FileCache.getGlobalFileCache().createCachedFile(files[0].getExtension()); try (OutputStream fos = new FileOutputStream(tmpFile)) { IoUtils.copy(zis, fos); } } String fileName = zipEntry.getName().replaceFirst(Pattern.quote(mPackageName + "/"), ""); if (fileName.isEmpty()) continue; // New tar entry TarArchiveEntry tarArchiveEntry = new TarArchiveEntry(fileName); if (tmpFile != null) { tarArchiveEntry.setSize(tmpFile.length()); } tos.putArchiveEntry(tarArchiveEntry); if (tmpFile != null) { // Copy from the temporary file try (FileInputStream fis = new FileInputStream(tmpFile)) { IoUtils.copy(fis, tos); } finally { FileCache.getGlobalFileCache().delete(tmpFile); } } tos.closeArchiveEntry(); } tos.finish(); } // Encrypt backups Path[] newBackupFiles = mBackupItem.encrypt(sos.getFiles().toArray(new Path[0])); for (Path file : newBackupFiles) { mChecksum.add(file.getName(), DigestUtils.getHexDigest(mDestMetadata.info.checksumAlgo, file)); } } catch (IOException e) { throw new BackupException("Backup failed for " + dataFile, e); } } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/backup/convert/SBConverter.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.backup.convert; import static io.github.muntashirakon.AppManager.backup.BackupManager.CERT_PREFIX; import static io.github.muntashirakon.AppManager.backup.BackupManager.getExt; import static io.github.muntashirakon.AppManager.utils.TarUtils.DEFAULT_SPLIT_SIZE; import android.annotation.SuppressLint; import android.annotation.UserIdInt; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.os.RemoteException; import android.os.UserHandleHidden; import androidx.annotation.NonNull; import androidx.core.content.pm.PackageInfoCompat; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import io.github.muntashirakon.AppManager.backup.BackupException; import io.github.muntashirakon.AppManager.backup.BackupItems; import io.github.muntashirakon.AppManager.backup.BackupFlags; import io.github.muntashirakon.AppManager.backup.BackupUtils; import io.github.muntashirakon.AppManager.backup.CryptoUtils; import io.github.muntashirakon.AppManager.backup.MetadataManager; import io.github.muntashirakon.AppManager.backup.struct.BackupMetadataV2; import io.github.muntashirakon.AppManager.backup.struct.BackupMetadataV5; import io.github.muntashirakon.AppManager.crypto.CryptoException; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.self.filecache.FileCache; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.utils.ArrayUtils; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.DigestUtils; import io.github.muntashirakon.AppManager.utils.FileUtils; import io.github.muntashirakon.AppManager.utils.TarUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.io.IoUtils; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; import io.github.muntashirakon.io.SplitOutputStream; public class SBConverter extends Converter { public static final String TAG = SBConverter.class.getSimpleName(); private final Path mBackupLocation; @UserIdInt private final int mUserId; private final String mPackageName; private final long mBackupTime; private final PackageManager mPm; private final List mFilesToBeDeleted = new ArrayList<>(); private BackupItems.Checksum mChecksum; private BackupMetadataV5 mDestMetadata; private BackupItems.BackupItem mBackupItem; private PackageInfo mPackageInfo; private Path mCachedApk; public SBConverter(@NonNull Path xmlFile) { mBackupLocation = xmlFile.getParent(); mPackageName = Paths.trimPathExtension(xmlFile.getName()); mBackupTime = xmlFile.lastModified(); mUserId = UserHandleHidden.myUserId(); mPm = ContextUtils.getContext().getPackageManager(); mFilesToBeDeleted.add(xmlFile); } @Override public String getPackageName() { return mPackageName; } @Override public void convert() throws BackupException { // Source metadata BackupMetadataV2 sourceMetadata = generateMetadata(); // Simulate a backup creation try { mBackupItem = BackupItems.createBackupItemGracefully(mUserId, "SB", mPackageName); } catch (IOException e) { throw new BackupException("Could not get backup files.", e); } boolean backupSuccess = false; try { try { // Destination metadata mDestMetadata = ConvertUtils.getV5Metadata(sourceMetadata, mBackupItem); } catch (CryptoException e) { throw new BackupException("Failed to get crypto " + mDestMetadata.info.crypto, e); } try { mChecksum = mBackupItem.getChecksum(); } catch (IOException e) { throw new BackupException("Failed to create checksum file.", e); } // Backup icon backupIcon(); if (mDestMetadata.info.flags.backupApkFiles()) { backupApkFile(); } if (mDestMetadata.info.flags.backupData()) { backupData(); } // Write modified metadata try { Map filenameChecksumMap = MetadataManager.writeMetadata(mDestMetadata, mBackupItem); for (Map.Entry filenameChecksumPair : filenameChecksumMap.entrySet()) { mChecksum.add(filenameChecksumPair.getKey(), filenameChecksumPair.getValue()); } } catch (IOException e) { throw new BackupException("Failed to write metadata."); } mChecksum.close(); // Encrypt checksum try { mBackupItem.encrypt(new Path[]{mChecksum.getFile()}); } catch (IOException e) { throw new BackupException("Failed to encrypt checksums.txt"); } // Replace current backup try { mBackupItem.commit(); } catch (IOException e) { throw new BackupException("Could not finalise backup.", e); } backupSuccess = true; } catch (BackupException e) { throw e; } catch (Throwable th) { throw new BackupException("Unknown error occurred.", th); } finally { mBackupItem.cleanup(); mCachedApk.requireParent().delete(); if (backupSuccess) { BackupUtils.putBackupToDbAndBroadcast(ContextUtils.getContext(), mDestMetadata); } } } @Override public void cleanup() { for (Path file : mFilesToBeDeleted) { file.delete(); } } private void backupApkFile() throws BackupException { Path sourceDir = mCachedApk.requireParent(); // Get certificate checksums try { String[] checksums = ConvertUtils.getChecksumsFromApk(mCachedApk, mDestMetadata.info.checksumAlgo); for (int i = 0; i < checksums.length; ++i) { mChecksum.add(CERT_PREFIX + i, checksums[i]); } } catch (Exception ignore) { } // Backup APK files String[] apkFiles = ArrayUtils.appendElement(String.class, mDestMetadata.metadata.splitConfigs, mDestMetadata.metadata.apkName); String sourceBackupFilePrefix = BackupUtils.getSourceFilePrefix(getExt(mDestMetadata.info.tarType)); Path[] sourceFiles; try { // We have to specify APK files because the folder may contain many sourceFiles = TarUtils.create(mDestMetadata.info.tarType, sourceDir, mBackupItem.getUnencryptedBackupPath(), sourceBackupFilePrefix, apkFiles, null, null, false).toArray(new Path[0]); } catch (Throwable th) { throw new BackupException("APK files backup is requested but no APK files have been backed up.", th); } try { sourceFiles = mBackupItem.encrypt(sourceFiles); } catch (IOException e) { throw new BackupException("Failed to encrypt " + Arrays.toString(sourceFiles)); } for (Path file : sourceFiles) { mChecksum.add(file.getName(), DigestUtils.getHexDigest(mDestMetadata.info.checksumAlgo, file)); } } private void backupData() throws BackupException { List dataFiles = new ArrayList<>(3); try { if (mDestMetadata.info.flags.backupInternalData()) { dataFiles.add(getIntDataFile()); } if (mDestMetadata.info.flags.backupExternalData()) { dataFiles.add(getExtDataFile()); } if (mDestMetadata.info.flags.backupMediaObb()) { dataFiles.add(getObbFile()); } } catch (FileNotFoundException e) { throw new BackupException("Could not get data files", e); } String tarType = mDestMetadata.info.tarType; int i = 0; for (Path dataFile : dataFiles) { String dataBackupFilePrefix = BackupUtils.getDataFilePrefix(i++, getExt(tarType)); try (ZipInputStream zis = new ZipInputStream(new BufferedInputStream(dataFile.openInputStream())); SplitOutputStream sos = new SplitOutputStream(mBackupItem.getUnencryptedBackupPath(), dataBackupFilePrefix, DEFAULT_SPLIT_SIZE); BufferedOutputStream bos = new BufferedOutputStream(sos); OutputStream os = TarUtils.createCompressedStream(bos, tarType)) { // TODO: 31/5/21 Check backup format (each zip file has a comment section which can be parsed as JSON) try (TarArchiveOutputStream tos = new TarArchiveOutputStream(os)) { tos.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); tos.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_POSIX); ZipEntry zipEntry; while ((zipEntry = zis.getNextEntry()) != null) { File tmpFile = null; if (!zipEntry.isDirectory()) { // We need to use a temporary file tmpFile = FileCache.getGlobalFileCache().createCachedFile(dataFile.getExtension()); try (OutputStream fos = new FileOutputStream(tmpFile)) { IoUtils.copy(zis, fos); } } String fileName = zipEntry.getName().replaceFirst(Pattern.quote(mPackageName + "/"), ""); if (fileName.isEmpty()) continue; // New tar entry TarArchiveEntry tarArchiveEntry = new TarArchiveEntry(fileName); if (tmpFile != null) { tarArchiveEntry.setSize(tmpFile.length()); } tos.putArchiveEntry(tarArchiveEntry); if (tmpFile != null) { // Copy from the temporary file try (FileInputStream fis = new FileInputStream(tmpFile)) { IoUtils.copy(fis, tos); } finally { FileCache.getGlobalFileCache().delete(tmpFile); } } tos.closeArchiveEntry(); } tos.finish(); } // Encrypt backups Path[] newBackupFiles = mBackupItem.encrypt(sos.getFiles().toArray(new Path[0])); for (Path file : newBackupFiles) { mChecksum.add(file.getName(), DigestUtils.getHexDigest(mDestMetadata.info.checksumAlgo, file)); } } catch (IOException e) { throw new BackupException("Backup failed for " + dataFile, e); } } } @SuppressLint("WrongConstant") @NonNull private BackupMetadataV2 generateMetadata() throws BackupException { BackupMetadataV2 metadataV2 = new BackupMetadataV2(); mCachedApk = FileUtils.getTempPath(mPackageName, "base.apk"); try (InputStream pis = getApkFile().openInputStream()) { try (OutputStream fos = mCachedApk.openOutputStream()) { IoUtils.copy(pis, fos); } mFilesToBeDeleted.add(getApkFile()); } catch (IOException e) { throw new BackupException("Could not cache APK file", e); } String filePath = Objects.requireNonNull(mCachedApk.getFilePath()); PackageInfo packageInfo = mPm.getPackageArchiveInfo(filePath, 0); if (packageInfo == null) { throw new BackupException("Could not fetch package info"); } mPackageInfo = packageInfo; Objects.requireNonNull(mPackageInfo.applicationInfo); mPackageInfo.applicationInfo.publicSourceDir = filePath; mPackageInfo.applicationInfo.sourceDir = filePath; ApplicationInfo applicationInfo = mPackageInfo.applicationInfo; if (!mPackageInfo.packageName.equals(mPackageName)) { throw new BackupException("Package name mismatch: Expected=" + mPackageName + ", Actual=" + mPackageInfo.packageName); } metadataV2.label = applicationInfo.loadLabel(mPm).toString(); metadataV2.packageName = mPackageName; metadataV2.versionName = mPackageInfo.versionName; metadataV2.versionCode = PackageInfoCompat.getLongVersionCode(mPackageInfo); metadataV2.isSystem = false; metadataV2.hasRules = false; metadataV2.backupTime = mBackupTime; metadataV2.crypto = CryptoUtils.MODE_NO_ENCRYPTION; metadataV2.apkName = "base.apk"; // Backup flags BackupFlags flags = new BackupFlags(BackupFlags.BACKUP_APK_FILES); try { mFilesToBeDeleted.add(getObbFile()); flags.addFlag(BackupFlags.BACKUP_EXT_OBB_MEDIA); } catch (FileNotFoundException ignore) { } try { mFilesToBeDeleted.add(getIntDataFile()); flags.addFlag(BackupFlags.BACKUP_INT_DATA); flags.addFlag(BackupFlags.BACKUP_CACHE); } catch (FileNotFoundException ignore) { } try { mFilesToBeDeleted.add(getExtDataFile()); flags.addFlag(BackupFlags.BACKUP_EXT_DATA); flags.addFlag(BackupFlags.BACKUP_CACHE); } catch (FileNotFoundException ignore) { } metadataV2.flags = flags; metadataV2.dataDirs = ConvertUtils.getDataDirs(mPackageName, mUserId, flags.backupInternalData(), flags.backupExternalData(), flags.backupMediaObb()); try { mFilesToBeDeleted.add(getSplitFile()); metadataV2.isSplitApk = true; } catch (FileNotFoundException e) { metadataV2.isSplitApk = false; } try { metadataV2.splitConfigs = cacheAndGetSplitConfigs(); } catch (IOException | RemoteException e) { throw new BackupException("Could not cache splits", e); } metadataV2.userId = mUserId; metadataV2.tarType = Prefs.BackupRestore.getCompressionMethod(); metadataV2.keyStore = false; metadataV2.installer = Prefs.Installer.getInstallerPackageName(); return metadataV2; } @NonNull private Path getApkFile() throws FileNotFoundException { return mBackupLocation.findFile(mPackageName + ".app"); } @NonNull private Path getSplitFile() throws FileNotFoundException { return mBackupLocation.findFile(mPackageName + ".splits"); } @NonNull private Path getObbFile() throws FileNotFoundException { return mBackupLocation.findFile(mPackageName + ".exp"); } @NonNull private Path getIntDataFile() throws FileNotFoundException { return mBackupLocation.findFile(mPackageName + ".dat"); } @NonNull private Path getExtDataFile() throws FileNotFoundException { return mBackupLocation.findFile(mPackageName + ".extdat"); } private String[] cacheAndGetSplitConfigs() throws IOException, RemoteException { List splits = new ArrayList<>(); Path splitFile; try { splitFile = getSplitFile(); } catch (FileNotFoundException e) { return ArrayUtils.emptyArray(String.class); } try (BufferedInputStream bis = new BufferedInputStream(splitFile.openInputStream()); ZipInputStream zis = new ZipInputStream(bis)) { ZipEntry zipEntry; while ((zipEntry = zis.getNextEntry()) != null) { if (zipEntry.isDirectory()) continue; String splitName = FileUtils.getFilenameFromZipEntry(zipEntry); splits.add(splitName); Path file = mCachedApk.requireParent().findOrCreateFile(splitName, null); try (OutputStream fos = file.openOutputStream()) { IoUtils.copy(zis, fos); } catch (IOException e) { file.delete(); throw e; } } } return splits.toArray(new String[0]); } private void backupIcon() { try { Path iconFile = mBackupItem.getIconFile(); try (OutputStream outputStream = iconFile.openOutputStream()) { Bitmap bitmap = UIUtils.getBitmapFromDrawable(mPackageInfo.applicationInfo.loadIcon(mPm)); bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream); outputStream.flush(); } } catch (Throwable th) { Log.w(TAG, "Could not back up icon.", th); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/backup/convert/TBConverter.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.backup.convert; import static io.github.muntashirakon.AppManager.backup.BackupManager.CERT_PREFIX; import static io.github.muntashirakon.AppManager.backup.BackupManager.getExt; import static io.github.muntashirakon.AppManager.utils.TarUtils.DEFAULT_SPLIT_SIZE; import static io.github.muntashirakon.AppManager.utils.TarUtils.TAR_BZIP2; import static io.github.muntashirakon.AppManager.utils.TarUtils.TAR_GZIP; import static io.github.muntashirakon.AppManager.utils.TarUtils.TAR_ZSTD; import android.annotation.UserIdInt; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.UserHandleHidden; import android.util.Base64; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; import org.apache.commons.compress.compressors.CompressorInputStream; import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream; import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.regex.Pattern; import io.github.muntashirakon.AppManager.backup.BackupException; import io.github.muntashirakon.AppManager.backup.BackupItems; import io.github.muntashirakon.AppManager.backup.BackupFlags; import io.github.muntashirakon.AppManager.backup.BackupUtils; import io.github.muntashirakon.AppManager.backup.CryptoUtils; import io.github.muntashirakon.AppManager.backup.MetadataManager; import io.github.muntashirakon.AppManager.backup.struct.BackupMetadataV2; import io.github.muntashirakon.AppManager.backup.struct.BackupMetadataV5; import io.github.muntashirakon.AppManager.crypto.CryptoException; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.utils.ArrayUtils; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.DigestUtils; import io.github.muntashirakon.AppManager.utils.FileUtils; import io.github.muntashirakon.AppManager.utils.TarUtils; import io.github.muntashirakon.io.IoUtils; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; import io.github.muntashirakon.io.SplitOutputStream; public class TBConverter extends Converter { public static final String TAG = TBConverter.class.getSimpleName(); public static final String PATH_SUFFIX = "TitaniumBackup"; private static final String INTERNAL_PREFIX = "data/data/"; private static final String EXTERNAL_PREFIX = "data/data/.external."; private final Path mBackupLocation; @UserIdInt private final int mUserId; private final Path mPropFile; private final String mPackageName; private final long mBackupTime; private final List mFilesToBeDeleted = new ArrayList<>(); private BackupItems.Checksum mChecksum; private BackupMetadataV2 mSourceMetadata; private BackupMetadataV5 mDestMetadata; private BackupItems.BackupItem mBackupItem; @Nullable private Bitmap mIcon; /** * A documentation about Titanium Backup is located at * GH#371. * * @param propFile Location to the properties file e.g. {@code /sdcard/TitaniumBackup/package.name-YYYYMMDD-HHMMSS.properties} */ public TBConverter(@NonNull Path propFile) { mPropFile = propFile; mBackupLocation = propFile.getParent(); mUserId = UserHandleHidden.myUserId(); String dirtyName = propFile.getName(); int idx = dirtyName.indexOf('-'); if (idx == -1) mPackageName = null; else mPackageName = dirtyName.substring(0, idx); mBackupTime = propFile.lastModified(); // TODO: Grab from the file name mFilesToBeDeleted.add(propFile); } @Override public void convert() throws BackupException { if (mPackageName == null) { throw new BackupException("Could not read package name."); } // Source metadata mSourceMetadata = readPropFile(); // Simulate a backup creation try { mBackupItem = BackupItems.createBackupItemGracefully(mUserId, "TB", mPackageName); } catch (IOException e) { throw new BackupException("Could not get backup files", e); } boolean backupSuccess = false; try { try { // Destination metadata mDestMetadata = ConvertUtils.getV5Metadata(mSourceMetadata, mBackupItem); // Destination APK will be renamed mDestMetadata.metadata.apkName = "base.apk"; } catch (CryptoException e) { throw new BackupException("Failed to get crypto " + mDestMetadata.info.crypto, e); } try { mChecksum = mBackupItem.getChecksum(); } catch (IOException e) { throw new BackupException("Failed to create checksum file.", e); } // Backup icon backupIcon(); if (mDestMetadata.info.flags.backupApkFiles()) { backupApkFile(); } if (mDestMetadata.info.flags.backupData()) { backupData(); } // Write modified metadata try { Map filenameChecksumMap = MetadataManager.writeMetadata(mDestMetadata, mBackupItem); for (Map.Entry filenameChecksumPair : filenameChecksumMap.entrySet()) { mChecksum.add(filenameChecksumPair.getKey(), filenameChecksumPair.getValue()); } } catch (IOException e) { throw new BackupException("Failed to write metadata.", e); } mChecksum.close(); // Encrypt checksum try { mBackupItem.encrypt(new Path[]{mChecksum.getFile()}); } catch (IOException e) { throw new BackupException("Failed to encrypt checksums.txt"); } // Replace current backup: // There's hardly any chance of getting a false here but checks are done anyway. try { mBackupItem.commit(); } catch (Exception e) { throw new BackupException("Could not finalise backup.", e); } backupSuccess = true; } catch (BackupException e) { throw e; } catch (Throwable th) { throw new BackupException("Unknown error occurred.", th); } finally { mBackupItem.cleanup(); if (backupSuccess) { BackupUtils.putBackupToDbAndBroadcast(ContextUtils.getContext(), mDestMetadata); } } } @Override public String getPackageName() { return mPackageName; } @Override public void cleanup() { for (Path file : mFilesToBeDeleted) { file.delete(); } } private void backupApkFile() throws BackupException { // Decompress APK file Path baseApkFile = FileUtils.getTempPath(mPackageName, mDestMetadata.metadata.apkName); try (InputStream pis = getApkFile(mSourceMetadata.apkName, mSourceMetadata.tarType).openInputStream(); BufferedInputStream bis = new BufferedInputStream(pis)) { CompressorInputStream is; if (TAR_GZIP.equals(mSourceMetadata.tarType)) { is = new GzipCompressorInputStream(bis, true); } else if (TAR_BZIP2.equals(mSourceMetadata.tarType)) { is = new BZip2CompressorInputStream(bis, true); } else { baseApkFile.requireParent().delete(); throw new BackupException("Invalid source compression type: " + mSourceMetadata.tarType); } try (OutputStream fos = baseApkFile.openOutputStream()) { // The whole file is the APK IoUtils.copy(is, fos); } finally { is.close(); } } catch (IOException e) { baseApkFile.requireParent().delete(); throw new BackupException("Couldn't decompress " + mSourceMetadata.apkName, e); } // Get certificate checksums try { String[] checksums = ConvertUtils.getChecksumsFromApk(baseApkFile, mDestMetadata.info.checksumAlgo); for (int i = 0; i < checksums.length; ++i) { mChecksum.add(CERT_PREFIX + i, checksums[i]); } } catch (Exception ignore) { } // Backup APK file String sourceBackupFilePrefix = BackupUtils.getSourceFilePrefix(getExt(mDestMetadata.info.tarType)); Path[] sourceFiles; try { sourceFiles = TarUtils.create(mDestMetadata.info.tarType, baseApkFile, mBackupItem.getUnencryptedBackupPath(), sourceBackupFilePrefix, /* language=regexp */new String[]{".*\\.apk"}, null, null, false) .toArray(new Path[0]); } catch (Throwable th) { throw new BackupException("APK files backup is requested but no APK files have been backed up.", th); } finally { baseApkFile.requireParent().delete(); } // Overwrite with the new files try { sourceFiles = mBackupItem.encrypt(sourceFiles); } catch (IOException e) { throw new BackupException("Failed to encrypt " + Arrays.toString(sourceFiles)); } for (Path file : sourceFiles) { mChecksum.add(file.getName(), DigestUtils.getHexDigest(mDestMetadata.info.checksumAlgo, file)); } } private void backupData() throws BackupException { Path dataFile; try { dataFile = getDataFile(Paths.trimPathExtension(mPropFile.getName()), mSourceMetadata.tarType); } catch (FileNotFoundException e) { throw new BackupException("Could not get data file", e); } String tarType = mDestMetadata.info.tarType; int i = 0; String intBackupFilePrefix = null; String extBackupFilePrefix = null; if (mDestMetadata.info.flags.backupInternalData()) { intBackupFilePrefix = BackupUtils.getDataFilePrefix(i++, getExt(tarType)); } if (mDestMetadata.info.flags.backupExternalData()) { extBackupFilePrefix = BackupUtils.getDataFilePrefix(i, getExt(tarType)); } try (BufferedInputStream bis = new BufferedInputStream(dataFile.openInputStream())) { CompressorInputStream cis; if (TAR_GZIP.equals(mSourceMetadata.tarType)) { cis = new GzipCompressorInputStream(bis); } else if (TAR_BZIP2.equals(mSourceMetadata.tarType)) { cis = new BZip2CompressorInputStream(bis); } else { throw new BackupException("Invalid compression type: " + tarType); } TarArchiveInputStream tis = new TarArchiveInputStream(cis); SplitOutputStream intSos = null, extSos = null; TarArchiveOutputStream intTos = null, extTos = null; if (intBackupFilePrefix != null) { intSos = new SplitOutputStream(mBackupItem.getUnencryptedBackupPath(), intBackupFilePrefix, DEFAULT_SPLIT_SIZE); BufferedOutputStream bos = new BufferedOutputStream(intSos); OutputStream cos = TarUtils.createCompressedStream(bos, tarType); intTos = new TarArchiveOutputStream(cos); intTos.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); intTos.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_POSIX); } if (extBackupFilePrefix != null) { extSos = new SplitOutputStream(mBackupItem.getUnencryptedBackupPath(), extBackupFilePrefix, DEFAULT_SPLIT_SIZE); BufferedOutputStream bos = new BufferedOutputStream(extSos); OutputStream cos = TarUtils.createCompressedStream(bos, tarType); extTos = new TarArchiveOutputStream(cos); extTos.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); extTos.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_POSIX); } // Add files TarArchiveEntry inTarEntry; while ((inTarEntry = tis.getNextEntry()) != null) { String fileName = inTarEntry.getName(); boolean isExternal = fileName.startsWith(EXTERNAL_PREFIX); // Get new file name fileName = fileName.replaceFirst((isExternal ? EXTERNAL_PREFIX : INTERNAL_PREFIX) + Pattern.quote(mPackageName + "/") + "\\./", ""); if (fileName.isEmpty()) continue; // New tar entry TarArchiveEntry outTarEntry = new TarArchiveEntry(fileName); outTarEntry.setMode(inTarEntry.getMode()); outTarEntry.setUserId(inTarEntry.getUserId()); outTarEntry.setGroupId(inTarEntry.getGroupId()); outTarEntry.setSize(inTarEntry.getSize()); if (isExternal) { if (extTos != null) { extTos.putArchiveEntry(outTarEntry); } } else { if (intTos != null) { intTos.putArchiveEntry(outTarEntry); } } if (!inTarEntry.isDirectory() && !inTarEntry.isSymbolicLink()) { if (isExternal) { if (extTos != null) { IoUtils.copy(tis, extTos); } } else { if (intTos != null) { IoUtils.copy(tis, intTos); } } } if (isExternal) { if (extTos != null) { extTos.closeArchiveEntry(); } } else { if (intTos != null) { intTos.closeArchiveEntry(); } } } // Archiving finished try { tis.close(); } catch (Exception ignore) { } if (intTos != null) { intTos.finish(); try { intTos.close(); } catch (Exception ignore) { } } if (extTos != null) { extTos.finish(); try { extTos.close(); } catch (Exception ignore) { } } // Encrypt created backups and generate checksum if (intSos != null) { // Encrypt backups Path[] newBackupFiles = mBackupItem.encrypt(intSos.getFiles().toArray(new Path[0])); for (Path file : newBackupFiles) { mChecksum.add(file.getName(), DigestUtils.getHexDigest(mDestMetadata.info.checksumAlgo, file)); } } if (extSos != null) { // Encrypt backups Path[] newBackupFiles = mBackupItem.encrypt(extSos.getFiles().toArray(new Path[0])); for (Path file : newBackupFiles) { mChecksum.add(file.getName(), DigestUtils.getHexDigest(mDestMetadata.info.checksumAlgo, file)); } } } catch (IOException e) { throw new BackupException("Could not backup data", e); } } private BackupMetadataV2 readPropFile() throws BackupException { try (InputStream is = mPropFile.openInputStream()) { BackupMetadataV2 metadataV2 = new BackupMetadataV2(); Properties prop = new Properties(); prop.load(is); metadataV2.label = prop.getProperty("app_label"); metadataV2.packageName = mPackageName; metadataV2.versionName = prop.getProperty("app_version_name"); metadataV2.versionCode = Integer.parseInt(prop.getProperty("app_version_code")); metadataV2.isSystem = "1".equals(prop.getProperty("app_is_system")); metadataV2.isSplitApk = false; metadataV2.splitConfigs = ArrayUtils.emptyArray(String.class); metadataV2.hasRules = false; metadataV2.backupTime = mBackupTime; metadataV2.crypto = CryptoUtils.MODE_NO_ENCRYPTION; // We only support no encryption mode for TB backups metadataV2.apkName = mPackageName + "-" + prop.getProperty("app_apk_md5") + ".apk"; metadataV2.userId = UserHandleHidden.myUserId(); // Compression type String compressionType = prop.getProperty("app_apk_codec"); if ("GZIP".equals(compressionType)) { metadataV2.tarType = TAR_GZIP; } else if ("BZIP2".equals(compressionType)) { metadataV2.tarType = TAR_BZIP2; } else throw new BackupException("Unsupported compression type: " + compressionType); // Flags metadataV2.flags = new BackupFlags(BackupFlags.BACKUP_MULTIPLE); try { mFilesToBeDeleted.add(getDataFile(Paths.trimPathExtension(mPropFile.getName()), metadataV2.tarType)); // No error = data file exists metadataV2.flags.addFlag(BackupFlags.BACKUP_INT_DATA); if ("1".equals(prop.getProperty("has_external_data"))) { metadataV2.flags.addFlag(BackupFlags.BACKUP_EXT_DATA); } metadataV2.flags.addFlag(BackupFlags.BACKUP_CACHE); } catch (FileNotFoundException ignore) { } try { mFilesToBeDeleted.add(getApkFile(metadataV2.apkName, metadataV2.tarType)); // No error = APK file exists metadataV2.flags.addFlag(BackupFlags.BACKUP_APK_FILES); } catch (FileNotFoundException ignore) { } metadataV2.dataDirs = ConvertUtils.getDataDirs(mPackageName, mUserId, metadataV2.flags .backupInternalData(), metadataV2.flags.backupExternalData(), false); metadataV2.keyStore = false; metadataV2.installer = Prefs.Installer.getInstallerPackageName(); String base64Icon = prop.getProperty("app_gui_icon"); if (base64Icon != null) { byte[] decodedBytes = Base64.decode(base64Icon, 0); mIcon = BitmapFactory.decodeByteArray(decodedBytes, 0, decodedBytes.length); } return metadataV2; } catch (IOException e) { throw new BackupException("Could not read the prop file", e); } } @NonNull private Path getDataFile(String filePrefix, @TarUtils.TarType String tarType) throws FileNotFoundException { String filename = filePrefix + ".tar"; if (TAR_BZIP2.equals(tarType)) filename += ".bz2"; else if (TAR_ZSTD.equals(tarType)) filename += ".zst"; else filename += ".gz"; return mBackupLocation.findFile(filename); } @NonNull private Path getApkFile(String apkName, @TarUtils.TarType String tarType) throws FileNotFoundException { if (TAR_BZIP2.equals(tarType)) apkName += ".bz2"; else if (TAR_ZSTD.equals(tarType)) apkName += ".zst"; else apkName += ".gz"; return mBackupLocation.findFile(apkName); } private void backupIcon() { if (mIcon == null) return; try { Path iconFile = mBackupItem.getIconFile(); try (OutputStream outputStream = iconFile.openOutputStream()) { mIcon.compress(Bitmap.CompressFormat.PNG, 100, outputStream); outputStream.flush(); } } catch (IOException e) { Log.w(TAG, "Could not back up icon."); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/backup/dialog/BackupFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.backup.dialog; import android.content.Context; import android.os.Bundle; import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.util.Set; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.backup.BackupFlags; import io.github.muntashirakon.AppManager.batchops.BatchOpsManager; import io.github.muntashirakon.AppManager.utils.DateUtils; import io.github.muntashirakon.dialog.TextInputDialogBuilder; import io.github.muntashirakon.widget.MaterialAlertView; public class BackupFragment extends Fragment { public static final String ARG_ALLOW_CUSTOM_USERS = "allow_custom"; @NonNull public static BackupFragment getInstance(boolean allowCustomUsers) { BackupFragment fragment = new BackupFragment(); Bundle args = new Bundle(); args.putBoolean(ARG_ALLOW_CUSTOM_USERS, allowCustomUsers); fragment.setArguments(args); return fragment; } private BackupRestoreDialogViewModel mViewModel; private Context mContext; @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_dialog_backup, container, false); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { mViewModel = new ViewModelProvider(requireParentFragment()).get(BackupRestoreDialogViewModel.class); mContext = requireContext(); boolean allowCustomUsers = requireArguments().getBoolean(ARG_ALLOW_CUSTOM_USERS); MaterialAlertView messageView = view.findViewById(R.id.message); RecyclerView recyclerView = view.findViewById(android.R.id.list); recyclerView.setLayoutManager(new LinearLayoutManager(mContext, LinearLayoutManager.VERTICAL, false)); int supportedFlags = BackupFlags.getSupportedBackupFlags(); // Remove unsupported flags supportedFlags &= ~BackupFlags.BACKUP_NO_SIGNATURE_CHECK; if (!allowCustomUsers) { supportedFlags &= ~BackupFlags.BACKUP_CUSTOM_USERS; } FlagsAdapter adapter = new FlagsAdapter(mContext, BackupFlags.fromPref().getFlags(), supportedFlags); recyclerView.setAdapter(adapter); Set uninstalledApps = mViewModel.getUninstalledApps(); if (!uninstalledApps.isEmpty()) { SpannableStringBuilder sb = new SpannableStringBuilder(getString(R.string.backup_apps_cannot_be_backed_up)); for (CharSequence appLabel : uninstalledApps) { sb.append("\n● ").append(appLabel); } messageView.setText(sb); messageView.setVisibility(View.VISIBLE); } view.findViewById(R.id.action_backup).setOnClickListener(v -> { BackupFlags newFlags = new BackupFlags(adapter.getSelectedFlags()); handleBackup(newFlags); }); } private void handleBackup(@NonNull BackupFlags flags) { BackupRestoreDialogViewModel.OperationInfo operationInfo = new BackupRestoreDialogViewModel.OperationInfo(); operationInfo.mode = BackupRestoreDialogFragment.MODE_BACKUP; operationInfo.flags = flags.getFlags(); operationInfo.op = BatchOpsManager.OP_BACKUP; if (flags.backupMultiple()) { // Multiple backup is requested, no need to warn users about backups since the // user has a choice between overwriting the existing backup or create a new one // TODO(18/9/20): Add overwrite option new TextInputDialogBuilder(mContext, R.string.input_backup_name) .setTitle(R.string.backup) .setHelperText(R.string.input_backup_name_description) .setPositiveButton(R.string.ok, (dialog, which, input, isChecked) -> { String backupName; if (TextUtils.isEmpty(input)) { backupName = DateUtils.formatMediumDateTime(mContext, System.currentTimeMillis()); } else { backupName = input.toString(); } operationInfo.backupNames = new String[]{backupName}; mViewModel.prepareForOperation(operationInfo); }) .show(); } else { // Base backup requested int baseBackupCount = mViewModel.getBackupInfoList().size() - mViewModel.getAppsWithoutBackups().size(); if (baseBackupCount > 0) { // One or more app has backups, warn users new MaterialAlertDialogBuilder(mContext) .setTitle(R.string.backup) .setMessage(getResources().getQuantityString(R.plurals.backup_exists_are_you_sure, baseBackupCount)) .setPositiveButton(R.string.yes, (dialog, which) -> mViewModel.prepareForOperation(operationInfo)) .setNegativeButton(R.string.no, null) .show(); } else { // No need to warn users, proceed to back up mViewModel.prepareForOperation(operationInfo); } } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/backup/dialog/BackupInfo.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.backup.dialog; import androidx.annotation.NonNull; import androidx.collection.ArraySet; import java.util.Collections; import java.util.List; import io.github.muntashirakon.AppManager.backup.struct.BackupMetadataV5; public class BackupInfo { @NonNull public final String packageName; @NonNull public final ArraySet userIds = new ArraySet<>(); private CharSequence mAppLabel; @NonNull private List mBackupMetadataList = Collections.emptyList(); private boolean mInstalled; private boolean mHasBaseBackup; BackupInfo(@NonNull String packageName, int userId) { this.packageName = packageName; this.userIds.add(userId); mAppLabel = packageName; } @NonNull public CharSequence getAppLabel() { return mAppLabel; } public void setAppLabel(@NonNull CharSequence appLabel) { mAppLabel = appLabel; } @NonNull public List getBackupMetadataList() { return mBackupMetadataList; } public void setBackupMetadataList(@NonNull List backupMetadataList) { mBackupMetadataList = backupMetadataList; } public boolean hasBaseBackup() { return mHasBaseBackup; } public void setHasBaseBackup(boolean hasBaseBackup) { mHasBaseBackup = hasBaseBackup; } public boolean isInstalled() { return mInstalled; } public void setInstalled(boolean installed) { mInstalled = installed; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/backup/dialog/BackupInfoState.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.backup.dialog; import androidx.annotation.IntDef; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @IntDef({ BackupInfoState.NONE, BackupInfoState.BACKUP_MULTIPLE, BackupInfoState.RESTORE_MULTIPLE, BackupInfoState.BOTH_MULTIPLE, BackupInfoState.BACKUP_SINGLE, BackupInfoState.RESTORE_SINGLE, BackupInfoState.BOTH_SINGLE, }) @Retention(RetentionPolicy.SOURCE) public @interface BackupInfoState { /** * None of the selected apps have backups nor any of them is installed. */ int NONE = 0; /** * None of the selected apps have backups but some of them are installed. */ int BACKUP_MULTIPLE = 1; /** * None of the apps are installed but a few have (base) backups. */ int RESTORE_MULTIPLE = 2; /** * Some apps are installed and some apps have (base) backups. */ int BOTH_MULTIPLE = 3; /** * The app is installed but has no backups */ int BACKUP_SINGLE = 4; /** * The apps is uninstalled but has backups */ int RESTORE_SINGLE = 5; /** * Some apps are installed and some apps have (base) backups. */ int BOTH_SINGLE = 6; } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/backup/dialog/BackupRestoreDialogFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.backup.dialog; import android.annotation.UserIdInt; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.TypedArray; import android.os.Bundle; import android.os.UserHandleHidden; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.lifecycle.ViewModelProvider; import androidx.viewpager2.adapter.FragmentStateAdapter; import androidx.viewpager2.widget.ViewPager2; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayoutMediator; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.batchops.BatchOpsManager; import io.github.muntashirakon.AppManager.batchops.BatchOpsService; import io.github.muntashirakon.AppManager.batchops.BatchQueueItem; import io.github.muntashirakon.AppManager.batchops.struct.BatchBackupOptions; import io.github.muntashirakon.AppManager.types.UserPackagePair; import io.github.muntashirakon.AppManager.users.UserInfo; import io.github.muntashirakon.AppManager.utils.ArrayUtils; import io.github.muntashirakon.AppManager.utils.StoragePermission; import io.github.muntashirakon.dialog.BottomSheetBehavior; import io.github.muntashirakon.dialog.CapsuleBottomSheetDialogFragment; import io.github.muntashirakon.dialog.DialogTitleBuilder; import io.github.muntashirakon.dialog.SearchableMultiChoiceDialogBuilder; public class BackupRestoreDialogFragment extends CapsuleBottomSheetDialogFragment { public static final String TAG = BackupRestoreDialogFragment.class.getSimpleName(); private static final String ARG_PACKAGE_PAIRS = "pkg_pairs"; private static final String ARG_CUSTOM_MODE = "custom_mode"; private static final String ARG_PREFERRED_USER_FOR_RESTORE = "pref_user_restore"; @NonNull public static BackupRestoreDialogFragment getInstance(@NonNull List userPackagePairs) { BackupRestoreDialogFragment fragment = new BackupRestoreDialogFragment(); Bundle args = new Bundle(); args.putParcelableArrayList(ARG_PACKAGE_PAIRS, new ArrayList<>(userPackagePairs)); fragment.setArguments(args); return fragment; } @NonNull public static BackupRestoreDialogFragment getInstanceWithPref(@NonNull List userPackagePairs, @UserIdInt int preferredUserForRestore) { BackupRestoreDialogFragment fragment = new BackupRestoreDialogFragment(); Bundle args = new Bundle(); args.putParcelableArrayList(ARG_PACKAGE_PAIRS, new ArrayList<>(userPackagePairs)); args.putInt(ARG_PREFERRED_USER_FOR_RESTORE, preferredUserForRestore); fragment.setArguments(args); return fragment; } @NonNull public static BackupRestoreDialogFragment getInstance(@NonNull List userPackagePairs, @ActionMode int mode) { BackupRestoreDialogFragment fragment = new BackupRestoreDialogFragment(); Bundle args = new Bundle(); args.putParcelableArrayList(ARG_PACKAGE_PAIRS, new ArrayList<>(userPackagePairs)); args.putInt(ARG_CUSTOM_MODE, mode); fragment.setArguments(args); return fragment; } @IntDef(flag = true, value = { MODE_BACKUP, MODE_RESTORE, MODE_DELETE }) @Retention(RetentionPolicy.SOURCE) public @interface ActionMode { } public static final int MODE_BACKUP = 1; public static final int MODE_RESTORE = 1 << 1; public static final int MODE_DELETE = 1 << 2; public interface ActionCompleteInterface { void onActionComplete(@ActionMode int mode, @NonNull String[] failedPackages); } public interface ActionBeginInterface { void onActionBegin(@ActionMode int mode); } @Nullable private ActionCompleteInterface mActionCompleteInterface; @Nullable private ActionBeginInterface mActionBeginInterface; @ActionMode private int mMode = MODE_BACKUP; private FragmentActivity mActivity; private BackupRestoreDialogViewModel mViewModel; private Fragment[] mTabFragments; private TypedArray mTabTitles; private DialogTitleBuilder mDialogTitleBuilder; private int mCustomModes; private final StoragePermission mStoragePermission = StoragePermission.init(this); private final BroadcastReceiver mBatchOpsBroadCastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (mActionCompleteInterface != null) { ArrayList failedPackages = intent.getStringArrayListExtra(BatchOpsService.EXTRA_FAILED_PKG); mActionCompleteInterface.onActionComplete(mMode, failedPackages != null ? failedPackages.toArray(new String[0]) : new String[0]); } mActivity.unregisterReceiver(mBatchOpsBroadCastReceiver); } }; public void setOnActionCompleteListener(@NonNull ActionCompleteInterface actionCompleteInterface) { mActionCompleteInterface = actionCompleteInterface; } public void setOnActionBeginListener(@NonNull ActionBeginInterface actionBeginInterface) { mActionBeginInterface = actionBeginInterface; } @NonNull @Override public View initRootView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.dialog_backup_restore, container, false); } @Override public boolean displayLoaderByDefault() { return true; } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); mActivity = requireActivity(); mStoragePermission.request(); } @Override public void onBodyInitialized(@NonNull View bodyView, @Nullable Bundle savedInstanceState) { mViewModel = new ViewModelProvider(this).get(BackupRestoreDialogViewModel.class); mActivity = requireActivity(); Bundle args = requireArguments(); List targetPackages = args.getParcelableArrayList(ARG_PACKAGE_PAIRS); mCustomModes = args.getInt(ARG_CUSTOM_MODE, MODE_BACKUP | MODE_RESTORE | MODE_DELETE); int preferredUserForRestore = args.getInt(ARG_PREFERRED_USER_FOR_RESTORE, -1); if (preferredUserForRestore >= 0) { mViewModel.setPreferredUserForRestore(preferredUserForRestore); } mDialogTitleBuilder = new DialogTitleBuilder(requireContext()) .setTitle(R.string.backup_restore) .setStartIcon(R.drawable.ic_backup_restore); setHeader(mDialogTitleBuilder.build()); mViewModel.getBackupInfoStateLiveData().observe(this, this::loadBody); mViewModel.getBackupOperationLiveData().observe(this, this::startOperation); mViewModel.getUserSelectionLiveData().observe(this, this::handleCustomUsers); mViewModel.processPackages(targetPackages); } private void loadBody(@BackupInfoState int state) { state = getRealState(state); Log.d(TAG, "Backup dialog state: " + state); switch (state) { default: case BackupInfoState.NONE: showBackupOptionsUnavailable(); break; case BackupInfoState.BACKUP_MULTIPLE: loadMultipleBackupFragment(); break; case BackupInfoState.RESTORE_MULTIPLE: loadMultipleRestoreFragment(); break; case BackupInfoState.BOTH_MULTIPLE: loadMultipleBackupRestoreViewPager(); break; case BackupInfoState.BACKUP_SINGLE: loadSingleBackupFragment(); break; case BackupInfoState.RESTORE_SINGLE: loadSingleRestoreFragment(); break; case BackupInfoState.BOTH_SINGLE: loadSingleBackupRestoreViewPager(); break; } } @BackupInfoState private int getRealState(@BackupInfoState int state) { boolean singleMode = state == BackupInfoState.BACKUP_SINGLE || state == BackupInfoState.RESTORE_SINGLE || state == BackupInfoState.BOTH_SINGLE; switch (state) { default: case BackupInfoState.NONE: return state; case BackupInfoState.BACKUP_MULTIPLE: case BackupInfoState.BACKUP_SINGLE: if ((mCustomModes & MODE_BACKUP) == 0) { return BackupInfoState.NONE; } break; case BackupInfoState.BOTH_MULTIPLE: case BackupInfoState.BOTH_SINGLE: boolean canBackup = (mCustomModes & MODE_BACKUP) != 0; boolean canRestore = (mCustomModes & MODE_RESTORE) != 0; if (!canBackup && !canRestore) { return BackupInfoState.NONE; } if (!canRestore) { return singleMode ? BackupInfoState.BACKUP_SINGLE : BackupInfoState.BACKUP_MULTIPLE; } if (!canBackup) { return singleMode ? BackupInfoState.RESTORE_SINGLE : BackupInfoState.RESTORE_MULTIPLE; } break; case BackupInfoState.RESTORE_MULTIPLE: case BackupInfoState.RESTORE_SINGLE: if ((mCustomModes & MODE_RESTORE) == 0) { return BackupInfoState.NONE; } break; } return state; } private void showBackupOptionsUnavailable() { getBody().findViewById(R.id.message).setVisibility(View.VISIBLE); getBody().findViewById(R.id.fragment_container_view_tag).setVisibility(View.GONE); finishLoading(); } public BackupFragment getBackupFragment() { return BackupFragment.getInstance(mViewModel.allowCustomUsersInBackup()); } private void loadMultipleBackupFragment() { mDialogTitleBuilder.setTitle(R.string.backup); setHeader(mDialogTitleBuilder.build()); finishLoading(); getChildFragmentManager() .beginTransaction() .replace(R.id.fragment_container_view_tag, getBackupFragment()) .commit(); } private void loadMultipleRestoreFragment() { mDialogTitleBuilder.setTitle(R.string.restore); updateMultipleRestoreHeader(); finishLoading(); getChildFragmentManager() .beginTransaction() .replace(R.id.fragment_container_view_tag, RestoreMultipleFragment.getInstance()) .commit(); } private void loadMultipleBackupRestoreViewPager() { updateMultipleRestoreHeader(); mTabTitles = getResources().obtainTypedArray(R.array.backup_restore_tabs_multiple); mTabFragments = new Fragment[mTabTitles.length()]; mTabFragments[0] = getBackupFragment(); mTabFragments[1] = RestoreMultipleFragment.getInstance(); getBody().findViewById(R.id.container).setVisibility(View.VISIBLE); ViewPager2 viewPager = getBody().findViewById(R.id.pager); TabLayout tabLayout = getBody().findViewById(R.id.tab_layout); viewPager.setOffscreenPageLimit(1); viewPager.registerOnPageChangeCallback(new ViewPagerUpdateScrollingChildListener(viewPager, getBehavior())); finishLoading(); viewPager.setAdapter(new BackupDialogFragmentPagerAdapter(this)); new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> tab.setText(mTabTitles.getString(position))) .attach(); } public void updateMultipleRestoreHeader() { // Display delete button mDialogTitleBuilder.setEndIcon(R.drawable.ic_trash_can, v -> handleDeleteBaseBackup()) .setEndIconContentDescription(R.string.delete_backup); setHeader(mDialogTitleBuilder.build()); } private void loadSingleBackupFragment() { mDialogTitleBuilder.setTitle(R.string.backup); updateSingleBackupHeader(); finishLoading(); getChildFragmentManager() .beginTransaction() .replace(R.id.fragment_container_view_tag, getBackupFragment()) .commit(); } private void loadSingleRestoreFragment() { mDialogTitleBuilder.setTitle(R.string.restore_dots); updateSingleBackupHeader(); finishLoading(); getChildFragmentManager() .beginTransaction() .replace(R.id.fragment_container_view_tag, RestoreSingleFragment.getInstance()) .commit(); } private void loadSingleBackupRestoreViewPager() { updateSingleBackupHeader(); mTabTitles = getResources().obtainTypedArray(R.array.backup_restore_tabs_single); mTabFragments = new Fragment[mTabTitles.length()]; mTabFragments[0] = getBackupFragment(); mTabFragments[1] = RestoreSingleFragment.getInstance(); getBody().findViewById(R.id.container).setVisibility(View.VISIBLE); ViewPager2 viewPager = getBody().findViewById(R.id.pager); TabLayout tabLayout = getBody().findViewById(R.id.tab_layout); viewPager.setOffscreenPageLimit(1); viewPager.registerOnPageChangeCallback(new ViewPagerUpdateScrollingChildListener(viewPager, getBehavior())); finishLoading(); viewPager.setAdapter(new BackupDialogFragmentPagerAdapter(this)); new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> tab.setText(mTabTitles.getString(position))) .attach(); } private void updateSingleBackupHeader() { mDialogTitleBuilder.setSubtitle(mViewModel.getBackupInfo().getAppLabel()); setHeader(mDialogTitleBuilder.build()); } public void handleCustomUsers(@NonNull BackupRestoreDialogViewModel.OperationInfo operationInfo) { // NonNull check is added because we are only here when there are more than one users List users = Objects.requireNonNull(operationInfo.userInfoList); CharSequence[] userNames = new String[users.size()]; List userHandles = new ArrayList<>(users.size()); int i = 0; for (UserInfo info : users) { userNames[i] = info.toLocalizedString(requireContext()); userHandles.add(info.id); ++i; } new SearchableMultiChoiceDialogBuilder<>(mActivity, userHandles, userNames) .setTitle(R.string.select_user) .addSelections(Collections.singletonList(UserHandleHidden.myUserId())) .showSelectAll(false) .setPositiveButton(R.string.ok, (dialog, which, selectedUsers) -> { if (!selectedUsers.isEmpty()) { operationInfo.selectedUsers = ArrayUtils.convertToIntArray(selectedUsers); } mViewModel.prepareForOperation(operationInfo); }) .setNegativeButton(R.string.cancel, null) .show(); } private void handleDeleteBaseBackup() { // TODO: 5/7/22 Clarify the message by including base backup in the message. // TODO: 5/7/22 Display a check box that will include all the backups instead of only base backups. new MaterialAlertDialogBuilder(mActivity) .setTitle(R.string.delete_backup) .setMessage(R.string.are_you_sure) .setPositiveButton(R.string.yes, (dialog, which) -> { BackupRestoreDialogViewModel.OperationInfo operationInfo = new BackupRestoreDialogViewModel.OperationInfo(); operationInfo.mode = BackupRestoreDialogFragment.MODE_DELETE; operationInfo.op = BatchOpsManager.OP_DELETE_BACKUP; mViewModel.prepareForOperation(operationInfo); }) .setNegativeButton(R.string.no, null) .show(); } @UiThread private void startOperation(@NonNull BackupRestoreDialogViewModel.OperationInfo operationInfo) { mMode = operationInfo.mode; if (mActionBeginInterface != null) { mActionBeginInterface.onActionBegin(operationInfo.mode); } ContextCompat.registerReceiver(mActivity, mBatchOpsBroadCastReceiver, new IntentFilter(BatchOpsService.ACTION_BATCH_OPS_COMPLETED), ContextCompat.RECEIVER_NOT_EXPORTED); // Start batch ops service BatchBackupOptions options = new BatchBackupOptions(operationInfo.flags, operationInfo.backupNames, operationInfo.relativeDirs); BatchQueueItem queueItem = BatchQueueItem.getBatchOpQueue(operationInfo.op, operationInfo.packageList, operationInfo.userIdListMappedToPackageList, options); Intent intent = BatchOpsService.getServiceIntent(mActivity, queueItem); ContextCompat.startForegroundService(mActivity, intent); dismiss(); } private class BackupDialogFragmentPagerAdapter extends FragmentStateAdapter { public BackupDialogFragmentPagerAdapter(@NonNull Fragment fragment) { super(fragment); } @NonNull @Override public Fragment createFragment(int position) { return mTabFragments[position]; } @Override public int getItemCount() { return mTabTitles.length(); } } private static class ViewPagerUpdateScrollingChildListener extends ViewPager2.OnPageChangeCallback { private final ViewPager2 mViewPager; private final BottomSheetBehavior mBehavior; private ViewPagerUpdateScrollingChildListener(ViewPager2 viewPager, BottomSheetBehavior behavior) { mViewPager = viewPager; mBehavior = behavior; } @Override public void onPageSelected(int position) { mViewPager.post(mBehavior::updateScrollingChild); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/backup/dialog/BackupRestoreDialogViewModel.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.backup.dialog; import android.annotation.UserIdInt; import android.app.Application; import android.os.PowerManager; import android.os.UserHandleHidden; import androidx.annotation.AnyThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.Future; import io.github.muntashirakon.AppManager.backup.BackupFlags; import io.github.muntashirakon.AppManager.backup.struct.BackupMetadataV5; import io.github.muntashirakon.AppManager.batchops.BatchOpsManager; import io.github.muntashirakon.AppManager.db.entity.App; import io.github.muntashirakon.AppManager.db.entity.Backup; import io.github.muntashirakon.AppManager.db.utils.AppDb; import io.github.muntashirakon.AppManager.types.UserPackagePair; import io.github.muntashirakon.AppManager.users.UserInfo; import io.github.muntashirakon.AppManager.users.Users; import io.github.muntashirakon.AppManager.utils.CpuUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; public class BackupRestoreDialogViewModel extends AndroidViewModel { public static class OperationInfo { @BackupRestoreDialogFragment.ActionMode public int mode; @BatchOpsManager.OpType public int op; @BackupFlags.BackupFlag public int flags; @Nullable public String[] backupNames; @Nullable public String[] relativeDirs; @Nullable public int[] selectedUsers; // Others public boolean handleMultipleUsers = true; @Nullable public List userInfoList; public ArrayList packageList; public ArrayList userIdListMappedToPackageList; } private int mWorstBackupFlag; private int[] mPreferredUsersForBackup; private int[] mPreferredUsersForRestore; private boolean mAllowCustomUsersInBackup = true; private Future mProcessPackageFuture; private Future mHandleUsersFuture; @NonNull private final List mBackupInfoList = new ArrayList<>(); private final Set mAppsWithoutBackups = new HashSet<>(); private final Set mUninstalledApps = new HashSet<>(); private final MutableLiveData mUserSelectionLiveData = new MutableLiveData<>(); private final MutableLiveData mBackupOperationLiveData = new MutableLiveData<>(); private final MutableLiveData mBackupInfoStateLiveData = new MutableLiveData<>(); public BackupRestoreDialogViewModel(@NonNull Application application) { super(application); } @Override protected void onCleared() { if (mProcessPackageFuture != null) { mProcessPackageFuture.cancel(true); } if (mHandleUsersFuture != null) { mHandleUsersFuture.cancel(true); } super.onCleared(); } public LiveData getBackupInfoStateLiveData() { return mBackupInfoStateLiveData; } public LiveData getBackupOperationLiveData() { return mBackupOperationLiveData; } public MutableLiveData getUserSelectionLiveData() { return mUserSelectionLiveData; } @NonNull public List getBackupInfoList() { return mBackupInfoList; } public Set getAppsWithoutBackups() { return mAppsWithoutBackups; } public Set getUninstalledApps() { return mUninstalledApps; } @NonNull public BackupInfo getBackupInfo() { return mBackupInfoList.get(0); } @BackupFlags.BackupFlag public int getWorstBackupFlag() { return mWorstBackupFlag; } public boolean allowCustomUsersInBackup() { return mAllowCustomUsersInBackup; } public void setPreferredUserForRestore(@UserIdInt int preferredUserForRestore) { mPreferredUsersForRestore = new int[]{preferredUserForRestore}; } @AnyThread public void processPackages(@Nullable List userPackagePairs) { mProcessPackageFuture = ThreadUtils.postOnBackgroundThread(() -> { if (userPackagePairs == null) { mBackupInfoStateLiveData.postValue(BackupInfoState.NONE); mWorstBackupFlag = 0; return; } PowerManager.WakeLock wakeLock = CpuUtils.getPartialWakeLock("backup_dialog_process"); wakeLock.acquire(); try { processPackagesInternal(userPackagePairs); } finally { CpuUtils.releaseWakeLock(wakeLock); } }); } @AnyThread public void prepareForOperation(@NonNull OperationInfo operationInfo) { mHandleUsersFuture = ThreadUtils.postOnBackgroundThread(() -> { if (operationInfo.handleMultipleUsers && operationInfo.mode != BackupRestoreDialogFragment.MODE_DELETE && (operationInfo.flags & BackupFlags.BACKUP_CUSTOM_USERS) != 0) { // Handle custom users for backup/restore operations if requested handleCustomUsers(operationInfo); return; } operationInfo.handleMultipleUsers = false; generatePackageUserIdLists(operationInfo); mBackupOperationLiveData.postValue(operationInfo); }); } private void processPackagesInternal(@NonNull List userPackagePairs) { Map backupInfoMap = new HashMap<>(); AppDb appDb = new AppDb(); // Fetch info for (UserPackagePair userPackagePair : userPackagePairs) { if (ThreadUtils.isInterrupted()) { return; } if (userPackagePair.getPackageName().equals("android")) { // Skip checking android package because it can't be backed up or restored. continue; } BackupInfo backupInfo = backupInfoMap.get(userPackagePair.getPackageName()); if (backupInfo != null) { // Entry exists, add user ID only backupInfo.userIds.add(userPackagePair.getUserId()); continue; } // Add new entry backupInfo = new BackupInfo(userPackagePair.getPackageName(), userPackagePair.getUserId()); List apps = appDb.getAllApplications(userPackagePair.getPackageName(), userPackagePair.getUserId()); List backups = appDb.getAllBackups(userPackagePair.getPackageName()); if (ThreadUtils.isInterrupted()) { return; } // Fetch backup info List metadataList = new ArrayList<>(); for (Backup backup : backups) { BackupMetadataV5 metadata; try { metadata = backup.getItem().getMetadata(); metadataList.add(metadata); } catch (IOException e) { // Not found continue; } if (metadata.isBaseBackup()) { backupInfo.setHasBaseBackup(true); } } if (ThreadUtils.isInterrupted()) { return; } backupInfo.setBackupMetadataList(metadataList); if (apps.isEmpty()) { backupInfo.setInstalled(false); } else { for (App app : apps) { backupInfo.setAppLabel(app.packageLabel); // Installation gets higher priority backupInfo.setInstalled(backupInfo.isInstalled() | app.isInstalled); } } if (!backupInfo.isInstalled() && backupInfo.getBackupMetadataList().isEmpty()) { // App cannot be backed up or restored continue; } backupInfoMap.put(userPackagePair.getPackageName(), backupInfo); } if (ThreadUtils.isInterrupted()) { return; } mBackupInfoList.clear(); mBackupInfoList.addAll(backupInfoMap.values()); mAppsWithoutBackups.clear(); mUninstalledApps.clear(); // Check if mBackupInfoList is singleton if (mBackupInfoList.size() == 1) { // Singleton list BackupInfo backupInfo = mBackupInfoList.get(0); if (backupInfo.isInstalled() && backupInfo.userIds.size() == 1) { // A special case where we need to check if we can allow custom users for backups mPreferredUsersForBackup = new int[]{Objects.requireNonNull(backupInfo.userIds.valueAt(0))}; List apps = appDb.getAllApplications(backupInfo.packageName); int userCount = 0; for (App app : apps) { if (app.isInstalled) { ++userCount; } } mAllowCustomUsersInBackup = userCount > 1; } } if (mPreferredUsersForBackup == null) { mPreferredUsersForBackup = new int[]{UserHandleHidden.myUserId()}; } if (mPreferredUsersForRestore == null) { mPreferredUsersForRestore = new int[]{UserHandleHidden.myUserId()}; } // Find status int status; mWorstBackupFlag = 0xffff_ffff; if (mBackupInfoList.size() == 1) { // Single backup BackupInfo backupInfo = mBackupInfoList.get(0); for (BackupMetadataV5 metadata : backupInfo.getBackupMetadataList()) { mWorstBackupFlag &= metadata.info.flags.getFlags(); } if (backupInfo.getBackupMetadataList().isEmpty()) { mAppsWithoutBackups.add(backupInfo.getAppLabel()); } if (!backupInfo.isInstalled()) { mUninstalledApps.add(backupInfo.getAppLabel()); } if (backupInfo.isInstalled() && !backupInfo.getBackupMetadataList().isEmpty()) { status = BackupInfoState.BOTH_SINGLE; } else if (backupInfo.isInstalled()) { status = BackupInfoState.BACKUP_SINGLE; } else if (!backupInfo.getBackupMetadataList().isEmpty()) { status = BackupInfoState.RESTORE_SINGLE; } else status = BackupInfoState.NONE; } else { // Multiple backup boolean hasInstalled = false; boolean hasBaseBackup = false; for (BackupInfo backupInfo : mBackupInfoList) { if (ThreadUtils.isInterrupted()) { return; } if (backupInfo.isInstalled()) { hasInstalled = true; } else { mUninstalledApps.add(backupInfo.getAppLabel()); } if (backupInfo.hasBaseBackup()) { hasBaseBackup = true; for (BackupMetadataV5 metadata : backupInfo.getBackupMetadataList()) { if (metadata.isBaseBackup()) { mWorstBackupFlag &= metadata.info.flags.getFlags(); } } } else { mAppsWithoutBackups.add(backupInfo.getAppLabel()); } } // Remove irrelevant flags int worstBackupFlag = mWorstBackupFlag & ~(BackupFlags.BACKUP_MULTIPLE | BackupFlags.BACKUP_CUSTOM_USERS | BackupFlags.BACKUP_NO_SIGNATURE_CHECK); hasBaseBackup = hasBaseBackup && worstBackupFlag > 0; if (hasInstalled && hasBaseBackup) { status = BackupInfoState.BOTH_MULTIPLE; } else if (hasInstalled) { status = BackupInfoState.BACKUP_MULTIPLE; } else if (hasBaseBackup) { status = BackupInfoState.RESTORE_MULTIPLE; } else status = BackupInfoState.NONE; } if (ThreadUtils.isInterrupted()) { return; } // Send status mBackupInfoStateLiveData.postValue(status); } @WorkerThread private void handleCustomUsers(@NonNull OperationInfo operationInfo) { operationInfo.handleMultipleUsers = false; List users = Users.getUsers(); if (users.size() <= 1) { // There's only one user (which should not happen because the flag should be hidden) // Strip custom users flag and start the operation operationInfo.flags &= ~BackupFlags.BACKUP_CUSTOM_USERS; generatePackageUserIdLists(operationInfo); mBackupOperationLiveData.postValue(operationInfo); return; } operationInfo.userInfoList = users; mUserSelectionLiveData.postValue(operationInfo); } @WorkerThread private void generatePackageUserIdLists(@NonNull OperationInfo operationInfo) { int[] userIds; if (operationInfo.selectedUsers != null) { userIds = operationInfo.selectedUsers; } else if (operationInfo.mode == BackupRestoreDialogFragment.MODE_BACKUP) { userIds = mPreferredUsersForBackup; } else { // restore/delete mode userIds = mPreferredUsersForRestore; } operationInfo.packageList = new ArrayList<>(); operationInfo.userIdListMappedToPackageList = new ArrayList<>(); // For singleton restore, cross user restore is supported. So, we need to handle that here. if (operationInfo.mode == BackupRestoreDialogFragment.MODE_RESTORE && mBackupInfoList.size() == 1) { BackupInfo backupInfo = mBackupInfoList.get(0); if (!backupInfo.getBackupMetadataList().isEmpty() && backupInfo.userIds.size() == 1) { // Singleton restore for (int userId : userIds) { // Same backup can be restored for multiple users operationInfo.packageList.add(backupInfo.packageName); operationInfo.userIdListMappedToPackageList.add(userId); } } } // Otherwise, user checks are mandatory. for (BackupInfo backupInfo : mBackupInfoList) { if (ThreadUtils.isInterrupted()) { return; } for (int userId : userIds) { if (backupInfo.userIds.contains(userId)) { operationInfo.packageList.add(backupInfo.packageName); operationInfo.userIdListMappedToPackageList.add(userId); } } } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/backup/dialog/FlagsAdapter.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.backup.dialog; import android.annotation.SuppressLint; import android.content.Context; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.CheckedTextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.resources.MaterialAttributes; import java.util.List; import io.github.muntashirakon.AppManager.backup.BackupFlags; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.util.AdapterUtils; class FlagsAdapter extends RecyclerView.Adapter { private final int mLayoutId; private final List mSupportedBackupFlags; private final CharSequence[] mSupportedBackupFlagNames; @BackupFlags.BackupFlag private final int mDisabledFlags; @BackupFlags.BackupFlag private int mSelectedFlags; @SuppressLint("RestrictedApi") public FlagsAdapter(@NonNull Context context, @BackupFlags.BackupFlag int flags, @BackupFlags.BackupFlag int supportedFlags) { this(context, flags, supportedFlags, 0); } @SuppressLint("RestrictedApi") public FlagsAdapter(@NonNull Context context, @BackupFlags.BackupFlag int flags, @BackupFlags.BackupFlag int supportedFlags, @BackupFlags.BackupFlag int disabledFlags) { mLayoutId = MaterialAttributes.resolveInteger(context, androidx.appcompat.R.attr.multiChoiceItemLayout, com.google.android.material.R.layout.mtrl_alert_select_dialog_multichoice); // We list |supportedFlags| and select |flags| by default mSupportedBackupFlags = BackupFlags.getBackupFlagsAsArray(supportedFlags); mSupportedBackupFlagNames = BackupFlags.getFormattedFlagNames(context, mSupportedBackupFlags); mSelectedFlags = flags; mDisabledFlags = disabledFlags; notifyItemRangeInserted(0, mSupportedBackupFlags.size()); } public int getSelectedFlags() { return mSelectedFlags; } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(mLayoutId, parent, false); return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { int flag = mSupportedBackupFlags.get(position); boolean isSelected = (mSelectedFlags & flag) != 0; boolean isDisabled = (mDisabledFlags & flag) != 0; holder.item.setChecked(isSelected); holder.item.setEnabled(!isDisabled); holder.item.setText(mSupportedBackupFlagNames[position]); holder.item.setOnClickListener(v -> { if (isSelected) { // Now unselected mSelectedFlags &= ~flag; } else { // Now selected mSelectedFlags |= flag; } notifyItemChanged(position, AdapterUtils.STUB); }); } @Override public int getItemCount() { return mSupportedBackupFlags.size(); } static class ViewHolder extends RecyclerView.ViewHolder { CheckedTextView item; public ViewHolder(@NonNull View itemView) { super(itemView); item = itemView.findViewById(android.R.id.text1); // textAppearanceBodyLarge item.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16); item.setTextColor(UIUtils.getTextColorSecondary(item.getContext())); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/backup/dialog/RestoreMultipleFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.backup.dialog; import android.content.Context; import android.os.Bundle; import android.text.SpannableStringBuilder; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.util.Set; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.backup.BackupFlags; import io.github.muntashirakon.AppManager.batchops.BatchOpsManager; import io.github.muntashirakon.widget.MaterialAlertView; public class RestoreMultipleFragment extends Fragment { @NonNull public static RestoreMultipleFragment getInstance() { return new RestoreMultipleFragment(); } private BackupRestoreDialogViewModel mViewModel; private Context mContext; @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_dialog_restore_multiple, container, false); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { mViewModel = new ViewModelProvider(requireParentFragment()).get(BackupRestoreDialogViewModel.class); mContext = requireContext(); MaterialAlertView messageView = view.findViewById(R.id.message); RecyclerView recyclerView = view.findViewById(android.R.id.list); recyclerView.setLayoutManager(new LinearLayoutManager(mContext, LinearLayoutManager.VERTICAL, false)); int supportedFlags = mViewModel.getWorstBackupFlag(); // Inject no signatures supportedFlags |= BackupFlags.BACKUP_NO_SIGNATURE_CHECK; supportedFlags |= BackupFlags.BACKUP_CUSTOM_USERS; int checkedFlags = BackupFlags.fromPref().getFlags() & supportedFlags; int disabledFlags = 0; if (!mViewModel.getUninstalledApps().isEmpty()) { checkedFlags |= BackupFlags.BACKUP_APK_FILES; disabledFlags |= BackupFlags.BACKUP_APK_FILES; } FlagsAdapter adapter = new FlagsAdapter(mContext, checkedFlags, supportedFlags, disabledFlags); recyclerView.setAdapter(adapter); Set appsWithoutBackups = mViewModel.getAppsWithoutBackups(); if (!appsWithoutBackups.isEmpty()) { SpannableStringBuilder sb = new SpannableStringBuilder(getString(R.string.backup_apps_cannot_be_restored)); for (CharSequence appLabel : appsWithoutBackups) { sb.append("\n● ").append(appLabel); } messageView.setText(sb); messageView.setVisibility(View.VISIBLE); } view.findViewById(R.id.action_restore).setOnClickListener(v -> { int newFlags = adapter.getSelectedFlags(); handleRestore(newFlags); }); } private void handleRestore(int flags) { new MaterialAlertDialogBuilder(mContext) .setTitle(R.string.restore) .setMessage(R.string.are_you_sure) .setPositiveButton(R.string.yes, (dialog, which) -> { BackupRestoreDialogViewModel.OperationInfo operationInfo = new BackupRestoreDialogViewModel.OperationInfo(); operationInfo.mode = BackupRestoreDialogFragment.MODE_RESTORE; operationInfo.op = BatchOpsManager.OP_RESTORE_BACKUP; operationInfo.flags = flags; mViewModel.prepareForOperation(operationInfo); }) .setNegativeButton(R.string.no, null) .show(); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/backup/dialog/RestoreSingleFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.backup.dialog; import android.annotation.SuppressLint; import android.content.Context; import android.os.Bundle; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.CheckedTextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.PopupMenu; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.button.MaterialButton; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.resources.MaterialAttributes; import java.io.IOException; import java.util.ArrayList; import java.util.List; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.backup.BackupFlags; import io.github.muntashirakon.AppManager.backup.struct.BackupMetadataV5; import io.github.muntashirakon.AppManager.batchops.BatchOpsManager; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.dialog.SearchableFlagsDialogBuilder; import io.github.muntashirakon.util.AdapterUtils; public class RestoreSingleFragment extends Fragment { public static RestoreSingleFragment getInstance() { return new RestoreSingleFragment(); } private BackupRestoreDialogViewModel mViewModel; private Context mContext; @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_dialog_restore_single, container, false); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { mViewModel = new ViewModelProvider(requireParentFragment()).get(BackupRestoreDialogViewModel.class); mContext = requireContext(); RecyclerView recyclerView = view.findViewById(android.R.id.list); MaterialButton restoreButton = view.findViewById(R.id.action_restore); MaterialButton deleteButton = view.findViewById(R.id.action_delete); MaterialButton moreButton = view.findViewById(R.id.more); recyclerView.setLayoutManager(new LinearLayoutManager(mContext, LinearLayoutManager.VERTICAL, false)); BackupAdapter adapter = new BackupAdapter(mContext, mViewModel.getBackupInfo().getBackupMetadataList(), (metadata, selectionCount, added) -> { restoreButton.setEnabled(selectionCount == 1); deleteButton.setEnabled(selectionCount > 0); }); recyclerView.setAdapter(adapter); restoreButton.setOnClickListener(v -> handleRestore(adapter.getSelectedBackups().get(0))); deleteButton.setOnClickListener(v -> handleDelete(adapter.getSelectedBackups())); moreButton.setOnClickListener(v -> { int total = adapter.selectionCount(); int frozenCount = adapter.getFrozenBackupSelectionCount(); PopupMenu popupMenu = new PopupMenu(mContext, v); Menu menu = popupMenu.getMenu(); MenuItem freezeMenuItem = menu.add(R.string.freeze); MenuItem unfreezeMenuItem = menu.add(R.string.unfreeze); freezeMenuItem.setEnabled((total - frozenCount) > 0); unfreezeMenuItem.setEnabled(frozenCount > 0); freezeMenuItem.setOnMenuItemClickListener(item -> { List selectedBackups = adapter.getSelectedBackups(); for (BackupMetadataV5 metadata : selectedBackups) { try { metadata.info.getBackupItem().freeze(); ++adapter.mFrozenBackupSelectionCount; } catch (IOException ignore) { } } adapter.notifyItemRangeChanged(0, adapter.getItemCount(), AdapterUtils.STUB); return true; }); unfreezeMenuItem.setOnMenuItemClickListener(item -> { List selectedBackups = adapter.getSelectedBackups(); for (BackupMetadataV5 metadata : selectedBackups) { try { metadata.info.getBackupItem().unfreeze(); --adapter.mFrozenBackupSelectionCount; } catch (IOException ignore) { } } adapter.notifyItemRangeChanged(0, adapter.getItemCount(), AdapterUtils.STUB); return true; }); popupMenu.show(); }); } private void handleRestore(@NonNull BackupMetadataV5 selectedBackup) { BackupFlags flags = selectedBackup.info.flags; BackupFlags enabledFlags = BackupFlags.fromPref(); enabledFlags.setFlags(flags.getFlags() & enabledFlags.getFlags()); List supportedBackupFlags = BackupFlags.getBackupFlagsAsArray(flags.getFlags()); // Inject no signatures supportedBackupFlags.add(BackupFlags.BACKUP_NO_SIGNATURE_CHECK); supportedBackupFlags.add(BackupFlags.BACKUP_CUSTOM_USERS); List disabledFlags = new ArrayList<>(); if (!mViewModel.getBackupInfo().isInstalled()) { enabledFlags.addFlag(BackupFlags.BACKUP_APK_FILES); disabledFlags.add(BackupFlags.BACKUP_APK_FILES); } new SearchableFlagsDialogBuilder<>(mContext, supportedBackupFlags, BackupFlags.getFormattedFlagNames(mContext, supportedBackupFlags), enabledFlags.getFlags()) .setTitle(R.string.backup_options) .addDisabledItems(disabledFlags) .setPositiveButton(R.string.restore, (dialog, which, selections) -> { int newFlags = 0; for (int flag : selections) { newFlags |= flag; } enabledFlags.setFlags(newFlags); BackupRestoreDialogViewModel.OperationInfo operationInfo = new BackupRestoreDialogViewModel.OperationInfo(); operationInfo.mode = BackupRestoreDialogFragment.MODE_RESTORE; operationInfo.op = BatchOpsManager.OP_RESTORE_BACKUP; operationInfo.flags = enabledFlags.getFlags(); operationInfo.relativeDirs = new String[]{selectedBackup.info.getRelativeDir()}; mViewModel.prepareForOperation(operationInfo); }) .setNegativeButton(R.string.cancel, null) .show(); } private void handleDelete(List selectedBackups) { new MaterialAlertDialogBuilder(mContext) .setTitle(R.string.delete_backup) .setMessage(R.string.are_you_sure) .setNegativeButton(R.string.no, null) .setPositiveButton(R.string.yes, (dialog, which) -> { List relativeDirs = new ArrayList<>(selectedBackups.size()); for (BackupMetadataV5 backup : selectedBackups) { relativeDirs.add(backup.info.getRelativeDir()); } BackupRestoreDialogViewModel.OperationInfo operationInfo = new BackupRestoreDialogViewModel.OperationInfo(); operationInfo.mode = BackupRestoreDialogFragment.MODE_DELETE; operationInfo.op = BatchOpsManager.OP_DELETE_BACKUP; operationInfo.relativeDirs = relativeDirs.toArray(new String[0]); mViewModel.prepareForOperation(operationInfo); }) .show(); } private static class BackupAdapter extends RecyclerView.Adapter { public interface OnSelectionListener { void onSelectionChanged(@Nullable BackupMetadataV5 metadata, int selectionCount, boolean added); } private final int mLayoutId; @NonNull private final List mBackups = new ArrayList<>(); @NonNull private final List mSelectedPositions = new ArrayList<>(); @NonNull private final OnSelectionListener mSelectionListener; private int mFrozenBackupSelectionCount = 0; @SuppressLint("RestrictedApi") public BackupAdapter(@NonNull Context context, @NonNull List backups, @NonNull OnSelectionListener selectionListener) { mSelectionListener = selectionListener; mLayoutId = MaterialAttributes.resolveInteger(context, androidx.appcompat.R.attr.multiChoiceItemLayout, com.google.android.material.R.layout.mtrl_alert_select_dialog_multichoice); mSelectionListener.onSelectionChanged(null, mSelectedPositions.size(), false); for (int i = 0; i < backups.size(); ++i) { BackupMetadataV5 backup = backups.get(i); mBackups.add(backup); if (backup.isBaseBackup()) { mSelectedPositions.add(i); if (backup.info.isFrozen()) { ++mFrozenBackupSelectionCount; } mSelectionListener.onSelectionChanged(backup, mSelectedPositions.size(), true); } } notifyItemRangeInserted(0, mBackups.size()); } public int selectionCount() { return mSelectedPositions.size(); } public int getFrozenBackupSelectionCount() { return mFrozenBackupSelectionCount; } @NonNull public List getSelectedBackups() { List selectedBackups = new ArrayList<>(); for (int position : mSelectedPositions) { selectedBackups.add(mBackups.get(position)); } return selectedBackups; } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(mLayoutId, parent, false); return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { BackupMetadataV5 metadata = mBackups.get(position); boolean isSelected = mSelectedPositions.contains(position); holder.item.setChecked(isSelected); holder.item.setText(metadata.toLocalizedString(holder.item.getContext())); holder.item.setOnClickListener(v -> { if (isSelected) { // Now unselected mSelectedPositions.remove((Integer) position); if (metadata.info.isFrozen()) { --mFrozenBackupSelectionCount; } mSelectionListener.onSelectionChanged(metadata, mSelectedPositions.size(), false); } else { // Now selected mSelectedPositions.add(position); if (metadata.info.isFrozen()) { ++mFrozenBackupSelectionCount; } mSelectionListener.onSelectionChanged(metadata, mSelectedPositions.size(), true); } notifyItemChanged(position, AdapterUtils.STUB); }); } @Override public int getItemCount() { return mBackups.size(); } static class ViewHolder extends RecyclerView.ViewHolder { CheckedTextView item; public ViewHolder(@NonNull View itemView) { super(itemView); item = itemView.findViewById(android.R.id.text1); // textAppearanceBodyLarge item.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16); item.setTextColor(UIUtils.getTextColorSecondary(item.getContext())); } } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/backup/struct/BackupMetadataV2.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.backup.struct; import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.json.JSONException; import org.json.JSONObject; import aosp.libcore.util.HexEncoding; import io.github.muntashirakon.AppManager.backup.BackupFlags; import io.github.muntashirakon.AppManager.backup.BackupItems; import io.github.muntashirakon.AppManager.backup.CryptoUtils; import io.github.muntashirakon.AppManager.backup.MetadataManager; import io.github.muntashirakon.AppManager.history.IJsonSerializer; import io.github.muntashirakon.AppManager.misc.VMRuntime; import io.github.muntashirakon.AppManager.utils.DigestUtils; import io.github.muntashirakon.AppManager.utils.JSONUtils; import io.github.muntashirakon.AppManager.utils.TarUtils; // For an extended documentation, see https://github.com/MuntashirAkon/AppManager/issues/30 // All the attributes must be non-null public class BackupMetadataV2 implements IJsonSerializer { @Nullable public String backupName; // This isn't part of the json file and for internal use only public BackupItems.BackupItem backupItem; // This isn't part of the json file and for internal use only public String label; // label public String packageName; // package_name public String versionName; // version_name public long versionCode; // version_code public String[] dataDirs; // data_dirs public boolean isSystem; // is_system public boolean isSplitApk; // is_split_apk public String[] splitConfigs; // split_configs public boolean hasRules; // has_rules public long backupTime; // backup_time @DigestUtils.Algorithm public String checksumAlgo = DigestUtils.SHA_256; // checksum_algo @CryptoUtils.Mode public String crypto; // crypto @Nullable public byte[] iv; // iv @Nullable public byte[] aes; // aes (encrypted using RSA/ECC, for RSA/ECC only) @Nullable public String keyIds; // key_ids /** * Metadata version. *

    *
  • {@code 1} - Alpha version, no longer supported
  • *
  • {@code 2} - Beta version (v2.5.2x), permissions aren't preserved (special action needed)
  • *
  • {@code 3} - From v2.6.x to v3.0.2 and v3.1.0-alpha01, permissions are preserved, AES GCM MAC size is 32 bits
  • *
  • {@code 4} - Since v3.0.3 and v3.1.0-alpha02, AES GCM MAC size is 128 bits
  • *
*/ public int version; // version public String apkName; // apk_name public String instructionSet = VMRuntime.getInstructionSet(Build.SUPPORTED_ABIS[0]); // instruction_set public BackupFlags flags; // flags public int userId; // user_handle @TarUtils.TarType public String tarType; // tar_type public boolean keyStore; // key_store public String installer; // installer public BackupMetadataV2() { version = MetadataManager.getCurrentBackupMetaVersion(); } public BackupMetadataV2(@NonNull BackupMetadataV2 metadata) { backupName = metadata.backupName; backupItem = metadata.backupItem; label = metadata.label; packageName = metadata.packageName; versionName = metadata.versionName; versionCode = metadata.versionCode; if (metadata.dataDirs != null) { dataDirs = metadata.dataDirs.clone(); } isSystem = metadata.isSystem; isSplitApk = metadata.isSplitApk; if (metadata.splitConfigs != null) { splitConfigs = metadata.splitConfigs.clone(); } hasRules = metadata.hasRules; backupTime = metadata.backupTime; checksumAlgo = metadata.checksumAlgo; crypto = metadata.crypto; if (metadata.iv != null) { iv = metadata.iv.clone(); } if (metadata.aes != null) { aes = metadata.aes.clone(); } keyIds = metadata.keyIds; version = metadata.version; apkName = metadata.apkName; instructionSet = metadata.instructionSet; flags = new BackupFlags(metadata.flags.getFlags()); userId = metadata.userId; tarType = metadata.tarType; keyStore = metadata.keyStore; installer = metadata.installer; } @NonNull @Override public JSONObject serializeToJson() throws JSONException { JSONObject rootObject = new JSONObject(); rootObject.put("label", label); rootObject.put("package_name", packageName); rootObject.put("version_name", versionName); rootObject.put("version_code", versionCode); rootObject.put("data_dirs", JSONUtils.getJSONArray(dataDirs)); rootObject.put("is_system", isSystem); rootObject.put("is_split_apk", isSplitApk); rootObject.put("split_configs", JSONUtils.getJSONArray(splitConfigs)); rootObject.put("has_rules", hasRules); rootObject.put("backup_time", backupTime); rootObject.put("checksum_algo", checksumAlgo); rootObject.put("crypto", crypto); rootObject.put("key_ids", keyIds); rootObject.put("iv", iv == null ? null : HexEncoding.encodeToString(iv)); rootObject.put("aes", aes == null ? null : HexEncoding.encodeToString(aes)); rootObject.put("version", version); rootObject.put("apk_name", apkName); rootObject.put("instruction_set", instructionSet); rootObject.put("flags", flags.getFlags()); rootObject.put("user_handle", userId); rootObject.put("tar_type", tarType); rootObject.put("key_store", keyStore); rootObject.put("installer", installer); return rootObject; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/backup/struct/BackupMetadataV5.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.backup.struct; import static io.github.muntashirakon.AppManager.backup.BackupUtils.getReadableTarType; import static io.github.muntashirakon.AppManager.utils.UIUtils.getSecondaryText; import static io.github.muntashirakon.AppManager.utils.UIUtils.getSmallerText; import static io.github.muntashirakon.AppManager.utils.UIUtils.getTitleText; import android.annotation.UserIdInt; import android.content.Context; import android.os.Build; import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.text.format.Formatter; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import org.json.JSONException; import org.json.JSONObject; import java.util.Locale; import java.util.Objects; import aosp.libcore.util.HexEncoding; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.backup.BackupFlags; import io.github.muntashirakon.AppManager.backup.BackupItems; import io.github.muntashirakon.AppManager.backup.CryptoUtils; import io.github.muntashirakon.AppManager.backup.MetadataManager; import io.github.muntashirakon.AppManager.crypto.AESCrypto; import io.github.muntashirakon.AppManager.crypto.Crypto; import io.github.muntashirakon.AppManager.crypto.CryptoException; import io.github.muntashirakon.AppManager.crypto.DummyCrypto; import io.github.muntashirakon.AppManager.crypto.ECCCrypto; import io.github.muntashirakon.AppManager.crypto.OpenPGPCrypto; import io.github.muntashirakon.AppManager.crypto.RSACrypto; import io.github.muntashirakon.AppManager.history.IJsonSerializer; import io.github.muntashirakon.AppManager.misc.VMRuntime; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.DateUtils; import io.github.muntashirakon.AppManager.utils.DigestUtils; import io.github.muntashirakon.AppManager.utils.JSONUtils; import io.github.muntashirakon.AppManager.utils.LangUtils; import io.github.muntashirakon.AppManager.utils.TarUtils; import io.github.muntashirakon.io.Paths; import io.github.muntashirakon.util.LocalizedString; public class BackupMetadataV5 implements LocalizedString { public static class Info implements IJsonSerializer { /** * Relative location of the backup from the AppManager directory, internal use only. */ private String mRelativeDir; public BackupItems.BackupItem mBackupItem; // This isn't part of the json file and for internal use only /** * Metadata version. * *
    *
  • {@code 1} - Alpha version, no longer supported
  • *
  • {@code 2} - Beta version (v2.5.2x), permissions aren't preserved (special action needed)
  • *
  • {@code 3} - From v2.6.x to v3.0.2 and v3.1.0-alpha01, permissions are preserved, AES GCM MAC size is 32 bits
  • *
  • {@code 4} - Since v3.0.3 and v3.1.0-alpha02, AES GCM MAC size is 128 bits
  • *
  • {@code 5} - Since v4.0.6, meta.json, info.json, privacy-friendly backup
  • *
*/ public final int version; // version public final long backupTime; // backup_time @NonNull public final BackupFlags flags; // flags @UserIdInt public final int userId; // user_handle @NonNull @TarUtils.TarType public final String tarType; // tar_type @NonNull @DigestUtils.Algorithm public final String checksumAlgo; // checksum_algo @NonNull @CryptoUtils.Mode public final String crypto; // crypto @Nullable public final byte[] iv; // iv @Nullable public final byte[] aes; // aes (encrypted using RSA, for RSA only) @Nullable public final String keyIds; // key_ids @Nullable private Crypto mCrypto; public Info(long backupTime, @NonNull BackupFlags flags, @UserIdInt int userId, @TarUtils.TarType @NonNull String tarType, @DigestUtils.Algorithm @NonNull String checksumAlgo, @CryptoUtils.Mode @NonNull String crypto, @Nullable byte[] iv, @Nullable byte[] aes, @Nullable String keyIds) { this.version = MetadataManager.getCurrentBackupMetaVersion(); this.backupTime = backupTime; this.flags = flags; this.userId = userId; this.tarType = tarType; this.checksumAlgo = checksumAlgo; this.crypto = crypto; this.iv = iv; this.aes = aes; this.keyIds = keyIds; verifyCrypto(); } public Info(@NonNull JSONObject rootObject) throws JSONException { this.version = rootObject.getInt("version"); this.backupTime = rootObject.getLong("backup_time"); this.flags = new BackupFlags(rootObject.getInt("flags")); this.userId = rootObject.getInt("user_handle"); this.tarType = rootObject.getString("tar_type"); this.checksumAlgo = rootObject.getString("checksum_algo"); this.crypto = rootObject.getString("crypto"); this.keyIds = JSONUtils.optString(rootObject, "key_ids"); String aesKey = JSONUtils.optString(rootObject, "aes"); this.aes = aesKey != null ? HexEncoding.decode(aesKey) : null; String iv = JSONUtils.optString(rootObject, "iv"); this.iv = iv != null ? HexEncoding.decode(iv) : null; verifyCrypto(); } public void setBackupItem(@NonNull BackupItems.BackupItem backupItem) { mBackupItem = backupItem; mRelativeDir = backupItem.getRelativeDir(); } public BackupItems.BackupItem getBackupItem() { return mBackupItem; } public String getRelativeDir() { return mRelativeDir; } // Get crypto only works when crypto is already setup. public Crypto getCrypto() throws CryptoException { if (mCrypto == null) { mCrypto = getCryptoInternal(); } return mCrypto; } public long getBackupSize() { if (mBackupItem == null) return 0L; return Paths.size(mBackupItem.getBackupPath()); } public boolean isFrozen() { return mBackupItem != null && mBackupItem.isFrozen(); } private void verifyCrypto() { switch (crypto) { case CryptoUtils.MODE_OPEN_PGP: Objects.requireNonNull(keyIds); assert !keyIds.isEmpty(); break; case CryptoUtils.MODE_RSA: case CryptoUtils.MODE_ECC: Objects.requireNonNull(aes); assert aes.length > 0; // Deliberate fallthrough case CryptoUtils.MODE_AES: Objects.requireNonNull(iv); assert iv.length > 0; break; case CryptoUtils.MODE_NO_ENCRYPTION: default: } } @NonNull private Crypto getCryptoInternal() throws CryptoException { switch (crypto) { case CryptoUtils.MODE_OPEN_PGP: Objects.requireNonNull(keyIds); return new OpenPGPCrypto(ContextUtils.getContext(), keyIds); case CryptoUtils.MODE_AES: { Objects.requireNonNull(iv); AESCrypto aesCrypto = new AESCrypto(iv); if (version < 4) { // Old backups use 32 bit MAC aesCrypto.setMacSizeBits(AESCrypto.MAC_SIZE_BITS_OLD); } return aesCrypto; } case CryptoUtils.MODE_RSA: { Objects.requireNonNull(iv); Objects.requireNonNull(aes); RSACrypto rsaCrypto = new RSACrypto(iv, aes); if (version < 4) { // Old backups use 32 bit MAC rsaCrypto.setMacSizeBits(AESCrypto.MAC_SIZE_BITS_OLD); } return rsaCrypto; } case CryptoUtils.MODE_ECC: { Objects.requireNonNull(iv); Objects.requireNonNull(aes); ECCCrypto eccCrypto = new ECCCrypto(iv, aes); if (version < 4) { // Old backups use 32 bit MAC eccCrypto.setMacSizeBits(AESCrypto.MAC_SIZE_BITS_OLD); } return eccCrypto; } case CryptoUtils.MODE_NO_ENCRYPTION: default: // Dummy crypto to generalise and return nonNull return new DummyCrypto(); } } @NonNull @Override public JSONObject serializeToJson() throws JSONException { JSONObject rootObject = new JSONObject(); rootObject.put("backup_time", backupTime); rootObject.put("checksum_algo", checksumAlgo); rootObject.put("crypto", crypto); rootObject.put("key_ids", keyIds); rootObject.put("iv", iv == null ? null : HexEncoding.encodeToString(iv)); rootObject.put("aes", aes == null ? null : HexEncoding.encodeToString(aes)); rootObject.put("version", version); rootObject.put("flags", flags.getFlags()); rootObject.put("user_handle", userId); rootObject.put("tar_type", tarType); return rootObject; } } public static class Metadata implements IJsonSerializer { // For backward compatibility only public final int version; // version @Nullable public final String backupName; // backup_name public boolean hasRules; // has_rules public String label; // label public String packageName; // package_name public String versionName; // version_name public long versionCode; // version_code public String[] dataDirs; // data_dirs public boolean isSystem; // is_system public boolean isSplitApk; // is_split_apk public String[] splitConfigs; // split_configs public String apkName; // apk_name public String instructionSet = VMRuntime.getInstructionSet(Build.SUPPORTED_ABIS[0]); // instruction_set public boolean keyStore; // key_store @Nullable public String installer; // installer public Metadata(@Nullable String backupName) { this.version = MetadataManager.getCurrentBackupMetaVersion(); this.backupName = backupName; } public Metadata(@NonNull Metadata metadata) { version = metadata.version; backupName = metadata.backupName; label = metadata.label; packageName = metadata.packageName; versionName = metadata.versionName; versionCode = metadata.versionCode; if (metadata.dataDirs != null) { dataDirs = metadata.dataDirs.clone(); } isSystem = metadata.isSystem; isSplitApk = metadata.isSplitApk; if (metadata.splitConfigs != null) { splitConfigs = metadata.splitConfigs.clone(); } hasRules = metadata.hasRules; apkName = metadata.apkName; instructionSet = metadata.instructionSet; keyStore = metadata.keyStore; installer = metadata.installer; } public Metadata(@NonNull JSONObject rootObject) throws JSONException { version = rootObject.getInt("version"); backupName = JSONUtils.optString(rootObject, "backup_name"); label = rootObject.getString("label"); packageName = rootObject.getString("package_name"); versionName = rootObject.getString("version_name"); versionCode = rootObject.getLong("version_code"); dataDirs = JSONUtils.getArray(String.class, rootObject.getJSONArray("data_dirs")); isSystem = rootObject.getBoolean("is_system"); isSplitApk = rootObject.getBoolean("is_split_apk"); splitConfigs = JSONUtils.getArray(String.class, rootObject.getJSONArray("split_configs")); hasRules = rootObject.getBoolean("has_rules"); apkName = rootObject.getString("apk_name"); instructionSet = rootObject.getString("instruction_set"); keyStore = rootObject.getBoolean("key_store"); installer = JSONUtils.optString(rootObject, "installer"); } @NonNull @Override public JSONObject serializeToJson() throws JSONException { JSONObject rootObject = new JSONObject(); rootObject.put("version", version); rootObject.put("backup_name", backupName); rootObject.put("label", label); rootObject.put("package_name", packageName); rootObject.put("version_name", versionName); rootObject.put("version_code", versionCode); rootObject.put("data_dirs", JSONUtils.getJSONArray(dataDirs)); rootObject.put("is_system", isSystem); rootObject.put("is_split_apk", isSplitApk); rootObject.put("split_configs", JSONUtils.getJSONArray(splitConfigs)); rootObject.put("has_rules", hasRules); rootObject.put("apk_name", apkName); rootObject.put("instruction_set", instructionSet); rootObject.put("key_store", keyStore); rootObject.put("installer", installer); return rootObject; } } @NonNull public final Info info; @NonNull public final Metadata metadata; public BackupMetadataV5(@NonNull Info info, @NonNull Metadata metadata) { this.info = info; this.metadata = metadata; } public boolean isBaseBackup() { return TextUtils.isEmpty(metadata.backupName); } @Override @NonNull @WorkerThread public CharSequence toLocalizedString(@NonNull Context context) { CharSequence titleText = isBaseBackup() ? context.getText(R.string.base_backup) : Objects.requireNonNull(metadata.backupName); StringBuilder subtitleText = new StringBuilder() .append(DateUtils.formatDateTime(context, info.backupTime)) .append(", ") .append(info.flags.toLocalisedString(context)) .append(", ") .append(context.getString(R.string.version)).append(LangUtils.getSeparatorString()).append(metadata.versionName) .append(", ") .append(context.getString(R.string.user_id)).append(LangUtils.getSeparatorString()).append(info.userId); if (info.crypto.equals(CryptoUtils.MODE_NO_ENCRYPTION)) { subtitleText.append(", ").append(context.getString(R.string.no_encryption)); } else { subtitleText.append(", ").append(context.getString(R.string.pgp_aes_rsa_encrypted, info.crypto.toUpperCase(Locale.ROOT))); } subtitleText.append(", ").append(context.getString(R.string.gz_bz2_compressed, getReadableTarType(info.tarType))); subtitleText.append(", ") .append(context.getString(R.string.size)).append(LangUtils.getSeparatorString()).append(Formatter .formatFileSize(context, info.getBackupSize())); if (info.isFrozen()) { subtitleText.append(", ").append(context.getText(R.string.frozen)); } return new SpannableStringBuilder(getTitleText(context, titleText)).append("\n") .append(getSmallerText(getSecondaryText(context, subtitleText))); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/backup/struct/BackupOpOptions.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.backup.struct; import android.annotation.UserIdInt; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.os.ParcelCompat; import org.json.JSONException; import org.json.JSONObject; import java.util.Objects; import io.github.muntashirakon.AppManager.backup.BackupFlags; import io.github.muntashirakon.AppManager.history.IJsonSerializer; import io.github.muntashirakon.AppManager.utils.JSONUtils; public class BackupOpOptions implements Parcelable, IJsonSerializer { @NonNull public final String packageName; @UserIdInt public final int userId; public final BackupFlags flags; @Nullable public final String backupName; public final boolean override; public BackupOpOptions(@NonNull String packageName, int userId, int flags, @Nullable String backupName, boolean override) { this.packageName = packageName; this.userId = userId; this.flags = new BackupFlags(flags); this.backupName = backupName; this.override = override; } protected BackupOpOptions(@NonNull Parcel in) { packageName = Objects.requireNonNull(in.readString()); userId = in.readInt(); flags = new BackupFlags(in.readInt()); backupName = in.readString(); override = ParcelCompat.readBoolean(in); } public static final Creator CREATOR = new Creator() { @Override @NonNull public BackupOpOptions createFromParcel(@NonNull Parcel in) { return new BackupOpOptions(in); } @Override @NonNull public BackupOpOptions[] newArray(int size) { return new BackupOpOptions[size]; } }; @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeString(packageName); dest.writeInt(userId); dest.writeInt(this.flags.getFlags()); dest.writeString(backupName); ParcelCompat.writeBoolean(dest, override); } public BackupOpOptions(@NonNull JSONObject jsonObject) throws JSONException { packageName = jsonObject.getString("package_name"); userId = jsonObject.getInt("user_id"); flags = new BackupFlags(jsonObject.getInt("flags")); backupName = JSONUtils.optString(jsonObject, "backup_name"); override = jsonObject.getBoolean("override"); } @NonNull @Override public JSONObject serializeToJson() throws JSONException { JSONObject jsonObject = new JSONObject(); jsonObject.put("package_name", packageName); jsonObject.put("user_id", userId); jsonObject.put("flags", flags.getFlags()); jsonObject.put("backup_name", backupName); jsonObject.put("override", override); return jsonObject; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/backup/struct/DeleteOpOptions.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.backup.struct; import android.annotation.UserIdInt; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.json.JSONException; import org.json.JSONObject; import java.util.Objects; import io.github.muntashirakon.AppManager.history.IJsonSerializer; import io.github.muntashirakon.AppManager.utils.JSONUtils; public class DeleteOpOptions implements Parcelable, IJsonSerializer { @NonNull public final String packageName; @UserIdInt public final int userId; @Nullable public final String[] relativeDirs; public DeleteOpOptions(@NonNull String packageName, @UserIdInt int userId, @Nullable String[] relativeDirs) { this.packageName = packageName; this.userId = userId; this.relativeDirs = relativeDirs; } protected DeleteOpOptions(@NonNull Parcel in) { packageName = Objects.requireNonNull(in.readString()); userId = in.readInt(); relativeDirs = Objects.requireNonNull(in.createStringArray()); } public static final Creator CREATOR = new Creator() { @Override @NonNull public DeleteOpOptions createFromParcel(@NonNull Parcel in) { return new DeleteOpOptions(in); } @Override @NonNull public DeleteOpOptions[] newArray(int size) { return new DeleteOpOptions[size]; } }; @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeString(packageName); dest.writeInt(userId); dest.writeStringArray(relativeDirs); } public DeleteOpOptions(@NonNull JSONObject jsonObject) throws JSONException { packageName = jsonObject.getString("package_name"); userId = jsonObject.getInt("user_id"); relativeDirs = JSONUtils.getArray(String.class, jsonObject.optJSONArray("relative_dirs")); } @NonNull @Override public JSONObject serializeToJson() throws JSONException { JSONObject jsonObject = new JSONObject(); jsonObject.put("package_name", packageName); jsonObject.put("user_id", userId); jsonObject.put("relative_dirs", JSONUtils.getJSONArray(relativeDirs)); return jsonObject; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/backup/struct/RestoreOpOptions.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.backup.struct; import android.annotation.UserIdInt; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.json.JSONException; import org.json.JSONObject; import java.util.Objects; import io.github.muntashirakon.AppManager.backup.BackupFlags; import io.github.muntashirakon.AppManager.history.IJsonSerializer; import io.github.muntashirakon.AppManager.utils.JSONUtils; public class RestoreOpOptions implements Parcelable, IJsonSerializer { @NonNull public final String packageName; @UserIdInt public final int userId; @Nullable public final String relativeDir; public final BackupFlags flags; public RestoreOpOptions(@NonNull String packageName, int userId, @Nullable String relativeDir, int flags) { this.packageName = packageName; this.userId = userId; this.relativeDir = relativeDir; this.flags = new BackupFlags(flags); } protected RestoreOpOptions(@NonNull Parcel in) { packageName = Objects.requireNonNull(in.readString()); userId = in.readInt(); relativeDir = in.readString(); flags = new BackupFlags(in.readInt()); } public static final Creator CREATOR = new Creator() { @Override @NonNull public RestoreOpOptions createFromParcel(@NonNull Parcel in) { return new RestoreOpOptions(in); } @Override @NonNull public RestoreOpOptions[] newArray(int size) { return new RestoreOpOptions[size]; } }; @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeString(packageName); dest.writeInt(userId); dest.writeString(relativeDir); dest.writeInt(this.flags.getFlags()); } public RestoreOpOptions(@NonNull JSONObject jsonObject) throws JSONException { packageName = jsonObject.getString("package_name"); userId = jsonObject.getInt("user_id"); relativeDir = JSONUtils.optString(jsonObject, "relative_dir"); flags = new BackupFlags(jsonObject.getInt("flags")); } @NonNull @Override public JSONObject serializeToJson() throws JSONException { JSONObject jsonObject = new JSONObject(); jsonObject.put("package_name", packageName); jsonObject.put("user_id", userId); jsonObject.put("relative_dir", relativeDir); jsonObject.put("flags", flags.getFlags()); return jsonObject; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/batchops/BatchOpsLogger.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.batchops; import androidx.annotation.NonNull; import java.io.File; import java.io.IOException; import io.github.muntashirakon.AppManager.logs.Logger; import io.github.muntashirakon.io.Paths; public class BatchOpsLogger extends Logger { private static final File LOG_FILE = new File(getLoggingDirectory(), "batch_ops.log"); protected BatchOpsLogger() throws IOException { super(LOG_FILE, false); } @NonNull public static String getAllLogs() { return Paths.get(LOG_FILE).getContentAsString(); } public static void clearLogs() { LOG_FILE.delete(); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/batchops/BatchOpsManager.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.batchops; import android.Manifest; import android.annotation.UserIdInt; import android.app.AppOpsManager; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.os.RemoteException; import android.os.UserHandleHidden; import androidx.annotation.CheckResult; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.WorkerThread; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.accessibility.AccessibilityMultiplexer; import io.github.muntashirakon.AppManager.apk.ApkUtils; import io.github.muntashirakon.AppManager.apk.dexopt.DexOptOptions; import io.github.muntashirakon.AppManager.apk.dexopt.DexOptimizer; import io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat; import io.github.muntashirakon.AppManager.backup.BackupException; import io.github.muntashirakon.AppManager.backup.BackupManager; import io.github.muntashirakon.AppManager.backup.convert.ConvertUtils; import io.github.muntashirakon.AppManager.backup.convert.Converter; import io.github.muntashirakon.AppManager.backup.dialog.BackupRestoreDialogFragment; import io.github.muntashirakon.AppManager.batchops.struct.BatchAppOpsOptions; import io.github.muntashirakon.AppManager.batchops.struct.BatchBackupImportOptions; import io.github.muntashirakon.AppManager.batchops.struct.BatchBackupOptions; import io.github.muntashirakon.AppManager.batchops.struct.BatchComponentOptions; import io.github.muntashirakon.AppManager.batchops.struct.BatchDexOptOptions; import io.github.muntashirakon.AppManager.batchops.struct.BatchFreezeOptions; import io.github.muntashirakon.AppManager.batchops.struct.BatchNetPolicyOptions; import io.github.muntashirakon.AppManager.batchops.struct.BatchPermissionOptions; import io.github.muntashirakon.AppManager.batchops.struct.IBatchOpOptions; import io.github.muntashirakon.AppManager.compat.AppOpsManagerCompat; import io.github.muntashirakon.AppManager.compat.NetworkPolicyManagerCompat; import io.github.muntashirakon.AppManager.compat.PackageManagerCompat; import io.github.muntashirakon.AppManager.compat.PermissionCompat; import io.github.muntashirakon.AppManager.compat.StorageManagerCompat; import io.github.muntashirakon.AppManager.logs.Logger; import io.github.muntashirakon.AppManager.progress.NotificationProgressHandler; import io.github.muntashirakon.AppManager.progress.NotificationProgressHandler.NotificationInfo; import io.github.muntashirakon.AppManager.progress.ProgressHandler; import io.github.muntashirakon.AppManager.rules.compontents.ComponentUtils; import io.github.muntashirakon.AppManager.rules.compontents.ComponentsBlocker; import io.github.muntashirakon.AppManager.rules.compontents.ExternalComponentsImporter; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.types.UserPackagePair; import io.github.muntashirakon.AppManager.utils.ArrayUtils; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.ExUtils; import io.github.muntashirakon.AppManager.utils.FreezeUtils; import io.github.muntashirakon.AppManager.utils.MultithreadedExecutor; import io.github.muntashirakon.AppManager.utils.PackageUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; @WorkerThread public class BatchOpsManager { public static final String TAG = "BatchOpsManager"; @IntDef(value = { OP_NONE, OP_ADVANCED_FREEZE, OP_BACKUP_APK, OP_BACKUP, OP_BLOCK_COMPONENTS, OP_BLOCK_TRACKERS, OP_CLEAR_CACHE, OP_CLEAR_DATA, OP_DELETE_BACKUP, OP_DEXOPT, OP_DISABLE_BACKGROUND, OP_EXPORT_RULES, OP_FORCE_STOP, OP_FREEZE, OP_GRANT_PERMISSIONS, OP_IMPORT_BACKUPS, OP_NET_POLICY, OP_REVOKE_PERMISSIONS, OP_RESTORE_BACKUP, OP_SET_APP_OPS, OP_UNBLOCK_COMPONENTS, OP_UNBLOCK_TRACKERS, OP_UNINSTALL, OP_UNFREEZE, }) @Retention(RetentionPolicy.SOURCE) public @interface OpType { } public static final int OP_NONE = -1; public static final int OP_BACKUP_APK = 0; public static final int OP_BACKUP = 1; public static final int OP_BLOCK_TRACKERS = 2; public static final int OP_CLEAR_DATA = 3; public static final int OP_DELETE_BACKUP = 4; public static final int OP_FREEZE = 5; public static final int OP_DISABLE_BACKGROUND = 6; public static final int OP_EXPORT_RULES = 7; public static final int OP_FORCE_STOP = 8; public static final int OP_RESTORE_BACKUP = 9; public static final int OP_UNBLOCK_TRACKERS = 10; public static final int OP_UNINSTALL = 11; public static final int OP_BLOCK_COMPONENTS = 12; public static final int OP_SET_APP_OPS = 13; public static final int OP_UNFREEZE = 14; public static final int OP_UNBLOCK_COMPONENTS = 15; public static final int OP_CLEAR_CACHE = 16; public static final int OP_GRANT_PERMISSIONS = 17; public static final int OP_REVOKE_PERMISSIONS = 18; public static final int OP_IMPORT_BACKUPS = 19; public static final int OP_NET_POLICY = 20; public static final int OP_DEXOPT = 21; public static final int OP_ADVANCED_FREEZE = 22; private static final String GROUP_ID = BuildConfig.APPLICATION_ID + ".notification_group.BATCH_OPS"; public static class BatchOpsInfo { @NonNull public static BatchOpsInfo fromQueue(@NonNull BatchQueueItem queueItem) { return new BatchOpsInfo(queueItem.getOp(), queueItem.getPackages(), queueItem.getUsers(), queueItem.getOptions()); } @NonNull public static BatchOpsInfo fromUserPackagePair(@OpType int op, @NonNull List pairs, @Nullable IBatchOpOptions options) { Result result = new Result(pairs); return new BatchOpsInfo(op, result.getFailedPackages(), result.getAssociatedUsers(), options); } @NonNull public static BatchOpsInfo getInstance(@OpType int op, @NonNull List packages, @NonNull List users, @Nullable IBatchOpOptions options) { return new BatchOpsInfo(op, packages, users, options); } @OpType public final int op; @NonNull public final List packages; @NonNull public final List users; @Nullable public final IBatchOpOptions options; private BatchOpsInfo( @OpType int op, @NonNull List packages, @NonNull List users, @Nullable IBatchOpOptions options) { this.op = op; this.packages = Collections.unmodifiableList(packages); this.users = Collections.unmodifiableList(users); this.options = options; assert packages.size() == users.size(); } public int size() { return packages.size(); } @NonNull public UserPackagePair getPair(int index) { return new UserPackagePair(packages.get(index), users.get(index)); } public List getPairList() { List userPackagePairs = new ArrayList<>(packages.size()); int size = size(); for (int i = 0; i < size; ++i) { userPackagePairs.add(getPair(i)); } return Collections.unmodifiableList(userPackagePairs); } } @Nullable public Logger mLogger; public final boolean mCustomLogger; @Nullable private ProgressHandler mProgressHandler; public BatchOpsManager() { mCustomLogger = false; mLogger = ExUtils.exceptionAsNull(BatchOpsLogger::new); } public BatchOpsManager(@Nullable Logger logger) { mLogger = logger; mCustomLogger = true; } public Result performOp(@NonNull BatchOpsInfo info, @Nullable ProgressHandler progressHandler) { mProgressHandler = progressHandler; return performOp(info); } @CheckResult @NonNull private Result performOp(@NonNull BatchOpsInfo info) { switch (info.op) { case OP_ADVANCED_FREEZE: return opFreeze(info); case OP_BACKUP_APK: return opBackupApk(info); case OP_BACKUP: return opBackupRestore(info, BackupRestoreDialogFragment.MODE_BACKUP); case OP_BLOCK_TRACKERS: return opBlockTrackers(info); case OP_CLEAR_DATA: return opClearData(info); case OP_DELETE_BACKUP: return opBackupRestore(info, BackupRestoreDialogFragment.MODE_DELETE); case OP_FREEZE: return opFreezeUnfreeze(info, true); case OP_DISABLE_BACKGROUND: return opDisableBackground(info); case OP_UNFREEZE: return opFreezeUnfreeze(info, false); case OP_EXPORT_RULES: break; // Done in the main activity case OP_FORCE_STOP: return opForceStop(info); case OP_RESTORE_BACKUP: return opBackupRestore(info, BackupRestoreDialogFragment.MODE_RESTORE); case OP_UNINSTALL: return opUninstall(info); case OP_UNBLOCK_TRACKERS: return opUnblockTrackers(info); case OP_BLOCK_COMPONENTS: return opBlockComponents(info); case OP_SET_APP_OPS: return opSetAppOps(info); case OP_UNBLOCK_COMPONENTS: return opUnblockComponents(info); case OP_CLEAR_CACHE: return opClearCache(info); case OP_GRANT_PERMISSIONS: return opGrantOrRevokePermissions(info, true); case OP_REVOKE_PERMISSIONS: return opGrantOrRevokePermissions(info, false); case OP_IMPORT_BACKUPS: return opImportBackups(info); case OP_NET_POLICY: return opNetPolicy(info); case OP_DEXOPT: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { return opPerformDexOpt(info); } return new Result(Collections.emptyList(), false); case OP_NONE: break; } return new Result(info.getPairList()); } public void conclude() { if (!mCustomLogger && mLogger != null) { mLogger.close(); } } @NonNull private Result opBackupApk(@NonNull BatchOpsInfo info) { List failedPackages = new ArrayList<>(); int max = info.size(); // Initial progress float lastProgress = mProgressHandler != null ? mProgressHandler.getLastProgress() : 0; Context context = ContextUtils.getContext(); UserPackagePair pair; for (int i = 0; i < max; ++i) { pair = info.getPair(i); updateProgress(lastProgress, i + 1); // Do operation try { ApkUtils.backupApk(context, pair.getPackageName(), pair.getUserId()); } catch (Exception e) { failedPackages.add(pair); log("====> op=BACKUP_APK, pkg=" + pair, e); } } return new Result(failedPackages); } @NonNull private Result opBackupRestore(@NonNull BatchOpsInfo info, @BackupRestoreDialogFragment.ActionMode int mode) { switch (mode) { case BackupRestoreDialogFragment.MODE_BACKUP: return backup(info); case BackupRestoreDialogFragment.MODE_RESTORE: return restoreBackups(info); case BackupRestoreDialogFragment.MODE_DELETE: return deleteBackups(info); } return new Result(info.getPairList()); } @NonNull private Result backup(@NonNull BatchOpsInfo info) { List failedPackages = Collections.synchronizedList(new ArrayList<>()); Context context = ContextUtils.getContext(); PackageManager pm = context.getPackageManager(); CharSequence operationName = context.getString(R.string.backup_restore); MultithreadedExecutor executor = MultithreadedExecutor.getNewInstance(); AtomicInteger counter = new AtomicInteger(0); float lastProgress = mProgressHandler != null ? mProgressHandler.getLastProgress() : 0; try { BatchBackupOptions options = Objects.requireNonNull((BatchBackupOptions) info.options); int max = info.size(); BackupManager backupManager = new BackupManager(); for (int i = 0; i < max; ++i) { UserPackagePair pair = info.getPair(i); executor.submit(() -> { synchronized (counter) { counter.set(counter.get() + 1); updateProgress(lastProgress, counter.get()); } CharSequence appLabel = PackageUtils.getPackageLabel(pm, pair.getPackageName(), pair.getUserId()); CharSequence title = context.getString(R.string.backing_up_app, appLabel); ProgressHandler subProgressHandler = newSubProgress(operationName, title); try { backupManager.backup(options.getBackupOpOptions(pair.getPackageName(), pair.getUserId()), subProgressHandler); } catch (BackupException e) { log("====> op=BACKUP_RESTORE, mode=BACKUP pkg=" + pair, e); failedPackages.add(pair); } if (subProgressHandler != null) { ThreadUtils.postOnMainThread(() -> subProgressHandler.onResult(null)); } }); } } catch (Throwable th) { log("====> op=BACKUP_RESTORE, mode=BACKUP", th); } executor.awaitCompletion(); return new Result(failedPackages); } @NonNull private Result restoreBackups(@NonNull BatchOpsInfo info) { List failedPackages = Collections.synchronizedList(new ArrayList<>()); Context context = ContextUtils.getContext(); PackageManager pm = context.getPackageManager(); CharSequence operationName = context.getString(R.string.backup_restore); MultithreadedExecutor executor = MultithreadedExecutor.getNewInstance(); AtomicBoolean requiresRestart = new AtomicBoolean(); AtomicInteger count = new AtomicInteger(0); float lastProgress = mProgressHandler != null ? mProgressHandler.getLastProgress() : 0; try { BatchBackupOptions options = Objects.requireNonNull((BatchBackupOptions) info.options); int max = info.size(); BackupManager backupManager = new BackupManager(); for (int i = 0; i < max; ++i) { UserPackagePair pair = info.getPair(i); executor.submit(() -> { synchronized (count) { count.set(count.get() + 1); updateProgress(lastProgress, count.get()); } CharSequence appLabel = PackageUtils.getPackageLabel(pm, pair.getPackageName(), pair.getUserId()); CharSequence title = context.getString(R.string.restoring_app, appLabel); ProgressHandler subProgressHandler = newSubProgress(operationName, title); try { backupManager.restore(options.getRestoreOpOptions(pair.getPackageName(), pair.getUserId()), subProgressHandler); requiresRestart.set(requiresRestart.get() | backupManager.requiresRestart()); } catch (Throwable e) { log("====> op=BACKUP_RESTORE, mode=RESTORE pkg=" + pair, e); failedPackages.add(pair); } if (subProgressHandler != null) { ThreadUtils.postOnMainThread(() -> subProgressHandler.onResult(null)); } }); } } catch (Throwable th) { log("====> op=BACKUP_RESTORE, mode=RESTORE", th); } executor.awaitCompletion(); Result result = new Result(failedPackages); result.setRequiresRestart(requiresRestart.get()); return result; } @NonNull private Result deleteBackups(@NonNull BatchOpsInfo info) { List failedPackages = new ArrayList<>(); float lastProgress = mProgressHandler != null ? mProgressHandler.getLastProgress() : 0; try { BatchBackupOptions options = Objects.requireNonNull((BatchBackupOptions) info.options); int max = info.size(); BackupManager backupManager = new BackupManager(); UserPackagePair pair; for (int i = 0; i < max; ++i) { updateProgress(lastProgress, i + 1); pair = info.getPair(i); try { backupManager.deleteBackup(options.getDeleteOpOptions(pair.getPackageName(), pair.getUserId())); } catch (BackupException e) { log("====> op=BACKUP_RESTORE, mode=DELETE pkg=" + pair, e); failedPackages.add(pair); } } } catch (Throwable th) { log("====> op=BACKUP_RESTORE, mode=DELETE", th); } return new Result(failedPackages); } @NonNull private Result opImportBackups(@NonNull BatchOpsInfo info) { final List failedPkgList = Collections.synchronizedList(new ArrayList<>()); MultithreadedExecutor executor = MultithreadedExecutor.getNewInstance(); try { int userId = UserHandleHidden.myUserId(); BatchBackupImportOptions options = (BatchBackupImportOptions) Objects.requireNonNull(info.options); Uri uri = options.getDirectory(); Path backupPath = Paths.get(uri); if (!backupPath.isDirectory()) { log("====> op=IMPORT_BACKUP, Not a directory."); return new Result(Collections.emptyList(), false); } Path[] files = ConvertUtils.getRelevantImportFiles(backupPath, options.getImportType()); fixProgress(files.length); float lastProgress = mProgressHandler != null ? mProgressHandler.getLastProgress() : 0; AtomicInteger i = new AtomicInteger(0); for (Path file : files) { executor.submit(() -> { synchronized (i) { i.set(i.get() + 1); updateProgress(lastProgress, i.get()); } Converter converter = ConvertUtils.getConversionUtil(options.getImportType(), file); try { converter.convert(); if (options.isRemoveImportedDirectory()) { // Since the conversion was successful, remove the files for it. converter.cleanup(); } } catch (BackupException e) { log("====> op=IMPORT_BACKUP, pkg=" + converter.getPackageName(), e); failedPkgList.add(new UserPackagePair(converter.getPackageName(), userId)); } }); } } catch (Throwable th) { log("====> op=IMPORT_BACKUP", th); } executor.awaitCompletion(); return new Result(failedPkgList); } @NonNull private Result opBlockComponents(@NonNull BatchOpsInfo info) { BatchComponentOptions options = (BatchComponentOptions) Objects.requireNonNull(info.options); List failedPackages = new ArrayList<>(); float lastProgress = mProgressHandler != null ? mProgressHandler.getLastProgress() : 0; int max = info.size(); UserPackagePair pair; for (int i = 0; i < max; ++i) { updateProgress(lastProgress, i + 1); pair = info.getPair(i); try { ComponentUtils.blockFilteredComponents(pair, options.getSignatures()); } catch (Exception e) { log("====> op=BLOCK_COMPONENTS, pkg=" + pair, e); failedPackages.add(pair); } } return new Result(failedPackages); } @NonNull private Result opBlockTrackers(@NonNull BatchOpsInfo info) { List failedPackages = new ArrayList<>(); float lastProgress = mProgressHandler != null ? mProgressHandler.getLastProgress() : 0; int max = info.size(); UserPackagePair pair; for (int i = 0; i < max; ++i) { updateProgress(lastProgress, i + 1); pair = info.getPair(i); try { ComponentUtils.blockTrackingComponents(pair); } catch (Exception e) { log("====> op=BLOCK_TRACKERS, pkg=" + pair, e); failedPackages.add(pair); } } return new Result(failedPackages); } @NonNull private Result opClearCache(@NonNull BatchOpsInfo info) { if (info.size() == 0) { // No packages supplied means trim all caches return opTrimCaches(); } List failedPackages = new ArrayList<>(); float lastProgress = mProgressHandler != null ? mProgressHandler.getLastProgress() : 0; int max = info.size(); UserPackagePair pair; for (int i = 0; i < max; ++i) { updateProgress(lastProgress, i + 1); pair = info.getPair(i); try { PackageManagerCompat.deleteApplicationCacheFilesAsUser(pair); } catch (Exception e) { log("====> op=CLEAR_CACHE, pkg=" + pair, e); failedPackages.add(pair); } } return new Result(failedPackages); } @NonNull private Result opTrimCaches() { long size = 1024L * 1024L * 1024L * 1024L; // 1 TB boolean isSuccessful; try { // TODO: 30/8/21 Iterate all volumes? PackageManagerCompat.freeStorageAndNotify(null /* internal */, size, StorageManagerCompat.FLAG_ALLOCATE_DEFY_ALL_RESERVED); isSuccessful = true; } catch (Throwable e) { log("====> op=TRIM_CACHES", e); isSuccessful = false; } return new Result(Collections.emptyList(), isSuccessful); } @NonNull private Result opClearData(@NonNull BatchOpsInfo info) { List failedPackages = new ArrayList<>(); float lastProgress = mProgressHandler != null ? mProgressHandler.getLastProgress() : 0; int max = info.size(); UserPackagePair pair; for (int i = 0; i < max; ++i) { updateProgress(lastProgress, i + 1); pair = info.getPair(i); try { PackageManagerCompat.clearApplicationUserData(pair); } catch (Exception e) { log("====> op=CLEAR_DATA, pkg=" + pair, e); failedPackages.add(pair); } } return new Result(failedPackages); } @NonNull private Result opFreeze(@NonNull BatchOpsInfo info) { BatchFreezeOptions options = (BatchFreezeOptions) Objects.requireNonNull(info.options); List failedPackages = new ArrayList<>(); float lastProgress = mProgressHandler != null ? mProgressHandler.getLastProgress() : 0; int max = info.size(); UserPackagePair pair; for (int i = 0; i < max; ++i) { updateProgress(lastProgress, i + 1); pair = info.getPair(i); int type; if (options.isPreferCustom()) { type = Optional.ofNullable(FreezeUtils.loadFreezeMethod(pair.getPackageName())) .orElse(options.getType()); } else type = options.getType(); try { FreezeUtils.freeze(pair.getPackageName(), pair.getUserId(), type); } catch (Throwable e) { log("====> op=ADVANCED_FREEZE, pkg=" + pair + ", type = " + type, e); failedPackages.add(pair); } } return new Result(failedPackages); } @NonNull private Result opFreezeUnfreeze(@NonNull BatchOpsInfo info, boolean freeze) { List failedPackages = new ArrayList<>(); float lastProgress = mProgressHandler != null ? mProgressHandler.getLastProgress() : 0; int max = info.size(); UserPackagePair pair; for (int i = 0; i < max; ++i) { updateProgress(lastProgress, i + 1); pair = info.getPair(i); try { if (freeze) { FreezeUtils.freeze(pair.getPackageName(), pair.getUserId()); } else { FreezeUtils.unfreeze(pair.getPackageName(), pair.getUserId()); } } catch (Throwable e) { log("====> op=APP_FREEZE, pkg=" + pair + ", freeze = " + freeze, e); failedPackages.add(pair); } } return new Result(failedPackages); } @NonNull private Result opDisableBackground(@NonNull BatchOpsInfo info) { List failedPackages = new ArrayList<>(); float lastProgress = mProgressHandler != null ? mProgressHandler.getLastProgress() : 0; AppOpsManagerCompat appOpsManager = new AppOpsManagerCompat(); int max = info.size(); UserPackagePair pair; for (int i = 0; i < max; ++i) { updateProgress(lastProgress, i + 1); pair = info.getPair(i); int uid = PackageUtils.getAppUid(pair); if (uid == -1) { failedPackages.add(pair); continue; } try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { appOpsManager.setMode(AppOpsManagerCompat.OP_RUN_IN_BACKGROUND, uid, pair.getPackageName(), AppOpsManager.MODE_IGNORED); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { appOpsManager.setMode(AppOpsManagerCompat.OP_RUN_ANY_IN_BACKGROUND, uid, pair.getPackageName(), AppOpsManager.MODE_IGNORED); } try (ComponentsBlocker cb = ComponentsBlocker.getMutableInstance(pair.getPackageName(), pair.getUserId())) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { cb.setAppOp(AppOpsManagerCompat.OP_RUN_IN_BACKGROUND, AppOpsManager.MODE_IGNORED); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { cb.setAppOp(AppOpsManagerCompat.OP_RUN_ANY_IN_BACKGROUND, AppOpsManager.MODE_IGNORED); } } } catch (Throwable e) { log("====> op=DISABLE_BACKGROUND, pkg=" + pair, e); failedPackages.add(pair); } } return new Result(failedPackages); } @NonNull private Result opGrantOrRevokePermissions(@NonNull BatchOpsInfo info, boolean isGrant) { BatchPermissionOptions options = (BatchPermissionOptions) Objects.requireNonNull(info.options); String[] permissions = options.getPermissions(); List failedPackages = new ArrayList<>(); float lastProgress = mProgressHandler != null ? mProgressHandler.getLastProgress() : 0; int max = info.size(); UserPackagePair pair; if (permissions.length == 1 && permissions[0].equals("*")) { // Wildcard detected for (int i = 0; i < max; ++i) { updateProgress(lastProgress, i + 1); pair = info.getPair(i); try { permissions = PackageUtils.getPermissionsForPackage(pair.getPackageName(), pair.getUserId()); if (permissions == null) continue; for (String permission : permissions) { if (isGrant) { PermissionCompat.grantPermission(pair.getPackageName(), permission, pair.getUserId()); } else { PermissionCompat.revokePermission(pair.getPackageName(), permission, pair.getUserId()); } } } catch (Throwable e) { log("====> op=GRANT_OR_REVOKE_PERMISSIONS, pkg=" + pair, e); failedPackages.add(pair); } } } else { for (int i = 0; i < max; ++i) { updateProgress(lastProgress, i + 1); pair = info.getPair(i); for (String permission : permissions) { try { if (isGrant) { PermissionCompat.grantPermission(pair.getPackageName(), permission, pair.getUserId()); } else { PermissionCompat.revokePermission(pair.getPackageName(), permission, pair.getUserId()); } } catch (Throwable e) { log("====> op=GRANT_OR_REVOKE_PERMISSIONS, pkg=" + pair, e); failedPackages.add(pair); } } } } return new Result(failedPackages); } @NonNull private Result opForceStop(@NonNull BatchOpsInfo info) { List failedPackages = new ArrayList<>(); float lastProgress = mProgressHandler != null ? mProgressHandler.getLastProgress() : 0; int max = info.size(); UserPackagePair pair; for (int i = 0; i < max; ++i) { updateProgress(lastProgress, i + 1); pair = info.getPair(i); try { PackageManagerCompat.forceStopPackage(pair.getPackageName(), pair.getUserId()); } catch (Throwable e) { log("====> op=FORCE_STOP, pkg=" + pair, e); failedPackages.add(pair); } } return new Result(failedPackages); } @NonNull private Result opNetPolicy(@NonNull BatchOpsInfo info) { List failedPackages = new ArrayList<>(); float lastProgress = mProgressHandler != null ? mProgressHandler.getLastProgress() : 0; BatchNetPolicyOptions options = (BatchNetPolicyOptions) Objects.requireNonNull(info.options); int max = info.size(); UserPackagePair pair; for (int i = 0; i < max; ++i) { updateProgress(lastProgress, i + 1); pair = info.getPair(i); try { int uid = PackageUtils.getAppUid(pair); NetworkPolicyManagerCompat.setUidPolicy(uid, options.getPolicies()); } catch (Throwable e) { log("====> op=NET_POLICY, pkg=" + pair, e); failedPackages.add(pair); } } return new Result(failedPackages); } @NonNull private Result opSetAppOps(@NonNull BatchOpsInfo info) { List failedPkgList = new ArrayList<>(); float lastProgress = mProgressHandler != null ? mProgressHandler.getLastProgress() : 0; AppOpsManagerCompat appOpsManager = new AppOpsManagerCompat(); BatchAppOpsOptions options = (BatchAppOpsOptions) Objects.requireNonNull(info.options); int[] appOps = options.getAppOps(); int max = info.size(); UserPackagePair pair; if (appOps.length == 1 && appOps[0] == AppOpsManagerCompat.OP_NONE) { // Wildcard detected for (int i = 0; i < max; ++i) { updateProgress(lastProgress, i + 1); pair = info.getPair(i); try { List appOpList = new ArrayList<>(); ApplicationInfo applicationInfo = PackageManagerCompat.getApplicationInfo(pair.getPackageName(), PackageManagerCompat.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES, pair.getUserId()); List entries = AppOpsManagerCompat.getConfiguredOpsForPackage( appOpsManager, applicationInfo.packageName, applicationInfo.uid); for (AppOpsManagerCompat.OpEntry entry : entries) { appOpList.add(entry.getOp()); } ExternalComponentsImporter.setModeToFilteredAppOps(appOpsManager, pair, ArrayUtils.convertToIntArray(appOpList), options.getMode()); } catch (Exception e) { log("====> op=SET_APP_OPS, pkg=" + pair, e); failedPkgList.add(pair); } } } else { for (int i = 0; i < max; ++i) { updateProgress(lastProgress, i + 1); pair = info.getPair(i); try { ExternalComponentsImporter.setModeToFilteredAppOps(appOpsManager, pair, appOps, options.getMode()); } catch (RemoteException e) { log("====> op=SET_APP_OPS, pkg=" + pair, e); failedPkgList.add(pair); } } } return new Result(failedPkgList); } @NonNull private Result opUnblockComponents(@NonNull BatchOpsInfo info) { List failedPackages = new ArrayList<>(); float lastProgress = mProgressHandler != null ? mProgressHandler.getLastProgress() : 0; BatchComponentOptions options = (BatchComponentOptions) Objects.requireNonNull(info.options); int max = info.size(); UserPackagePair pair; for (int i = 0; i < max; ++i) { updateProgress(lastProgress, i + 1); pair = info.getPair(i); try { ComponentUtils.unblockFilteredComponents(pair, options.getSignatures()); } catch (Throwable th) { log("====> op=UNBLOCK_COMPONENTS, pkg=" + pair, th); failedPackages.add(pair); } } return new Result(failedPackages); } @NonNull private Result opUnblockTrackers(@NonNull BatchOpsInfo info) { List failedPackages = new ArrayList<>(); float lastProgress = mProgressHandler != null ? mProgressHandler.getLastProgress() : 0; int max = info.size(); UserPackagePair pair; for (int i = 0; i < max; ++i) { updateProgress(lastProgress, i + 1); pair = info.getPair(i); try { ComponentUtils.unblockTrackingComponents(pair); } catch (Throwable th) { log("====> op=UNBLOCK_TRACKERS, pkg=" + pair, th); failedPackages.add(pair); } } return new Result(failedPackages); } @NonNull private Result opUninstall(@NonNull BatchOpsInfo info) { List failedPackages = new ArrayList<>(); float lastProgress = mProgressHandler != null ? mProgressHandler.getLastProgress() : 0; AccessibilityMultiplexer accessibility = AccessibilityMultiplexer.getInstance(); if (!SelfPermissions.checkSelfOrRemotePermission(Manifest.permission.DELETE_PACKAGES)) { // Try to use accessibility in unprivileged mode accessibility.enableUninstall(true); } int max = info.size(); UserPackagePair pair; for (int i = 0; i < max; ++i) { updateProgress(lastProgress, i + 1); pair = info.getPair(i); PackageInstallerCompat installer = PackageInstallerCompat.getNewInstance(); if (!installer.uninstall(pair.getPackageName(), pair.getUserId(), false)) { log("====> op=UNINSTALL, pkg=" + pair); failedPackages.add(pair); } } accessibility.enableUninstall(false); return new Result(failedPackages); } @RequiresApi(Build.VERSION_CODES.N) @NonNull private Result opPerformDexOpt(@NonNull BatchOpsInfo info) { List failedPackages = new ArrayList<>(); IPackageManager pm = PackageManagerCompat.getPackageManager(); DexOptOptions options = ((BatchDexOptOptions) Objects.requireNonNull(info.options)).getDexOptOptions(); if (info.size() > 0) { // Override options.packages with this list Set packages = new HashSet<>(info.size()); packages.addAll(info.packages); options.packages = packages.toArray(new String[0]); } else if (options.packages == null) { // Include all packages try { options.packages = pm.getAllPackages().toArray(new String[0]); } catch (RemoteException e) { log("====> op=DEXOPT", e); return new Result(failedPackages, false); } } fixProgress(options.packages.length); float lastProgress = mProgressHandler != null ? mProgressHandler.getLastProgress() : 0; int i = 0; for (String packageName : options.packages) { updateProgress(lastProgress, ++i); if (packageName.equals(BuildConfig.APPLICATION_ID)) { // Ignore App Manager continue; } DexOptimizer dexOptimizer = new DexOptimizer(pm, packageName); if (options.compilerFiler != null) { boolean result = true; if (options.clearProfileData) { result &= dexOptimizer.clearApplicationProfileData(); } result &= dexOptimizer.performDexOptMode(options.checkProfiles, options.compilerFiler, options.forceCompilation, options.bootComplete, null); if (!result) { log("====> op=DEXOPT, pkg=" + packageName + ", failed=dexopt-mode", dexOptimizer.getLastError()); failedPackages.add(new UserPackagePair(packageName, 0)); continue; } } if (options.compileLayouts && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { boolean result = true; if (options.clearProfileData) { result &= dexOptimizer.clearApplicationProfileData(); } result &= dexOptimizer.compileLayouts(); if (!result) { log("====> op=DEXOPT, pkg=" + packageName + ", failed=compile-layouts", dexOptimizer.getLastError()); failedPackages.add(new UserPackagePair(packageName, 0)); continue; } } if (options.forceDexOpt) { if (!dexOptimizer.forceDexOpt()) { log("====> op=DEXOPT, pkg=" + packageName + ", failed=force-dexopt", dexOptimizer.getLastError()); failedPackages.add(new UserPackagePair(packageName, 0)); } } } return new Result(failedPackages); } private void log(@Nullable String message, @Nullable Throwable th) { if (mLogger != null) { mLogger.println(message, th); } } private void log(@Nullable String message) { if (mLogger != null) { mLogger.println(message); } } private void updateProgress(float last, int current) { if (mProgressHandler == null) { return; } // Current progress = last progress + current mProgressHandler.postUpdate(last + current); } private void fixProgress(int appendMax) { if (mProgressHandler == null) { return; } int max = Math.max(mProgressHandler.getLastMax(), 0) + appendMax; float current = mProgressHandler.getLastProgress(); mProgressHandler.postUpdate(max, current); } @Nullable private ProgressHandler newSubProgress(@Nullable CharSequence operationName, @Nullable CharSequence title) { if (mProgressHandler == null) { return null; } Object message = mProgressHandler.getLastMessage(); if (message == null) { return null; } ProgressHandler p = mProgressHandler.newSubProgressHandler(); if (p instanceof NotificationProgressHandler) { NotificationInfo parentNotificationInfo = (NotificationInfo) message; NotificationInfo notificationInfo = new NotificationInfo(parentNotificationInfo) .setOperationName(operationName) .setTitle(title); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { notificationInfo.setGroupId(GROUP_ID); } ThreadUtils.postOnMainThread(() -> p.onProgressStart(-1, 0, notificationInfo)); } return p; } public static class Result { @NonNull private final ArrayList mFailedPackages; @NonNull private final ArrayList mAssociatedUsers; private final boolean mIsSuccessful; private boolean mRequiresRestart; public Result(@NonNull List failedUserPackagePairs) { this(failedUserPackagePairs, failedUserPackagePairs.isEmpty()); } public Result(@NonNull List failedUserPackagePairs, boolean isSuccessful) { mFailedPackages = new ArrayList<>(); mAssociatedUsers = new ArrayList<>(); for (UserPackagePair userPackagePair : failedUserPackagePairs) { mFailedPackages.add(userPackagePair.getPackageName()); mAssociatedUsers.add(userPackagePair.getUserId()); } mIsSuccessful = isSuccessful; } public boolean requiresRestart() { return mRequiresRestart; } public void setRequiresRestart(boolean requiresRestart) { mRequiresRestart = requiresRestart; } public boolean isSuccessful() { return mIsSuccessful; } @NonNull public ArrayList getFailedPackages() { return mFailedPackages; } @NonNull @UserIdInt public ArrayList getAssociatedUsers() { return mAssociatedUsers; } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/batchops/BatchOpsResultsActivity.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.batchops; import android.content.Intent; import android.graphics.Typeface; import android.os.Bundle; import android.text.SpannableString; import android.text.Spanned; import android.text.style.StyleSpan; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatEditText; import androidx.core.content.ContextCompat; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.button.MaterialButton; import java.util.ArrayList; import java.util.Collections; import java.util.List; import io.github.muntashirakon.AppManager.BaseActivity; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.intercept.IntentCompat; import io.github.muntashirakon.AppManager.utils.PackageUtils; import io.github.muntashirakon.AppManager.utils.RestartUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.util.AccessibilityUtils; public class BatchOpsResultsActivity extends BaseActivity { private RecyclerView mRecyclerView; private AppCompatEditText mLogViewer; @Nullable private BatchQueueItem mBatchQueueItem; @Override protected void onAuthenticated(@Nullable Bundle savedInstanceState) { if (getIntent() == null) { finish(); return; } if (restartIfNeeded(getIntent())) { return; } setContentView(R.layout.activity_batch_ops_results); setSupportActionBar(findViewById(R.id.toolbar)); findViewById(R.id.progress_linear).setVisibility(View.GONE); mRecyclerView = findViewById(R.id.list); mRecyclerView.setLayoutManager(UIUtils.getGridLayoutAt450Dp(this)); MaterialButton logToggler = findViewById(R.id.action_view_logs); mLogViewer = findViewById(R.id.text); mLogViewer.setKeyListener(null); logToggler.setOnClickListener(v -> { mLogViewer.setVisibility(View.VISIBLE); AccessibilityUtils.requestAccessibilityFocus(mLogViewer); }); handleIntent(getIntent()); } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { outState.clear(); super.onSaveInstanceState(outState); } @Override protected void onNewIntent(@NonNull Intent intent) { super.onNewIntent(intent); if (restartIfNeeded(getIntent())) { return; } handleIntent(intent); } private void handleIntent(@NonNull Intent intent) { mBatchQueueItem = IntentCompat.getUnwrappedParcelableExtra(intent, BatchOpsService.EXTRA_QUEUE_ITEM, BatchQueueItem.class); if (mBatchQueueItem == null) { finish(); return; } setTitle(intent.getStringExtra(BatchOpsService.EXTRA_FAILURE_MESSAGE)); ArrayList packageLabels = PackageUtils.packagesToAppLabels(getPackageManager(), mBatchQueueItem.getPackages(), mBatchQueueItem.getUsers()); RecyclerAdapter adapter = new RecyclerAdapter(packageLabels); mRecyclerView.setAdapter(adapter); if (packageLabels != null) { adapter.notifyItemRangeInserted(0, packageLabels.size()); } mLogViewer.setText(getFormattedLogs(BatchOpsLogger.getAllLogs())); intent.removeExtra(BatchOpsService.EXTRA_QUEUE_ITEM); } private static boolean restartIfNeeded(@NonNull Intent intent) { if (intent.getBooleanExtra(BatchOpsService.EXTRA_REQUIRES_RESTART, false)) { RestartUtils.restart(RestartUtils.RESTART_NORMAL); return true; } return false; } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.activity_batch_ops_results_actions, menu); return super.onCreateOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int id = item.getItemId(); if (id == android.R.id.home) { finish(); return true; } else if (id == R.id.action_retry) { if (mBatchQueueItem != null) { Intent BatchOpsIntent = BatchOpsService.getServiceIntent(this, mBatchQueueItem); ContextCompat.startForegroundService(this, BatchOpsIntent); } } return super.onOptionsItemSelected(item); } @Override protected void onDestroy() { BatchOpsLogger.clearLogs(); super.onDestroy(); } public CharSequence getFormattedLogs(String logs) { SpannableString str = new SpannableString(logs); int fIndex = 0; while(true) { fIndex = logs.indexOf("====> ", fIndex); if (fIndex == -1) { return str; } int lIndex = logs.indexOf('\n', fIndex); str.setSpan(new StyleSpan(Typeface.BOLD), fIndex, lIndex, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); fIndex = lIndex; } } static class RecyclerAdapter extends RecyclerView.Adapter { @NonNull private final List mAppLabels; public RecyclerAdapter(@Nullable List appLabels) { mAppLabels = appLabels == null ? Collections.emptyList() : appLabels; } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(android.R.layout.simple_list_item_1, parent, false); return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { holder.itemView.setText(mAppLabels.get(position)); } @Override public int getItemCount() { return mAppLabels.size(); } static class ViewHolder extends RecyclerView.ViewHolder { TextView itemView; public ViewHolder(@NonNull View itemView) { super(itemView); this.itemView = (TextView) itemView; } } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/batchops/BatchOpsService.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.batchops; import static io.github.muntashirakon.AppManager.history.ops.OpHistoryManager.HISTORY_TYPE_BATCH_OPS; import android.app.Activity; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.os.Build; import android.os.PowerManager; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.NotificationManagerCompat; import androidx.core.app.PendingIntentCompat; import androidx.core.app.ServiceCompat; import java.util.ArrayList; import java.util.Objects; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.batchops.BatchOpsManager.BatchOpsInfo; import io.github.muntashirakon.AppManager.history.ops.OpHistoryManager; import io.github.muntashirakon.AppManager.intercept.IntentCompat; import io.github.muntashirakon.AppManager.main.MainActivity; import io.github.muntashirakon.AppManager.progress.NotificationProgressHandler; import io.github.muntashirakon.AppManager.progress.NotificationProgressHandler.NotificationManagerInfo; import io.github.muntashirakon.AppManager.progress.ProgressHandler; import io.github.muntashirakon.AppManager.progress.QueuedProgressHandler; import io.github.muntashirakon.AppManager.types.ForegroundService; import io.github.muntashirakon.AppManager.utils.CpuUtils; import io.github.muntashirakon.AppManager.utils.NotificationUtils; public class BatchOpsService extends ForegroundService { public static final String EXTRA_QUEUE_ITEM = "queue_item"; /** * Name of the batch operation, {@link Integer} value. One of the {@link BatchOpsManager.OpType}. */ public static final String EXTRA_OP = "EXTRA_OP"; /** * An {@link ArrayList} of package names (string value) on which operations will be carried out. */ public static final String EXTRA_OP_PKG = "EXTRA_OP_PKG"; /** * An {@link ArrayList} of package name (string value) which are failed after the batch * operation is complete. */ public static final String EXTRA_FAILED_PKG = "EXTRA_FAILED_PKG_ARR"; /** * The failure message. */ public static final String EXTRA_FAILURE_MESSAGE = "EXTRA_FAILURE_MESSAGE"; /** * Boolean value to describe whether a reboot is required. */ public static final String EXTRA_REQUIRES_RESTART = "requires_restart"; /** * Send to the appropriate broadcast receiver denoting that the batch operation is completed. It * includes the following extras: *
    *
  • * {@link #EXTRA_OP} is the integer value denoting the type of operation. *
  • *
  • * {@link #EXTRA_OP_PKG} is the array of packages on which operations were carried out. Never null. *
  • *
  • * {@link #EXTRA_FAILED_PKG} is the array of failed packages. Never null. *
  • *
*/ public static final String ACTION_BATCH_OPS_COMPLETED = BuildConfig.APPLICATION_ID + ".action.BATCH_OPS_COMPLETED"; public static final String ACTION_BATCH_OPS_STARTED = BuildConfig.APPLICATION_ID + ".action.BATCH_OPS_STARTED"; /** * Notification channel ID */ public static final String CHANNEL_ID = BuildConfig.APPLICATION_ID + ".channel.BATCH_OPS"; @NonNull public static Intent getServiceIntent(@NonNull Context context, @NonNull BatchQueueItem queueItem) { Intent intent = new Intent(context, BatchOpsService.class); IntentCompat.putWrappedParcelableExtra(intent, EXTRA_QUEUE_ITEM, queueItem); return intent; } private QueuedProgressHandler mProgressHandler; private NotificationProgressHandler.NotificationInfo mNotificationInfo; private PowerManager.WakeLock mWakeLock; public BatchOpsService() { super("BatchOpsService"); } @Override public void onCreate() { super.onCreate(); mWakeLock = CpuUtils.getPartialWakeLock("batch_ops"); mWakeLock.acquire(); } @Override public int onStartCommand(@Nullable Intent intent, int flags, int startId) { if (isWorking()) return super.onStartCommand(intent, flags, startId); BatchQueueItem item = getQueueItem(intent); NotificationManagerInfo notificationManagerInfo = new NotificationManagerInfo(CHANNEL_ID, "Batch Ops Progress", NotificationManagerCompat.IMPORTANCE_LOW); mProgressHandler = new NotificationProgressHandler(this, notificationManagerInfo, NotificationUtils.HIGH_PRIORITY_NOTIFICATION_INFO, NotificationUtils.HIGH_PRIORITY_NOTIFICATION_INFO); mProgressHandler.setProgressTextInterface(ProgressHandler.PROGRESS_REGULAR); Intent notificationIntent = new Intent(this, MainActivity.class); PendingIntent pendingIntent = PendingIntentCompat.getActivity(this, 0, notificationIntent, 0, false); mNotificationInfo = new NotificationProgressHandler.NotificationInfo() .setOperationName(getHeader(item)) .setBody(getString(R.string.operation_running)) .setDefaultAction(pendingIntent); mProgressHandler.onAttach(this, mNotificationInfo); return super.onStartCommand(intent, flags, startId); } @Override protected void onHandleIntent(@Nullable Intent intent) { BatchQueueItem item = getQueueItem(intent); if (item == null || item.getOp() == BatchOpsManager.OP_NONE) { sendResults(Activity.RESULT_CANCELED, item, null); return; } sendStarted(item); // Update progress if (mProgressHandler != null) { mProgressHandler.postUpdate(item.getPackages().size(), 0); } BatchOpsManager batchOpsManager = new BatchOpsManager(); BatchOpsManager.Result result = batchOpsManager.performOp(BatchOpsInfo.fromQueue(item), mProgressHandler); batchOpsManager.conclude(); OpHistoryManager.addHistoryItem(HISTORY_TYPE_BATCH_OPS, item, result.isSuccessful()); if (result.isSuccessful()) { sendResults(Activity.RESULT_OK, item, result); } else { sendResults(Activity.RESULT_FIRST_USER, item, result); } } @Override protected void onQueued(@Nullable Intent intent) { BatchQueueItem item = getQueueItem(intent); if (item == null) { return; } String opTitle = getDesiredOpTitle(this, item.getOp()); Object notificationInfo = new NotificationProgressHandler.NotificationInfo() .setAutoCancel(true) .setTime(System.currentTimeMillis()) .setOperationName(getHeader(item)) .setTitle(opTitle) .setBody(getString(R.string.added_to_queue)); mProgressHandler.onQueue(notificationInfo); } @Override protected void onStartIntent(@Nullable Intent intent) { BatchQueueItem item = getQueueItem(intent); int op = item != null ? item.getOp() : BatchOpsManager.OP_NONE; mNotificationInfo.setTitle(getDesiredOpTitle(this, op)).setOperationName(getHeader(item)); mProgressHandler.onProgressStart(-1, 0, mNotificationInfo); } @Override public void onDestroy() { ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE); if (mProgressHandler != null) { mProgressHandler.onDetach(this); } CpuUtils.releaseWakeLock(mWakeLock); super.onDestroy(); } @Nullable private BatchQueueItem getQueueItem(@Nullable Intent intent) { if (intent == null) { return null; } return IntentCompat.getUnwrappedParcelableExtra(intent, EXTRA_QUEUE_ITEM, BatchQueueItem.class); } private void sendStarted(@NonNull BatchQueueItem queueItem) { Intent broadcastIntent = new Intent(ACTION_BATCH_OPS_STARTED); broadcastIntent.setPackage(getPackageName()); broadcastIntent.putExtra(EXTRA_OP, queueItem.getOp()); broadcastIntent.putExtra(EXTRA_OP_PKG, queueItem.getPackages().toArray(new String[0])); sendBroadcast(broadcastIntent); } private void sendResults(int result, @Nullable BatchQueueItem queueItem, @Nullable BatchOpsManager.Result opResult) { Intent broadcastIntent = new Intent(ACTION_BATCH_OPS_COMPLETED); broadcastIntent.setPackage(getPackageName()); broadcastIntent.putExtra(EXTRA_OP, queueItem != null ? queueItem.getOp() : BatchOpsManager.OP_NONE); broadcastIntent.putExtra(EXTRA_OP_PKG, queueItem != null ? queueItem.getPackages().toArray(new String[0]) : new String[0]); broadcastIntent.putStringArrayListExtra(EXTRA_FAILED_PKG, opResult != null ? opResult.getFailedPackages() : null); sendBroadcast(broadcastIntent); sendNotification(result, queueItem, opResult); } private void sendNotification(int result, @Nullable BatchQueueItem queueItem, @Nullable BatchOpsManager.Result opResult) { String contentTitle = getDesiredOpTitle(this, queueItem != null ? queueItem.getOp() : BatchOpsManager.OP_NONE); NotificationProgressHandler.NotificationInfo notificationInfo = new NotificationProgressHandler.NotificationInfo() .setAutoCancel(true) .setTime(System.currentTimeMillis()) .setOperationName(getHeader(queueItem)) .setTitle(contentTitle); switch (result) { case Activity.RESULT_CANCELED: // Cancelled break; case Activity.RESULT_OK: // Successful notificationInfo.setBody(getString(R.string.the_operation_was_successful)); break; case Activity.RESULT_FIRST_USER: // Failed Objects.requireNonNull(opResult); Objects.requireNonNull(queueItem); queueItem.setPackages(opResult.getFailedPackages()); queueItem.setUsers(opResult.getAssociatedUsers()); String detailsMessage = getString(R.string.full_stop_tap_to_see_details); String message = getDesiredErrorString(queueItem.getOp(), opResult.getFailedPackages().size()); Intent intent = new Intent(this, BatchOpsResultsActivity.class); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { intent.setIdentifier(String.valueOf(System.currentTimeMillis())); } intent.putExtra(EXTRA_FAILURE_MESSAGE, message); IntentCompat.putWrappedParcelableExtra(intent, EXTRA_QUEUE_ITEM, queueItem); PendingIntent pendingIntent = PendingIntentCompat.getActivity(this, 0, intent, PendingIntent.FLAG_ONE_SHOT, false); notificationInfo.setDefaultAction(pendingIntent); notificationInfo.setBody(message + detailsMessage); } if (opResult != null && opResult.requiresRestart()) { Intent intent = new Intent(this, BatchOpsResultsActivity.class); intent.putExtra(EXTRA_REQUIRES_RESTART, true); PendingIntent pendingIntent = PendingIntentCompat.getActivity(this, 0, intent, PendingIntent.FLAG_ONE_SHOT, false); notificationInfo.addAction(0, getString(R.string.restart_device), pendingIntent); } mProgressHandler.onResult(notificationInfo); } @NonNull public String getHeader(@Nullable BatchQueueItem item) { if (item != null) { String title = item.getTitle(); if (title != null) { return title; } } return getString(R.string.batch_ops); } @NonNull public static String getDesiredOpTitle(@NonNull Context context, @BatchOpsManager.OpType int op) { switch (op) { case BatchOpsManager.OP_BACKUP: case BatchOpsManager.OP_DELETE_BACKUP: case BatchOpsManager.OP_RESTORE_BACKUP: return context.getString(R.string.backup_restore); case BatchOpsManager.OP_BACKUP_APK: return context.getString(R.string.save_apk); case BatchOpsManager.OP_BLOCK_TRACKERS: return context.getString(R.string.block_trackers); case BatchOpsManager.OP_CLEAR_DATA: return context.getString(R.string.clear_data); case BatchOpsManager.OP_CLEAR_CACHE: return context.getString(R.string.clear_cache); case BatchOpsManager.OP_FREEZE: case BatchOpsManager.OP_ADVANCED_FREEZE: return context.getString(R.string.freeze); case BatchOpsManager.OP_DISABLE_BACKGROUND: return context.getString(R.string.disable_background); case BatchOpsManager.OP_UNFREEZE: return context.getString(R.string.unfreeze); case BatchOpsManager.OP_EXPORT_RULES: return context.getString(R.string.export_blocking_rules); case BatchOpsManager.OP_FORCE_STOP: return context.getString(R.string.force_stop); case BatchOpsManager.OP_NET_POLICY: return context.getString(R.string.net_policy); case BatchOpsManager.OP_UNINSTALL: return context.getString(R.string.uninstall); case BatchOpsManager.OP_UNBLOCK_TRACKERS: return context.getString(R.string.unblock_trackers); case BatchOpsManager.OP_BLOCK_COMPONENTS: return context.getString(R.string.block_components_dots); case BatchOpsManager.OP_UNBLOCK_COMPONENTS: return context.getString(R.string.unblock_components_dots); case BatchOpsManager.OP_SET_APP_OPS: return context.getString(R.string.set_mode_for_app_ops_dots); case BatchOpsManager.OP_IMPORT_BACKUPS: return context.getString(R.string.pref_import_backups); case BatchOpsManager.OP_DEXOPT: return context.getString(R.string.batch_ops_runtime_optimization); case BatchOpsManager.OP_NONE: break; } return context.getString(R.string.batch_ops); } private String getDesiredErrorString(int op, int failedCount) { switch (op) { case BatchOpsManager.OP_BACKUP: return getResources().getQuantityString(R.plurals.alert_failed_to_backup, failedCount, failedCount); case BatchOpsManager.OP_DELETE_BACKUP: return getResources().getQuantityString(R.plurals.alert_failed_to_delete_backup, failedCount, failedCount); case BatchOpsManager.OP_RESTORE_BACKUP: return getResources().getQuantityString(R.plurals.alert_failed_to_restore, failedCount, failedCount); case BatchOpsManager.OP_EXPORT_RULES: case BatchOpsManager.OP_NONE: break; case BatchOpsManager.OP_BACKUP_APK: return getResources().getQuantityString(R.plurals.failed_to_backup_some_apk_files, failedCount, failedCount); case BatchOpsManager.OP_BLOCK_TRACKERS: return getResources().getQuantityString(R.plurals.alert_failed_to_disable_trackers, failedCount, failedCount); case BatchOpsManager.OP_CLEAR_DATA: return getResources().getQuantityString(R.plurals.alert_failed_to_clear_data, failedCount, failedCount); case BatchOpsManager.OP_FREEZE: case BatchOpsManager.OP_ADVANCED_FREEZE: return getResources().getQuantityString(R.plurals.alert_failed_to_freeze, failedCount, failedCount); case BatchOpsManager.OP_UNFREEZE: return getResources().getQuantityString(R.plurals.alert_failed_to_unfreeze, failedCount, failedCount); case BatchOpsManager.OP_DISABLE_BACKGROUND: return getResources().getQuantityString(R.plurals.alert_failed_to_disable_background, failedCount, failedCount); case BatchOpsManager.OP_FORCE_STOP: return getResources().getQuantityString(R.plurals.alert_failed_to_force_stop, failedCount, failedCount); case BatchOpsManager.OP_UNINSTALL: return getResources().getQuantityString(R.plurals.alert_failed_to_uninstall, failedCount, failedCount); case BatchOpsManager.OP_UNBLOCK_TRACKERS: return getResources().getQuantityString(R.plurals.alert_failed_to_unblock_trackers, failedCount, failedCount); case BatchOpsManager.OP_BLOCK_COMPONENTS: return getResources().getQuantityString(R.plurals.alert_failed_to_block_components, failedCount, failedCount); case BatchOpsManager.OP_UNBLOCK_COMPONENTS: return getResources().getQuantityString(R.plurals.alert_failed_to_unblock_components, failedCount, failedCount); case BatchOpsManager.OP_SET_APP_OPS: return getResources().getQuantityString(R.plurals.alert_failed_to_set_app_ops, failedCount, failedCount); case BatchOpsManager.OP_IMPORT_BACKUPS: return getResources().getQuantityString(R.plurals.alert_failed_to_import_backups, failedCount, failedCount); case BatchOpsManager.OP_DEXOPT: return getResources().getQuantityString(R.plurals.alert_failed_to_optimize_apps, failedCount, failedCount); } return getString(R.string.error); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/batchops/BatchQueueItem.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.batchops; import android.content.res.Resources; import android.os.Parcel; import android.os.Parcelable; import android.os.UserHandleHidden; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.core.os.ParcelCompat; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.Objects; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.batchops.BatchOpsManager.OpType; import io.github.muntashirakon.AppManager.batchops.struct.IBatchOpOptions; import io.github.muntashirakon.AppManager.history.IJsonSerializer; import io.github.muntashirakon.AppManager.history.JsonDeserializer; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.JSONUtils; import io.github.muntashirakon.util.ParcelUtils; public class BatchQueueItem implements Parcelable, IJsonSerializer { @NonNull public static BatchQueueItem getBatchOpQueue(@OpType int op, @Nullable ArrayList packages, @Nullable ArrayList users, @Nullable IBatchOpOptions options) { return new BatchQueueItem(R.string.batch_ops, op, packages, users, options); } @NonNull public static BatchQueueItem getOneClickQueue(@OpType int op, @Nullable ArrayList packages, @Nullable ArrayList users, @Nullable IBatchOpOptions args) { return new BatchQueueItem(R.string.one_click_ops, op, packages, users, args); } @StringRes private final int mTitleRes; @OpType private final int mOp; @NonNull private ArrayList mPackages; @Nullable private ArrayList mUsers; @Nullable private final IBatchOpOptions mOptions; private BatchQueueItem(@StringRes int titleRes, @OpType int op, @Nullable ArrayList packages, @Nullable ArrayList users, @Nullable IBatchOpOptions options) { mTitleRes = titleRes; mOp = op; mPackages = packages != null ? packages : new ArrayList<>(0); mUsers = users; mOptions = options; } @StringRes public int getTitleRes() { return mTitleRes; } @Nullable public String getTitle() { try { return ContextUtils.getContext().getString(mTitleRes); } catch (Resources.NotFoundException e) { // This resource may not always be found return null; } } public int getOp() { return mOp; } @NonNull public ArrayList getPackages() { return mPackages; } public void setPackages(@NonNull ArrayList packages) { mPackages = packages; } @NonNull public ArrayList getUsers() { if (mUsers == null) { int size = mPackages.size(); int userId = UserHandleHidden.myUserId(); mUsers = new ArrayList<>(size); for (int i = 0; i < size; ++i) { mUsers.add(userId); } } else { assert mPackages.size() == mUsers.size(); } return mUsers; } public void setUsers(@Nullable ArrayList users) { mUsers = users; } @Nullable public IBatchOpOptions getOptions() { return mOptions; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeInt(mTitleRes); dest.writeInt(mOp); dest.writeStringList(mPackages); ParcelUtils.writeArrayList(mUsers, dest); dest.writeParcelable(mOptions, flags); } protected BatchQueueItem(@NonNull JSONObject jsonObject) throws JSONException { mTitleRes = jsonObject.getInt("title_res"); mOp = jsonObject.getInt("op"); mPackages = JSONUtils.getArray(jsonObject.getJSONArray("packages")); mUsers = JSONUtils.getArray(jsonObject.getJSONArray("users")); JSONObject options = jsonObject.optJSONObject("options"); mOptions = options != null ? IBatchOpOptions.DESERIALIZER.deserialize(options) : null; } @NonNull @Override public JSONObject serializeToJson() throws JSONException { JSONObject jsonObject = new JSONObject(); jsonObject.put("title_res", mTitleRes); jsonObject.put("op", mOp); jsonObject.put("packages", JSONUtils.getJSONArray(mPackages)); jsonObject.put("users", JSONUtils.getJSONArray(mUsers)); jsonObject.put("options", mOptions != null ? mOptions.serializeToJson() : null); return jsonObject; } protected BatchQueueItem(@NonNull Parcel in) { mTitleRes = in.readInt(); mOp = in.readInt(); mPackages = Objects.requireNonNull(in.createStringArrayList()); mUsers = ParcelUtils.readArrayList(in, Integer.class.getClassLoader()); mOptions = ParcelCompat.readParcelable(in, IBatchOpOptions.class.getClassLoader(), IBatchOpOptions.class); } public static final JsonDeserializer.Creator DESERIALIZER = BatchQueueItem::new; public static final Creator CREATOR = new Creator() { @NonNull @Override public BatchQueueItem createFromParcel(@NonNull Parcel in) { return new BatchQueueItem(in); } @NonNull @Override public BatchQueueItem[] newArray(int size) { return new BatchQueueItem[size]; } }; } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/batchops/struct/BatchAppOpsOptions.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.batchops.struct; import android.os.Parcel; import androidx.annotation.NonNull; import org.json.JSONException; import org.json.JSONObject; import java.util.Objects; import io.github.muntashirakon.AppManager.history.JsonDeserializer; import io.github.muntashirakon.AppManager.utils.JSONUtils; public class BatchAppOpsOptions implements IBatchOpOptions { public static final String TAG = BatchAppOpsOptions.class.getSimpleName(); @NonNull private int[] mAppOps; private int mMode; public BatchAppOpsOptions(@NonNull int[] appOps, int mode) { mAppOps = appOps; mMode = mode; } @NonNull public int[] getAppOps() { return mAppOps; } public int getMode() { return mMode; } protected BatchAppOpsOptions(@NonNull Parcel in) { mAppOps = Objects.requireNonNull(in.createIntArray()); mMode = in.readInt(); } public static final Creator CREATOR = new Creator() { @Override @NonNull public BatchAppOpsOptions createFromParcel(@NonNull Parcel in) { return new BatchAppOpsOptions(in); } @Override @NonNull public BatchAppOpsOptions[] newArray(int size) { return new BatchAppOpsOptions[size]; } }; @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeIntArray(mAppOps); dest.writeInt(mMode); } protected BatchAppOpsOptions(@NonNull JSONObject jsonObject) throws JSONException { assert jsonObject.getString("tag").equals(TAG); mAppOps = JSONUtils.getIntArray(jsonObject.getJSONArray("app_ops")); mMode = jsonObject.getInt("mode"); } public static final JsonDeserializer.Creator DESERIALIZER = BatchAppOpsOptions::new; @NonNull @Override public JSONObject serializeToJson() throws JSONException { JSONObject jsonObject = new JSONObject(); jsonObject.put("tag", TAG); jsonObject.put("app_ops", JSONUtils.getJSONArray(mAppOps)); jsonObject.put("mode", mMode); return jsonObject; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/batchops/struct/BatchBackupImportOptions.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.batchops.struct; import android.net.Uri; import android.os.Parcel; import androidx.annotation.NonNull; import androidx.core.os.ParcelCompat; import org.json.JSONException; import org.json.JSONObject; import java.util.Objects; import io.github.muntashirakon.AppManager.backup.convert.ImportType; import io.github.muntashirakon.AppManager.history.JsonDeserializer; public class BatchBackupImportOptions implements IBatchOpOptions { public static final String TAG = BatchBackupImportOptions.class.getSimpleName(); @ImportType private int mImportType; @NonNull private Uri mDirectory; private boolean mRemoveImportedDirectory; public BatchBackupImportOptions(@ImportType int importType, @NonNull Uri directory, boolean removeImportedDirectory) { mImportType = importType; mDirectory = directory; mRemoveImportedDirectory = removeImportedDirectory; } @ImportType public int getImportType() { return mImportType; } @NonNull public Uri getDirectory() { return mDirectory; } public boolean isRemoveImportedDirectory() { return mRemoveImportedDirectory; } protected BatchBackupImportOptions(@NonNull Parcel in) { mImportType = in.readInt(); mDirectory = Objects.requireNonNull(ParcelCompat.readParcelable(in, Uri.class.getClassLoader(), Uri.class)); mRemoveImportedDirectory = in.readByte() != 0; } public static final Creator CREATOR = new Creator() { @Override @NonNull public BatchBackupImportOptions createFromParcel(@NonNull Parcel in) { return new BatchBackupImportOptions(in); } @Override @NonNull public BatchBackupImportOptions[] newArray(int size) { return new BatchBackupImportOptions[size]; } }; @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeInt(mImportType); dest.writeParcelable(mDirectory, flags); dest.writeByte((byte) (mRemoveImportedDirectory ? 1 : 0)); } protected BatchBackupImportOptions(@NonNull JSONObject jsonObject) throws JSONException { assert jsonObject.getString("tag").equals(TAG); mImportType = jsonObject.getInt("import_type"); mDirectory = Uri.parse(jsonObject.getString("directory")); mRemoveImportedDirectory = jsonObject.getBoolean("remove_imported_directory"); } public static final JsonDeserializer.Creator DESERIALIZER = BatchBackupImportOptions::new; @NonNull @Override public JSONObject serializeToJson() throws JSONException { JSONObject jsonObject = new JSONObject(); jsonObject.put("tag", TAG); jsonObject.put("import_type", mImportType); jsonObject.put("directory", mDirectory.toString()); jsonObject.put("remove_imported_directory", mRemoveImportedDirectory); return jsonObject; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/batchops/struct/BatchBackupOptions.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.batchops.struct; import android.annotation.UserIdInt; import android.os.Parcel; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.json.JSONException; import org.json.JSONObject; import io.github.muntashirakon.AppManager.backup.BackupFlags; import io.github.muntashirakon.AppManager.backup.BackupUtils; import io.github.muntashirakon.AppManager.backup.struct.BackupOpOptions; import io.github.muntashirakon.AppManager.backup.struct.DeleteOpOptions; import io.github.muntashirakon.AppManager.backup.struct.RestoreOpOptions; import io.github.muntashirakon.AppManager.db.entity.Backup; import io.github.muntashirakon.AppManager.history.JsonDeserializer; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.DateUtils; import io.github.muntashirakon.AppManager.utils.JSONUtils; public class BatchBackupOptions implements IBatchOpOptions { public static final String TAG = BatchBackupOptions.class.getSimpleName(); @BackupFlags.BackupFlag private final int mFlags; @Nullable private final String[] mBackupNames; @Nullable private final String[] mRelativeDirs; public BatchBackupOptions(@BackupFlags.BackupFlag int flags, @Nullable String[] backupNames, @Nullable String[] relativeDirs) { mFlags = flags; mBackupNames = backupNames; mRelativeDirs = relativeDirs; } public BackupOpOptions getBackupOpOptions(@NonNull String packageName, @UserIdInt int userId) { String backupName; boolean customBackup = (mFlags & BackupFlags.BACKUP_MULTIPLE) != 0; if (mBackupNames != null && mBackupNames.length > 0) { backupName = mBackupNames[0]; } else { backupName = customBackup ? DateUtils.formatMediumDateTime(ContextUtils.getContext(), System.currentTimeMillis()) : null; } return new BackupOpOptions(packageName, userId, mFlags, backupName, !customBackup); } public RestoreOpOptions getRestoreOpOptions(@NonNull String packageName, @UserIdInt int userId) { // For restore operation, backup names (v4) and relative dirs are only set for single // package backups. In all other cases, it only uses base backups. String relativeDir; if (mRelativeDirs != null && mRelativeDirs.length > 0) { relativeDir = mRelativeDirs[0]; } else { if (mBackupNames == null || mBackupNames.length == 0) { // Base backup relativeDir = null; } else { // Generate relative directories Backup backup = BackupUtils.retrieveLatestBackupFromDb(userId, mBackupNames[0], packageName); if (backup == null) { throw new IllegalArgumentException("Backup with name " + mBackupNames[0] + " doesn't exist."); } relativeDir = backup.relativeDir; } } return new RestoreOpOptions(packageName, userId, relativeDir, mFlags); } public DeleteOpOptions getDeleteOpOptions(@NonNull String packageName, @UserIdInt int userId) { // For delete operation, backup names (v4) and relative dirs are only set for single // package backups. In all other cases, it only uses base backups. String[] relativeDirs; if (mRelativeDirs != null) { relativeDirs = mRelativeDirs; } else { if (mBackupNames == null || mBackupNames.length == 0) { // Base backup relativeDirs = null; } else { // Generate relative directories relativeDirs = new String[mBackupNames.length]; for (int i = 0; i < relativeDirs.length; ++i) { Backup backup = BackupUtils.retrieveLatestBackupFromDb(userId, mBackupNames[i], packageName); if (backup == null) { throw new IllegalArgumentException("Backup with name " + mBackupNames[i] + " doesn't exist."); } relativeDirs[i] = backup.relativeDir; } } } return new DeleteOpOptions(packageName, userId, relativeDirs); } protected BatchBackupOptions(@NonNull Parcel in) { mFlags = in.readInt(); mBackupNames = in.createStringArray(); mRelativeDirs = in.createStringArray(); } public static final Creator CREATOR = new Creator() { @Override @NonNull public BatchBackupOptions createFromParcel(@NonNull Parcel in) { return new BatchBackupOptions(in); } @Override @NonNull public BatchBackupOptions[] newArray(int size) { return new BatchBackupOptions[size]; } }; @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeInt(mFlags); dest.writeStringArray(mBackupNames); dest.writeStringArray(mRelativeDirs); } public BatchBackupOptions(@NonNull JSONObject jsonObject) throws JSONException { assert jsonObject.getString("tag").equals(TAG); mFlags = jsonObject.getInt("flags"); mBackupNames = JSONUtils.getArray(String.class, jsonObject.optJSONArray("backup_names")); mRelativeDirs = JSONUtils.getArray(String.class, jsonObject.optJSONArray("relative_dirs")); } public static final JsonDeserializer.Creator DESERIALIZER = BatchBackupOptions::new; @NonNull @Override public JSONObject serializeToJson() throws JSONException { JSONObject jsonObject = new JSONObject(); jsonObject.put("tag", TAG); jsonObject.put("flags", mFlags); jsonObject.put("backup_names", JSONUtils.getJSONArray(mBackupNames)); jsonObject.put("relative_dirs", JSONUtils.getJSONArray(mRelativeDirs)); return jsonObject; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/batchops/struct/BatchComponentOptions.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.batchops.struct; import android.os.Parcel; import androidx.annotation.NonNull; import org.json.JSONException; import org.json.JSONObject; import java.util.Objects; import io.github.muntashirakon.AppManager.history.JsonDeserializer; import io.github.muntashirakon.AppManager.utils.JSONUtils; public class BatchComponentOptions implements IBatchOpOptions { public static final String TAG = BatchComponentOptions.class.getSimpleName(); @NonNull private String[] mSignatures; public BatchComponentOptions(@NonNull String[] signatures) { mSignatures = signatures; } @NonNull public String[] getSignatures() { return mSignatures; } protected BatchComponentOptions(@NonNull Parcel in) { mSignatures = Objects.requireNonNull(in.createStringArray()); } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeStringArray(mSignatures); } protected BatchComponentOptions(@NonNull JSONObject jsonObject) throws JSONException { assert jsonObject.getString("tag").equals(TAG); mSignatures = JSONUtils.getArray(String.class, jsonObject.getJSONArray("signatures")); } public static final JsonDeserializer.Creator DESERIALIZER = BatchComponentOptions::new; @NonNull @Override public JSONObject serializeToJson() throws JSONException { JSONObject jsonObject = new JSONObject(); jsonObject.put("tag", TAG); jsonObject.put("signatures", JSONUtils.getJSONArray(mSignatures)); return jsonObject; } @Override public int describeContents() { return 0; } public static final Creator CREATOR = new Creator() { @Override @NonNull public BatchComponentOptions createFromParcel(@NonNull Parcel in) { return new BatchComponentOptions(in); } @Override @NonNull public BatchComponentOptions[] newArray(int size) { return new BatchComponentOptions[size]; } }; } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/batchops/struct/BatchDexOptOptions.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.batchops.struct; import android.os.Parcel; import androidx.annotation.NonNull; import androidx.core.os.ParcelCompat; import org.json.JSONException; import org.json.JSONObject; import java.util.Objects; import io.github.muntashirakon.AppManager.apk.dexopt.DexOptOptions; import io.github.muntashirakon.AppManager.history.JsonDeserializer; public class BatchDexOptOptions implements IBatchOpOptions { public static final String TAG = BatchDexOptOptions.class.getSimpleName(); private DexOptOptions mDexOptOptions; public BatchDexOptOptions(@NonNull DexOptOptions dexOptOptions) { mDexOptOptions = dexOptOptions; } public DexOptOptions getDexOptOptions() { return mDexOptOptions; } protected BatchDexOptOptions(@NonNull Parcel in) { mDexOptOptions = Objects.requireNonNull(ParcelCompat.readParcelable(in, DexOptOptions.class.getClassLoader(), DexOptOptions.class)); } public static final Creator CREATOR = new Creator() { @Override @NonNull public BatchDexOptOptions createFromParcel(@NonNull Parcel in) { return new BatchDexOptOptions(in); } @Override @NonNull public BatchDexOptOptions[] newArray(int size) { return new BatchDexOptOptions[size]; } }; @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeParcelable(mDexOptOptions, flags); } protected BatchDexOptOptions(@NonNull JSONObject jsonObject) throws JSONException { assert jsonObject.getString("tag").equals(TAG); mDexOptOptions = DexOptOptions.DESERIALIZER.deserialize(jsonObject.getJSONObject("dex_opt_options")); } public static final JsonDeserializer.Creator DESERIALIZER = BatchDexOptOptions::new; @NonNull @Override public JSONObject serializeToJson() throws JSONException { JSONObject jsonObject = new JSONObject(); jsonObject.put("tag", TAG); jsonObject.put("dex_opt_options", mDexOptOptions.serializeToJson()); return jsonObject; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/batchops/struct/BatchFreezeOptions.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.batchops.struct; import android.os.Parcel; import androidx.annotation.NonNull; import org.json.JSONException; import org.json.JSONObject; import io.github.muntashirakon.AppManager.history.JsonDeserializer; import io.github.muntashirakon.AppManager.utils.FreezeUtils; public class BatchFreezeOptions implements IBatchOpOptions { public static final String TAG = BatchFreezeOptions.class.getSimpleName(); @FreezeUtils.FreezeMethod private int mType; private boolean mPreferCustom; public BatchFreezeOptions(@FreezeUtils.FreezeMethod int type, boolean preferCustom) { mType = type; mPreferCustom = preferCustom; } public int getType() { return mType; } public boolean isPreferCustom() { return mPreferCustom; } @Override public int describeContents() { return 0; } protected BatchFreezeOptions(@NonNull JSONObject jsonObject) throws JSONException { assert jsonObject.getString("tag").equals(TAG); mType = jsonObject.getInt("type"); mPreferCustom = jsonObject.getBoolean("prefer_custom"); } public static final JsonDeserializer.Creator DESERIALIZER = BatchFreezeOptions::new; protected BatchFreezeOptions(@NonNull Parcel in) { mType = in.readInt(); mPreferCustom = in.readByte() != 0; } public static final Creator CREATOR = new Creator() { @NonNull @Override public BatchFreezeOptions createFromParcel(@NonNull Parcel in) { return new BatchFreezeOptions(in); } @NonNull @Override public BatchFreezeOptions[] newArray(int size) { return new BatchFreezeOptions[size]; } }; @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeInt(mType); dest.writeByte((byte) (mPreferCustom ? 1 : 0)); } @NonNull @Override public JSONObject serializeToJson() throws JSONException { JSONObject jsonObject = new JSONObject(); jsonObject.put("tag", TAG); jsonObject.put("type", mType); jsonObject.put("prefer_custom", mPreferCustom); return jsonObject; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/batchops/struct/BatchNetPolicyOptions.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.batchops.struct; import android.os.Parcel; import androidx.annotation.NonNull; import org.json.JSONException; import org.json.JSONObject; import io.github.muntashirakon.AppManager.compat.NetworkPolicyManagerCompat.NetPolicy; import io.github.muntashirakon.AppManager.history.JsonDeserializer; public class BatchNetPolicyOptions implements IBatchOpOptions { public static final String TAG = BatchNetPolicyOptions.class.getSimpleName(); @NetPolicy private int mPolicies; public BatchNetPolicyOptions(@NetPolicy int policies) { mPolicies = policies; } @NetPolicy public int getPolicies() { return mPolicies; } protected BatchNetPolicyOptions(@NonNull Parcel in) { mPolicies = in.readInt(); } public static final Creator CREATOR = new Creator() { @Override @NonNull public BatchNetPolicyOptions createFromParcel(@NonNull Parcel in) { return new BatchNetPolicyOptions(in); } @Override @NonNull public BatchNetPolicyOptions[] newArray(int size) { return new BatchNetPolicyOptions[size]; } }; @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeInt(mPolicies); } protected BatchNetPolicyOptions(@NonNull JSONObject jsonObject) throws JSONException { assert jsonObject.getString("tag").equals(TAG); mPolicies = jsonObject.getInt("policies"); } public static final JsonDeserializer.Creator DESERIALIZER = BatchNetPolicyOptions::new; @NonNull @Override public JSONObject serializeToJson() throws JSONException { JSONObject jsonObject = new JSONObject(); jsonObject.put("tag", TAG); jsonObject.put("policies", mPolicies); return jsonObject; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/batchops/struct/BatchPermissionOptions.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.batchops.struct; import android.os.Parcel; import androidx.annotation.NonNull; import org.json.JSONException; import org.json.JSONObject; import java.util.Objects; import io.github.muntashirakon.AppManager.history.JsonDeserializer; import io.github.muntashirakon.AppManager.utils.JSONUtils; public class BatchPermissionOptions implements IBatchOpOptions { public static final String TAG = BatchPermissionOptions.class.getSimpleName(); @NonNull private String[] mPermissions; public BatchPermissionOptions(@NonNull String[] permissions) { mPermissions = permissions; } @NonNull public String[] getPermissions() { return mPermissions; } protected BatchPermissionOptions(@NonNull Parcel in) { mPermissions = Objects.requireNonNull(in.createStringArray()); } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeStringArray(mPermissions); } protected BatchPermissionOptions(@NonNull JSONObject jsonObject) throws JSONException { assert jsonObject.getString("tag").equals(TAG); mPermissions = JSONUtils.getArray(String.class, jsonObject.getJSONArray("permissions")); } public static final JsonDeserializer.Creator DESERIALIZER = BatchPermissionOptions::new; @NonNull @Override public JSONObject serializeToJson() throws JSONException { JSONObject jsonObject = new JSONObject(); jsonObject.put("tag", TAG); jsonObject.put("permissions", JSONUtils.getJSONArray(mPermissions)); return jsonObject; } @Override public int describeContents() { return 0; } public static final Creator CREATOR = new Creator() { @Override @NonNull public BatchPermissionOptions createFromParcel(@NonNull Parcel in) { return new BatchPermissionOptions(in); } @Override @NonNull public BatchPermissionOptions[] newArray(int size) { return new BatchPermissionOptions[size]; } }; } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/batchops/struct/IBatchOpOptions.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.batchops.struct; import android.os.Parcelable; import org.json.JSONException; import io.github.muntashirakon.AppManager.history.IJsonSerializer; import io.github.muntashirakon.AppManager.history.JsonDeserializer; import io.github.muntashirakon.AppManager.utils.JSONUtils; public interface IBatchOpOptions extends Parcelable, IJsonSerializer { JsonDeserializer.Creator DESERIALIZER = jsonObject -> { String tag = JSONUtils.getString(jsonObject, "tag"); if (BatchAppOpsOptions.TAG.equals(tag)) { return BatchAppOpsOptions.DESERIALIZER.deserialize(jsonObject); } else if (BatchBackupImportOptions.TAG.equals(tag)) { return BatchBackupImportOptions.DESERIALIZER.deserialize(jsonObject); } else if (BatchBackupOptions.TAG.equals(tag)) { return BatchBackupOptions.DESERIALIZER.deserialize(jsonObject); } else if (BatchComponentOptions.TAG.equals(tag)) { return BatchComponentOptions.DESERIALIZER.deserialize(jsonObject); } else if (BatchDexOptOptions.TAG.equals(tag)) { return BatchDexOptOptions.DESERIALIZER.deserialize(jsonObject); } else if (BatchFreezeOptions.TAG.equals(tag)) { return BatchFreezeOptions.DESERIALIZER.deserialize(jsonObject); } else if (BatchNetPolicyOptions.TAG.equals(tag)) { return BatchNetPolicyOptions.DESERIALIZER.deserialize(jsonObject); } else if (BatchPermissionOptions.TAG.equals(tag)) { return BatchPermissionOptions.DESERIALIZER.deserialize(jsonObject); } else throw new JSONException("Invalid tag: " + tag); }; } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/changelog/Changelog.java ================================================ // SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.changelog; import androidx.annotation.NonNull; import java.util.LinkedList; // Copyright 2013 Gabriele Mariotti // Copyright 2022 Muntashir Al-Islam public class Changelog { @NonNull private final LinkedList mChangelogItems; private boolean mBulletedList; public Changelog() { mChangelogItems = new LinkedList<>(); } public void addItem(@NonNull ChangelogItem row) { mChangelogItems.add(row); } /** * Clear all rows */ public void clearAllRows() { mChangelogItems.clear(); } public boolean isBulletedList() { return mBulletedList; } public void setBulletedList(boolean bulletedList) { mBulletedList = bulletedList; } public LinkedList getChangelogItems() { return mChangelogItems; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/changelog/ChangelogHeader.java ================================================ // SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.changelog; import androidx.annotation.NonNull; import java.util.Locale; // Copyright 2013 Gabriele Mariotti // Copyright 2022 Muntashir Al-Islam public class ChangelogHeader extends ChangelogItem { @NonNull private final String mVersionName; private final long mVersionCode; @NonNull private final String mReleaseType; @NonNull private final String mReleaseDate; public ChangelogHeader(@NonNull String versionName, long versionCode, @NonNull String releaseType, @NonNull String releaseDate) { super(parseHeaderText(versionName, versionCode), HEADER); mVersionName = versionName; mVersionCode = versionCode; mReleaseType = releaseType; mReleaseDate = releaseDate; setBulletedList(false); } @NonNull public String getVersionName() { return mVersionName; } public long getVersionCode() { return mVersionCode; } @NonNull public String getReleaseType() { return mReleaseType; } @NonNull public String getReleaseDate() { return mReleaseDate; } @NonNull private static CharSequence parseHeaderText(@NonNull String versionName, long versionCode) { return String.format(Locale.getDefault(), "Version %s (%d)", versionName, versionCode); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/changelog/ChangelogItem.java ================================================ // SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.changelog; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.text.HtmlCompat; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; // Copyright 2013 Gabriele Mariotti // Copyright 2022 Muntashir Al-Islam public class ChangelogItem { @IntDef({HEADER, TITLE, NOTE, NEW, IMPROVE, FIX}) @Retention(RetentionPolicy.SOURCE) public @interface ChangelogType { } public static final int HEADER = -1; public static final int TITLE = 0; public static final int NOTE = 1; public static final int NEW = 2; public static final int IMPROVE = 3; public static final int FIX = 4; @IntDef({TEXT_SMALL, TEXT_MEDIUM, TEXT_LARGE}) @Retention(RetentionPolicy.SOURCE) public @interface ChangeTextType { } public static final int TEXT_SMALL = 0; public static final int TEXT_MEDIUM = 1; public static final int TEXT_LARGE = 2; @ChangelogType public final int type; @NonNull private final CharSequence mChangeText; private boolean mBulletedList; private boolean mSubtext; @Nullable private String mChangeTitle; @ChangeTextType private int mChangeTextType; public ChangelogItem(@ChangelogType int type) { mChangeText = ""; this.type = type; } public ChangelogItem(@NonNull CharSequence changeText, @ChangelogType int type) { mChangeText = changeText; this.type = type; } public ChangelogItem(@NonNull String changeText, @ChangelogType int type) { mChangeText = parseChangeText(changeText); this.type = type; } public boolean isBulletedList() { return mBulletedList; } public void setBulletedList(boolean bulletedList) { mBulletedList = bulletedList; } public boolean isSubtext() { return mSubtext; } public void setSubtext(boolean subtext) { mSubtext = subtext; mChangeTextType = subtext ? TEXT_SMALL : TEXT_MEDIUM; } @NonNull public CharSequence getChangeText() { return mChangeText; } @Nullable public String getChangeTitle() { return mChangeTitle; } void setChangeTitle(@Nullable String changeTitle) { mChangeTitle = changeTitle; } @ChangeTextType public int getChangeTextType() { return mChangeTextType; } public void setChangeTextType(@ChangeTextType int changeTextType) { mChangeTextType = changeTextType; } @NonNull public static CharSequence parseChangeText(@NonNull String changeText) { // TODO: Supported markups **Bold**, __Italic__, `Monospace`, ~~Strikethrough~~, [Link](link_name) changeText = changeText.replaceAll("\\[", "<").replaceAll("\\]", ">"); return HtmlCompat.fromHtml(changeText, HtmlCompat.FROM_HTML_MODE_COMPACT); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/changelog/ChangelogParser.java ================================================ // SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.changelog; import android.content.Context; import android.util.Log; import android.util.Xml; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RawRes; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Objects; /** * Read and parse res/raw/changelog.xml. */ // Copyright 2013 Gabriele Mariotti // Copyright 2022 Muntashir Al-Islam public class ChangelogParser { private static final String TAG = ChangelogParser.class.getSimpleName(); private static final String TAG_CHANGELOG = "changelog"; private static final String TAG_RELEASE = "release"; private static final String TAG_TITLE = "title"; private static final String TAG_NEW = "new"; private static final String TAG_IMPROVE = "improve"; private static final String TAG_FIX = "fix"; private static final String TAG_NOTE = "note"; private static final String ATTR_RELEASE_TYPE = "type"; private static final String ATTR_RELEASE_VERSION = "version"; private static final String ATTR_RELEASE_CODE = "code"; private static final String ATTR_RELEASE_DATE = "date"; private static final String ATTR_TYPE = "type"; private static final String ATTR_TITLE = "title"; private static final String ATTR_BULLET = "bullet"; private static final String ATTR_SUBTEXT = "subtext"; private static final List CHANGE_LOG_TAGS = new ArrayList() {{ add(TAG_TITLE); add(TAG_NEW); add(TAG_IMPROVE); add(TAG_FIX); add(TAG_NOTE); }}; protected Context mContext; @RawRes private final int mChangeLogFileResourceId; private final long mStartVersion; /** * Create a new instance for a context and for a custom changelog file. *

* You have to use file in res/raw folder. * * @param context current Context * @param changeLogFileResourceId reference for a custom xml file */ public ChangelogParser(@NonNull Context context, @RawRes int changeLogFileResourceId) { this(context, changeLogFileResourceId, 0); } /** * Create a new instance for a context and for a custom changelog file. *

* You have to use file in res/raw folder. * * @param context current Context * @param changeLogFileResourceId reference for a custom xml file */ public ChangelogParser(@NonNull Context context, @RawRes int changeLogFileResourceId, long startVersion) { mContext = context; mChangeLogFileResourceId = changeLogFileResourceId; mStartVersion = startVersion; } /** * Read and parse res/raw/changelog.xml * * @return {@link Changelog} obj with all data * @throws IOException if changelog.xml is not found * @throws XmlPullParserException if there are errors during parsing */ @NonNull public Changelog parse() throws IOException, XmlPullParserException { try (InputStream is = mContext.getResources().openRawResource(mChangeLogFileResourceId)) { Changelog changelog = new Changelog(); XmlPullParser parser = Xml.newPullParser(); parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false); parser.setInput(is, "UTF-8"); parser.nextTag(); // Read changelog tag readChangelogTag(parser, changelog); return changelog; } } /** * Parse changelog node */ protected void readChangelogTag(@NonNull XmlPullParser parser, @NonNull Changelog changeLog) throws IOException, XmlPullParserException { // changelog is the root if (parser.getDepth() != 1) { Log.e(TAG, String.format(Locale.ROOT, "Invalid depth %d, expecting depth 1.", parser.getDepth())); return; } parser.require(XmlPullParser.START_TAG, null, TAG_CHANGELOG); // Read attributes String showBullet = parser.getAttributeValue(null, ATTR_BULLET); changeLog.setBulletedList("true".equals(showBullet)); // Parse nested nodes while (parser.next() != XmlPullParser.END_TAG) { if (parser.getEventType() != XmlPullParser.START_TAG || parser.getDepth() != 2) { continue; } if (TAG_RELEASE.equals(parser.getName())) { // Parse new, improve, fix, note readReleaseTag(parser, changeLog); } else { Log.w(TAG, String.format(Locale.ROOT, "Unknown tag (%s) at depth 2." + parser.getName())); } } } /** * Parse changeLogVersion node */ private void readReleaseTag(@NonNull XmlPullParser parser, @NonNull Changelog changeLog) throws IOException, XmlPullParserException { // Ensure release tag if (parser.getDepth() != 2) { Log.e(TAG, String.format(Locale.ROOT, "Invalid depth %d, expecting depth 2.", parser.getDepth())); return; } parser.require(XmlPullParser.START_TAG, null, TAG_RELEASE); // Read attributes String versionName = Objects.requireNonNull(parser.getAttributeValue(null, ATTR_RELEASE_VERSION)); String versionCodeStr = Objects.requireNonNull(parser.getAttributeValue(null, ATTR_RELEASE_CODE)); long versionCode = 0; try { versionCode = Integer.parseInt(versionCodeStr); } catch (NumberFormatException e) { Log.w(TAG, "Error while parsing versionCode."); } String releaseDate = Objects.requireNonNull(parser.getAttributeValue(null, ATTR_RELEASE_DATE)); String releaseType = Objects.requireNonNull(parser.getAttributeValue(null, ATTR_RELEASE_TYPE)); // Skip parsing this node if versionCode < startVersionCode if (versionCode < mStartVersion) { while (parser.next() != XmlPullParser.END_TAG) { // Continue parsing until end is reached } return; } // Set release meta changeLog.addItem(new ChangelogHeader(versionName, versionCode, releaseType, releaseDate)); // Parse nested nodes while (parser.next() != XmlPullParser.END_TAG) { if (parser.getEventType() != XmlPullParser.START_TAG || parser.getDepth() != 3) { continue; } if (CHANGE_LOG_TAGS.contains(parser.getName())) { readChangelogItemTags(parser, changeLog); } else { Log.w(TAG, String.format(Locale.ROOT, "Unknown tag (%s) at depth 3." + parser.getName())); } } } /** * Parse changeLogText node */ private void readChangelogItemTags(@NonNull XmlPullParser parser, @NonNull Changelog changeLog) throws XmlPullParserException, IOException { if (parser.getDepth() != 3) { Log.e(TAG, String.format(Locale.ROOT, "Invalid depth %d, expecting depth 3.", parser.getDepth())); return; } String tag = parser.getName(); // Read attributes String changeTextType = parser.getAttributeValue(null, ATTR_TYPE); String title = parser.getAttributeValue(null, ATTR_TITLE); String showBullet = parser.getAttributeValue(null, ATTR_BULLET); String subtext = parser.getAttributeValue(null, ATTR_SUBTEXT); // Read text String changeText = null; if (parser.next() == XmlPullParser.TEXT) { changeText = parser.getText(); parser.nextTag(); } // Set type int type; switch (tag) { default: case TAG_NOTE: type = ChangelogItem.NOTE; break; case TAG_TITLE: type = ChangelogItem.TITLE; break; case TAG_NEW: type = ChangelogItem.NEW; break; case TAG_IMPROVE: type = ChangelogItem.IMPROVE; break; case TAG_FIX: type = ChangelogItem.FIX; break; } ChangelogItem changelogItem = changeText == null ? new ChangelogItem(type) : new ChangelogItem(changeText, type); changelogItem.setChangeTitle(title); changelogItem.setBulletedList("true".equals(showBullet) || changeLog.isBulletedList()); changelogItem.setChangeTextType(getChangeTextType(changeTextType)); changelogItem.setSubtext("true".equals(subtext)); changeLog.addItem(changelogItem); } @ChangelogItem.ChangeTextType private static int getChangeTextType(@Nullable String rawText) { if (rawText == null) { return ChangelogItem.TEXT_MEDIUM; } switch (rawText) { default: case "medium": return ChangelogItem.TEXT_MEDIUM; case "large": return ChangelogItem.TEXT_LARGE; case "small": return ChangelogItem.TEXT_SMALL; } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/changelog/ChangelogRecyclerAdapter.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.changelog; import android.content.Context; import android.graphics.Color; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.method.LinkMovementMethod; import android.text.style.ImageSpan; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.ColorInt; import androidx.annotation.ColorRes; import androidx.annotation.NonNull; import androidx.annotation.StringRes; import androidx.annotation.StyleRes; import androidx.core.widget.TextViewCompat; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.chip.ChipDrawable; import java.util.ArrayList; import java.util.List; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.util.AdapterUtils; import io.github.muntashirakon.util.UiUtils; public class ChangelogRecyclerAdapter extends RecyclerView.Adapter { private final List mAdapterList = new ArrayList<>(); public ChangelogRecyclerAdapter() { } public void setAdapterList(@NonNull List list) { synchronized (mAdapterList) { AdapterUtils.notifyDataSetChanged(this, mAdapterList, list); } } @ChangelogItem.ChangelogType @Override public int getItemViewType(int position) { synchronized (mAdapterList) { return mAdapterList.get(position).type; } } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, @ChangelogItem.ChangelogType int viewType) { View v; if (viewType == ChangelogItem.HEADER) { v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_changelog_header, parent, false); } else { v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_changelog_item, parent, false); } return new ViewHolder(v, viewType); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { ChangelogItem changelogItem; synchronized (mAdapterList) { changelogItem = mAdapterList.get(position); } Context context = holder.itemView.getContext(); switch (changelogItem.type) { case ChangelogItem.HEADER: holder.label.setText(((ChangelogHeader) changelogItem).getReleaseType()); holder.title.setText(changelogItem.getChangeText()); holder.subtitle.setText(((ChangelogHeader) changelogItem).getReleaseDate()); break; default: case ChangelogItem.TITLE: TextViewCompat.setTextAppearance(holder.subtitle, getTitleTextAppearance(changelogItem.getChangeTextType())); holder.subtitle.setText(getChangeText(context, changelogItem)); break; case ChangelogItem.FIX: case ChangelogItem.IMPROVE: case ChangelogItem.NEW: case ChangelogItem.NOTE: TextViewCompat.setTextAppearance(holder.subtitle, getChangeTextAppearance(changelogItem.getChangeTextType())); holder.subtitle.setText(getChangeText(context, changelogItem)); break; } } @Override public int getItemCount() { synchronized (mAdapterList) { return mAdapterList.size(); } } @NonNull private CharSequence getChangeText(@NonNull Context context, @NonNull ChangelogItem item) { SpannableStringBuilder sb = new SpannableStringBuilder(); if (item.isBulletedList()) { if (item.isSubtext()) { sb.append(" "); } sb.append("• "); } else { // Display tag @StringRes int tagNameRes; @ColorInt int color; @ColorRes int backgroundColorRes; switch (item.type) { case ChangelogItem.FIX: tagNameRes = R.string.changelog_type_fix; backgroundColorRes = io.github.muntashirakon.ui.R.color.changelog_fix; color = Color.BLACK; break; case ChangelogItem.IMPROVE: tagNameRes = R.string.changelog_type_improve; backgroundColorRes = io.github.muntashirakon.ui.R.color.changelog_improve; color = Color.WHITE; break; case ChangelogItem.NEW: tagNameRes = R.string.changelog_type_new; backgroundColorRes = io.github.muntashirakon.ui.R.color.changelog_new; color = Color.WHITE; break; case ChangelogItem.HEADER: case ChangelogItem.TITLE: case ChangelogItem.NOTE: default: tagNameRes = 0; backgroundColorRes = 0; color = 0; break; } if (tagNameRes != 0) { ChipDrawable chip = ChipDrawable.createFromAttributes(context, null, com.google.android.material.R.attr.chipStandaloneStyle, com.google.android.material.R.style.Widget_Material3_Chip_Assist_Elevated); chip.setTextResource(tagNameRes); chip.setTextColor(color); chip.setTextSize(UiUtils.spToPx(context, 10)); chip.setChipBackgroundColorResource(backgroundColorRes); chip.setCloseIconVisible(false); chip.setChipStartPadding(0); chip.setChipEndPadding(0); chip.setBounds(0, 0, chip.getIntrinsicWidth(), UiUtils.dpToPx(context, 20)); ImageSpan span = new ImageSpan(chip); sb.append(" "); sb.setSpan(span, sb.length() - 1, sb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); sb.append(" "); } } if (item.getChangeTitle() != null) { sb.append('[').append(item.getChangeTitle()).append("] "); } return sb.append(item.getChangeText()); } @StyleRes public static int getChangeTextAppearance(@ChangelogItem.ChangeTextType int type) { switch(type) { default: case ChangelogItem.TEXT_MEDIUM: return com.google.android.material.R.style.TextAppearance_Material3_BodyMedium; case ChangelogItem.TEXT_LARGE: return com.google.android.material.R.style.TextAppearance_Material3_BodyLarge; case ChangelogItem.TEXT_SMALL: return com.google.android.material.R.style.TextAppearance_Material3_BodySmall; } } @StyleRes public static int getTitleTextAppearance(@ChangelogItem.ChangeTextType int type) { switch(type) { default: case ChangelogItem.TEXT_MEDIUM: return com.google.android.material.R.style.TextAppearance_Material3_TitleMedium; case ChangelogItem.TEXT_LARGE: return com.google.android.material.R.style.TextAppearance_Material3_TitleLarge; case ChangelogItem.TEXT_SMALL: return com.google.android.material.R.style.TextAppearance_Material3_TitleSmall; } } public static class ViewHolder extends RecyclerView.ViewHolder { public final TextView label; public final TextView title; public final TextView subtitle; public ViewHolder(@NonNull View itemView, @ChangelogItem.ChangelogType int viewType) { super(itemView); label = itemView.findViewById(R.id.item_label); title = itemView.findViewById(R.id.item_title); subtitle = itemView.findViewById(R.id.item_subtitle); subtitle.setMovementMethod(LinkMovementMethod.getInstance()); if (viewType == ChangelogItem.HEADER) { title.setMovementMethod(LinkMovementMethod.getInstance()); } } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/compat/ActivityManagerCompat.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.compat; import android.Manifest; import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.ActivityManagerNative; import android.app.IActivityManager; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.IContentProvider; import android.content.IIntentReceiver; import android.content.Intent; import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; import android.os.SystemClock; import android.os.UserHandleHidden; import android.provider.Settings; import android.text.TextUtils; import android.view.KeyEvent; import androidx.annotation.MainThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.RequiresPermission; import androidx.annotation.VisibleForTesting; import androidx.annotation.WorkerThread; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.ListIterator; import java.util.regex.Matcher; import java.util.regex.Pattern; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.fm.FmProvider; import io.github.muntashirakon.AppManager.ipc.ProxyBinder; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.runner.Runner; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.users.Users; import io.github.muntashirakon.AppManager.utils.ExUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; public final class ActivityManagerCompat { public interface ActivityLaunchUserInteractionRequiredCallback { @WorkerThread void onInteraction(); } @RequiresPermission(allOf = { Manifest.permission.WRITE_SECURE_SETTINGS, ManifestCompat.permission.INJECT_EVENTS }) @MainThread public static boolean startActivityViaAssist(@NonNull Context context, @NonNull ComponentName activity, @Nullable ActivityLaunchUserInteractionRequiredCallback callback) throws SecurityException { // Need two permissions: WRITE_SECURE_SETTINGS and INJECT_EVENTS SelfPermissions.requireSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS); boolean canInjectEvents = SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.INJECT_EVENTS); ContentResolver resolver = context.getContentResolver(); // Backup assistant value String assistantComponent = Settings.Secure.getString(resolver, "assistant"); if (canInjectEvents) { ThreadUtils.postOnBackgroundThread(() -> { try { // Set assistant value to the target activity component Settings.Secure.putString(resolver, "assistant", activity.flattenToShortString()); // Run it as an assistant by injecting KEYCODE_ASSIST (219) InputManagerCompat.sendKeyEvent(KeyEvent.KEYCODE_ASSIST, false); // Wait until system opens the new assistant (i.e., activity), this is an empirical value SystemClock.sleep(500); } finally { // Restore assistant value Settings.Secure.putString(resolver, "assistant", assistantComponent); } }); } else if (callback != null) { // Cannot launch event by default, use callback ThreadUtils.postOnBackgroundThread(() -> { try { // Set assistant value to the target activity component Settings.Secure.putString(resolver, "assistant", activity.flattenToShortString()); // Trigger callback callback.onInteraction(); } finally { // Restore assistant value Settings.Secure.putString(resolver, "assistant", assistantComponent); } }); } // else do nothing return canInjectEvents; } @SuppressWarnings("deprecation") public static int startActivity(@NonNull Intent intent, @UserIdInt int userHandle) throws SecurityException { IActivityManager am; String callingPackage; if (intent.getData() != null && FmProvider.AUTHORITY.equals(intent.getData().getAuthority())) { // We need unprivileged authority for this am = getActivityManagerUnprivileged(); callingPackage = BuildConfig.APPLICATION_ID; } else { am = getActivityManager(); callingPackage = SelfPermissions.getCallingPackage(Users.getSelfOrRemoteUid()); } try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { return am.startActivityAsUserWithFeature(null, callingPackage, null, intent, intent.getType(), null, null, 0, 0, null, null, userHandle); } else { return am.startActivityAsUser(null, callingPackage, intent, intent.getType(), null, null, 0, 0, null, null, userHandle); } } catch (RemoteException e) { return ExUtils.rethrowFromSystemServer(e); } } @SuppressWarnings("deprecation") public static ComponentName startService(Intent intent, @UserIdInt int userHandle, boolean asForeground) throws RemoteException { IActivityManager am = getActivityManager(); String callingPackage = SelfPermissions.getCallingPackage(Users.getSelfOrRemoteUid()); ComponentName cn; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { cn = am.startService(null, intent, intent.getType(), asForeground, callingPackage, null, userHandle); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { cn = am.startService(null, intent, intent.getType(), asForeground, callingPackage, userHandle); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { cn = am.startService(null, intent, intent.getType(), callingPackage, userHandle); } else cn = am.startService(null, intent, intent.getType(), userHandle); return cn; } public static int sendBroadcast(Intent intent, @UserIdInt int userHandle) throws RemoteException { IActivityManager am = getActivityManager(); int res; IIntentReceiver receiver = new IntentReceiver(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { res = am.broadcastIntentWithFeature(null, null, intent, null, receiver, 0, null, null, null, AppOpsManagerCompat.OP_NONE, null, true, false, userHandle); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { res = am.broadcastIntent(null, intent, null, null, 0, null, null, null, AppOpsManagerCompat.OP_NONE, null, true, false, userHandle); } else { res = am.broadcastIntent(null, intent, null, null, 0, null, null, null, AppOpsManagerCompat.OP_NONE, true, false, userHandle); } return res; } @Nullable public static IContentProvider getContentProviderExternal(String name, int userId, IBinder token, String tag) throws RemoteException { IActivityManager am = getActivityManager(); try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { return am.getContentProviderExternal(name, userId, token, tag).provider; } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { return ((android.app.ContentProviderHolder) am.getContentProviderExternal(name, userId, token)).provider; } else { return ((IActivityManager.ContentProviderHolder) am.getContentProviderExternal(name, userId, token)).provider; } } catch (NullPointerException e) { return null; } } @NonNull public static List getRunningServices(String packageName, @UserIdInt int userId) { List runningServices; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 && !SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.REAL_GET_TASKS) && canDumpRunningServices()) { // Fetch running services by parsing dumpsys output runningServices = getRunningServicesUsingDumpSys(packageName); } else { // For no-root, this returns services running in the current UID since Android Oreo try { runningServices = getActivityManager().getServices(100, 0); } catch (RemoteException e) { return Collections.emptyList(); } } List res = new ArrayList<>(); for (ActivityManager.RunningServiceInfo info : runningServices) { if (info.service.getPackageName().equals(packageName) && userId == UserHandleHidden.getUserId(info.uid)) { res.add(info); } } return res; } @NonNull public static List getRunningAppProcesses() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.REAL_GET_TASKS) && canDumpRunningServices()) { // Fetch running app processes by parsing dumpsys output if root/ADB is disabled // and android.permission.DUMP is granted return getRunningAppProcessesUsingDumpSys(); } else { // For no-root, this returns app processes running in the current UID since Android M return ExUtils.requireNonNullElse(() -> getActivityManager().getRunningAppProcesses(), Collections.emptyList()); } } @RequiresPermission("android.permission.KILL_UID") public static void killUid(int uid, String reason) throws RemoteException { getActivityManager().killUid(UserHandleHidden.getAppId(uid), UserHandleHidden.getUserId(uid), reason); } @SuppressWarnings("deprecation") public static IActivityManager getActivityManager() { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { return IActivityManager.Stub.asInterface(ProxyBinder.getService(Context.ACTIVITY_SERVICE)); } else { return ActivityManagerNative.asInterface(ProxyBinder.getService(Context.ACTIVITY_SERVICE)); } } public static IActivityManager getActivityManagerUnprivileged() { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { return IActivityManager.Stub.asInterface(ProxyBinder.getUnprivilegedService(Context.ACTIVITY_SERVICE)); } else { return ActivityManagerNative.asInterface(ProxyBinder.getUnprivilegedService(Context.ACTIVITY_SERVICE)); } } @SuppressWarnings("RegExpRedundantEscape") private static final Pattern APP_PROCESS_REGEX = Pattern.compile("\\*[A-Z]+\\* UID (\\d+) ProcessRecord\\{[0-9a-f]+ (\\d+):([^/]+)/[^\\}]+\\}"); @SuppressWarnings("RegExpRedundantEscape") private static final Pattern PKG_LIST_REGEX = Pattern.compile("packageList=\\{([^/]+)\\}"); @NonNull private static List getRunningAppProcessesUsingDumpSys() { List runningAppProcessInfos = new ArrayList<>(); Runner.Result result = Runner.runCommand(new String[]{"dumpsys", "activity", "processes"}); if (!result.isSuccessful()) return runningAppProcessInfos; List appProcessDump = result.getOutputAsList(1); return parseRunningAppProcesses(appProcessDump); } @VisibleForTesting @NonNull static List parseRunningAppProcesses(@NonNull List appProcessesDump) { List runningAppProcessInfos = new ArrayList<>(); Matcher aprMatcher; Matcher pkgrMatcher; String line; ListIterator it = appProcessesDump.listIterator(); if (!it.hasNext()) return runningAppProcessInfos; aprMatcher = APP_PROCESS_REGEX.matcher(it.next()); while (it.hasNext()) { if (!aprMatcher.find(0)) { // No matches found, check the next line aprMatcher = APP_PROCESS_REGEX.matcher(it.next()); continue; } // Matches found String uid = aprMatcher.group(1); String pid = aprMatcher.group(2); String processName = aprMatcher.group(3); if (uid == null || pid == null || processName == null) { // Criteria didn't match aprMatcher = APP_PROCESS_REGEX.matcher(it.next()); continue; } line = it.next(); aprMatcher = APP_PROCESS_REGEX.matcher(line); while (it.hasNext()) { if (aprMatcher.find(0)) { // found next ProcessRecord, no need to continue the search for pkgList break; } pkgrMatcher = PKG_LIST_REGEX.matcher(line); if (!pkgrMatcher.find(0)) { // Process didn't match, find next line line = it.next(); aprMatcher = APP_PROCESS_REGEX.matcher(line); continue; } // Found a pkgList String pkgList = pkgrMatcher.group(1); if (pkgList != null) { ActivityManager.RunningAppProcessInfo info = new ActivityManager.RunningAppProcessInfo(); info.uid = Integer.decode(uid); info.pid = Integer.decode(pid); info.processName = processName; String[] split = pkgList.split(", "); info.pkgList = new String[split.length]; System.arraycopy(split, 0, info.pkgList, 0, split.length); runningAppProcessInfos.add(info); } line = it.next(); aprMatcher = APP_PROCESS_REGEX.matcher(line); } } return runningAppProcessInfos; } @SuppressWarnings("RegExpRedundantEscape") private static final Pattern SERVICE_RECORD_REGEX = Pattern.compile("\\* ServiceRecord\\{[0-9a-f]+ u(\\d+) ([^\\}]+)\\}"); @SuppressWarnings("RegExpRedundantEscape") private static final Pattern PROCESS_RECORD_REGEX = Pattern.compile("app=ProcessRecord\\{[0-9a-f]+ (\\d+):([^/]+)/([^\\}]+)\\}"); @NonNull private static List getRunningServicesUsingDumpSys(String packageName) { List runningServices = new ArrayList<>(); Runner.Result result = Runner.runCommand(new String[]{"dumpsys", "activity", "services", "-p", packageName}); if (!result.isSuccessful()) return runningServices; List serviceDump = result.getOutputAsList(1); return parseRunningServices(serviceDump); } @VisibleForTesting @NonNull static List parseRunningServices(@NonNull List serviceDump) { List runningServices = new ArrayList<>(); Matcher srMatcher; Matcher prMatcher; ComponentName service; String line; ListIterator it = serviceDump.listIterator(); if (!it.hasNext()) return runningServices; srMatcher = SERVICE_RECORD_REGEX.matcher(it.next()); while (it.hasNext()) { // hasNext check doesn't omit anything since we'd still have to check for ProcessRecord if (!srMatcher.find(0)) { // No matches found, check the next line srMatcher = SERVICE_RECORD_REGEX.matcher(it.next()); continue; } // Matches found String userId = srMatcher.group(1); String serviceName = srMatcher.group(2); if (userId == null || serviceName == null) { // Criteria didn't match srMatcher = SERVICE_RECORD_REGEX.matcher(it.next()); continue; } // This is actually the short process name, original service name is under intent (in the next line) int i = serviceName.indexOf(':'); service = ComponentName.unflattenFromString(i == -1 ? serviceName : serviceName.substring(0, i)); line = it.next(); srMatcher = SERVICE_RECORD_REGEX.matcher(line); while (it.hasNext()) { if (srMatcher.find(0)) { // found next ServiceRecord, no need to continue the search for ProcessRecord break; } prMatcher = PROCESS_RECORD_REGEX.matcher(line); if (!prMatcher.find(0)) { // Process didn't match, find next line line = it.next(); srMatcher = SERVICE_RECORD_REGEX.matcher(line); continue; } // Found a ProcessRecord String pid = prMatcher.group(1); String processName = prMatcher.group(2); String userInfo = prMatcher.group(3); if (pid != null && processName != null && userInfo != null) { ActivityManager.RunningServiceInfo info = new ActivityManager.RunningServiceInfo(); info.pid = Integer.decode(pid); info.process = processName; info.service = service; // UID if (TextUtils.isDigitsOnly(userInfo)) { // UID < 10000 info.uid = Integer.decode(userInfo); } else if (userInfo.startsWith("u")) { // u(a|s)[i] userInfo = userInfo.substring(("u" + userId).length()); // u removed int iIdx = userInfo.indexOf('i'); int iIndex = iIdx == -1 ? userInfo.length() : iIdx; if (userInfo.startsWith("a")) { // User app info.uid = UserHandleHidden.getUid(Integer.decode(userId), 10_000 + Integer.decode(userInfo.substring(1, iIndex))); } else if (userInfo.startsWith("s")) { // System app info.uid = UserHandleHidden.getUid(Integer.decode(userId), Integer.decode(userInfo.substring(1, iIndex))); } else throw new IllegalStateException("No valid UID info found in ProcessRecord"); } else throw new IllegalStateException("Invalid user info section in ProcessRecord"); // TODO: 1/9/21 Parse others runningServices.add(info); } line = it.next(); srMatcher = SERVICE_RECORD_REGEX.matcher(line); } } return runningServices; } @RequiresApi(Build.VERSION_CODES.M) private static boolean canDumpRunningServices() { return SelfPermissions.checkSelfPermission(Manifest.permission.DUMP) && SelfPermissions.checkSelfPermission(Manifest.permission.PACKAGE_USAGE_STATS); } final static class IntentReceiver extends IIntentReceiver.Stub { private boolean mFinished = false; public void performReceive(Intent intent, int resultCode, String data, Bundle extras, boolean ordered, boolean sticky, int sendingUser) { String line = "Broadcast completed: result=" + resultCode; if (data != null) line = line + ", data=\"" + data + "\""; if (extras != null) line = line + ", extras: " + extras; Log.e("AM", line); synchronized (this) { mFinished = true; notifyAll(); } } public synchronized void waitForFinish() { try { while (!mFinished) wait(); } catch (InterruptedException e) { throw new IllegalStateException(e); } } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/compat/AppOpsManagerCompat.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.compat; import android.annotation.SuppressLint; import android.annotation.UserIdInt; import android.app.AppOpsManager; import android.app.AppOpsManagerHidden; import android.content.Context; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import android.os.RemoteException; import androidx.annotation.IntRange; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.RequiresPermission; import androidx.collection.SparseArrayCompat; import androidx.core.os.ParcelCompat; import com.android.internal.app.IAppOpsService; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import aosp.libcore.util.EmptyArray; import dev.rikka.tools.refine.Refine; import io.github.muntashirakon.AppManager.ipc.ProxyBinder; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.utils.ExUtils; import io.github.muntashirakon.AppManager.utils.MiuiUtils; @SuppressLint("SoonBlockedPrivateApi") public class AppOpsManagerCompat { @IntRange(from = -1, to = 5) @Retention(RetentionPolicy.SOURCE) public @interface Mode { } public static final int OP_FLAG_SELF; public static final int OP_FLAG_TRUSTED_PROXY; public static final int OP_FLAG_UNTRUSTED_PROXY; public static final int OP_FLAG_TRUSTED_PROXIED; public static final int OP_FLAG_UNTRUSTED_PROXIED; public static final int OP_FLAGS_ALL; public static final int OP_FLAGS_ALL_TRUSTED; static { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { OP_FLAG_SELF = AppOpsManagerHidden.OP_FLAG_SELF; OP_FLAG_TRUSTED_PROXY = AppOpsManagerHidden.OP_FLAG_TRUSTED_PROXY; OP_FLAG_UNTRUSTED_PROXY = AppOpsManagerHidden.OP_FLAG_UNTRUSTED_PROXY; OP_FLAG_TRUSTED_PROXIED = AppOpsManagerHidden.OP_FLAG_TRUSTED_PROXIED; OP_FLAG_UNTRUSTED_PROXIED = AppOpsManagerHidden.OP_FLAG_UNTRUSTED_PROXIED; OP_FLAGS_ALL = AppOpsManagerHidden.OP_FLAGS_ALL; OP_FLAGS_ALL_TRUSTED = AppOpsManagerHidden.OP_FLAGS_ALL_TRUSTED; } else { OP_FLAG_SELF = 0; OP_FLAG_TRUSTED_PROXY = 0; OP_FLAG_UNTRUSTED_PROXY = 0; OP_FLAG_TRUSTED_PROXIED = 0; OP_FLAG_UNTRUSTED_PROXIED = 0; OP_FLAGS_ALL = 0; OP_FLAGS_ALL_TRUSTED = 0; } } @Retention(RetentionPolicy.SOURCE) public @interface OpFlags { } public static final int UID_STATE_PERSISTENT; public static final int UID_STATE_TOP; public static final int UID_STATE_FOREGROUND_SERVICE_LOCATION; public static final int UID_STATE_FOREGROUND_SERVICE; public static final int UID_STATE_FOREGROUND; public static final int UID_STATE_BACKGROUND; public static final int UID_STATE_CACHED; public static final int MAX_PRIORITY_UID_STATE; public static final int MIN_PRIORITY_UID_STATE; static { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { UID_STATE_PERSISTENT = AppOpsManagerHidden.UID_STATE_PERSISTENT; UID_STATE_TOP = AppOpsManagerHidden.UID_STATE_TOP; UID_STATE_FOREGROUND_SERVICE = AppOpsManagerHidden.UID_STATE_FOREGROUND_SERVICE; UID_STATE_FOREGROUND = AppOpsManagerHidden.UID_STATE_FOREGROUND; UID_STATE_BACKGROUND = AppOpsManagerHidden.UID_STATE_BACKGROUND; UID_STATE_CACHED = AppOpsManagerHidden.UID_STATE_CACHED; } else { UID_STATE_PERSISTENT = 0; UID_STATE_TOP = 0; UID_STATE_FOREGROUND_SERVICE = 0; UID_STATE_FOREGROUND = 0; UID_STATE_BACKGROUND = 0; UID_STATE_CACHED = 0; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { UID_STATE_FOREGROUND_SERVICE_LOCATION = AppOpsManagerHidden.UID_STATE_FOREGROUND_SERVICE_LOCATION; MAX_PRIORITY_UID_STATE = AppOpsManagerHidden.MAX_PRIORITY_UID_STATE; MIN_PRIORITY_UID_STATE = AppOpsManagerHidden.MIN_PRIORITY_UID_STATE; } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) { UID_STATE_FOREGROUND_SERVICE_LOCATION = 0; MAX_PRIORITY_UID_STATE = UID_STATE_PERSISTENT; MIN_PRIORITY_UID_STATE = UID_STATE_CACHED; } else { UID_STATE_FOREGROUND_SERVICE_LOCATION = 0; MAX_PRIORITY_UID_STATE = 0; MIN_PRIORITY_UID_STATE = 0; } } @Retention(RetentionPolicy.SOURCE) public @interface UidState { } private static final SparseArrayCompat sModes = new SparseArrayCompat<>(); private static final String[] sOpToString; public static final int OP_NONE = AppOpsManagerHidden.OP_NONE; /** * Control whether an application is allowed to run in the background. */ @RequiresApi(Build.VERSION_CODES.N) public static final int OP_RUN_IN_BACKGROUND; /** * Run jobs when in background */ @RequiresApi(Build.VERSION_CODES.P) public static final int OP_RUN_ANY_IN_BACKGROUND; public static final int _NUM_OP = AppOpsManagerHidden._NUM_OP; static { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { OP_RUN_IN_BACKGROUND = AppOpsManagerHidden.OP_RUN_IN_BACKGROUND; } else { //noinspection NewApi OP_RUN_IN_BACKGROUND = 0; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { OP_RUN_ANY_IN_BACKGROUND = AppOpsManagerHidden.OP_RUN_ANY_IN_BACKGROUND; } else { //noinspection NewApi OP_RUN_ANY_IN_BACKGROUND = 0; } } /** * Mapping from a permission to the corresponding app op. */ private static final HashMap sPermToOp = new HashMap<>(); /** * Some ops don't have any permissions associated with them and are enabled by default. * We are interested in the parents of these ops. */ public static List sOpWithoutPerms; static { String[] opToString = EmptyArray.STRING; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { // Only needed for API 22 and earlier try { Field sOpToStringField = AppOpsManagerHidden.class.getDeclaredField("sOpToString"); sOpToStringField.setAccessible(true); opToString = (String[]) sOpToStringField.get(null); } catch (NoSuchFieldException | IllegalAccessException e) { e.printStackTrace(); } } sOpToString = opToString; for (Field field : AppOpsManager.class.getDeclaredFields()) { field.setAccessible(true); if (field.getType() == int.class && field.getName().startsWith("MODE_")) { try { sModes.put(field.getInt(null), field.getName()); } catch (IllegalAccessException ignore) { } } } HashSet opWithoutPerms = new HashSet<>(); for (int i = 0; i < _NUM_OP; i++) { String permission = AppOpsManagerHidden.opToPermission(i); if (permission != null) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { // Only needed for API 22 and earlier sPermToOp.put(permission, i); } } else { // No permission opWithoutPerms.add(AppOpsManagerHidden.opToSwitch(i)); } } sOpWithoutPerms = new ArrayList<>(opWithoutPerms); } public static boolean isMiuiOp(int op) { try { return MiuiUtils.isMiui() && op > AppOpsManagerHidden.MIUI_OP_START; } catch (Throwable e) { return false; } } @NonNull public static List getAllOps() { List appOps = new ArrayList<>(); for (int i = 0; i < _NUM_OP; ++i) { appOps.add(i); } if (MiuiUtils.isMiui()) { try { for (int op = AppOpsManagerHidden.MIUI_OP_START + 1; op < AppOpsManagerHidden.MIUI_OP_END; ++op) { appOps.add(op); } } catch (Exception ignore) { } } return appOps; } @NonNull public static List getOpsWithoutPermissions() { return sOpWithoutPerms; } @NonNull public static List getModeConstants() { return new ArrayList(sModes.size()) {{ for (int i = 0; i < sModes.size(); ++i) { add(sModes.keyAt(i)); } }}; } @NonNull public static String modeToName(@IntRange(from = -1) int mode) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { return AppOpsManagerHidden.modeToName(mode); } // Fallback for pre28 String fieldName = sModes.get(mode); if (fieldName == null) { return "mode=" + mode; } switch (mode) { case AppOpsManager.MODE_ALLOWED: return "allow"; case AppOpsManager.MODE_IGNORED: return "ignore"; case AppOpsManager.MODE_ERRORED: return "deny"; // Rests have the same name as the constant name in lower case minus the MODE_ prefix } return fieldName.substring(5).toLowerCase(Locale.ROOT); } /** * Retrieve the op switch that controls the given operation. */ public static int opToSwitch(int op) { return AppOpsManagerHidden.opToSwitch(op); } @NonNull public static String opToName(int op) { return AppOpsManagerHidden.opToName(op); } /** * Retrieve the permission associated with an operation, or null if there is not one. */ @Nullable public static String opToPermission(int op) { return AppOpsManagerHidden.opToPermission(op); } /** * Retrieve the default mode for the operation. */ public static int opToDefaultMode(int op) { try { return AppOpsManagerHidden.opToDefaultMode(op); } catch (NoSuchMethodError e) { return AppOpsManagerHidden.opToDefaultMode(op, false); } } /** * Retrieve the app op code for a permission, or {@link #OP_NONE} if there is not one. * This API is intended to be used for mapping runtime or appop permissions * to the corresponding app op. */ public static int permissionToOpCode(String permission) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { return AppOpsManagerHidden.permissionToOpCode(permission); } // Fallback for Lollipop Integer boxedOpCode = sPermToOp.get(permission); if (boxedOpCode == null || boxedOpCode >= _NUM_OP) { return OP_NONE; } return boxedOpCode; } /** * Gets the app op name associated with a given permission. * The app op name is one of the public constants defined * in this class such as {@code #OPSTR_COARSE_LOCATION}. * This API is intended to be used for mapping runtime * permissions to the corresponding app op. * * @param permission The permission. * @return The app op associated with the permission or null. */ @Nullable public static String permissionToOp(String permission) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { return AppOpsManagerHidden.permissionToOp(permission); } // Fallback for Lollipop final int opCode = permissionToOpCode(permission); if (opCode == OP_NONE) { return null; } return sOpToString[opCode]; } public static class PackageOps implements Parcelable { private final String mPackageName; private final int mUid; private final List mEntries; public PackageOps(String packageName, int uid, List entries) { mPackageName = packageName; mUid = uid; mEntries = entries; } public String getPackageName() { return mPackageName; } public int getUid() { return mUid; } public List getOps() { return mEntries; } @NonNull @Override public String toString() { return "PackageOps{" + "mPackageName='" + mPackageName + '\'' + ", mUid=" + mUid + ", mEntries=" + mEntries + '}'; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeString(mPackageName); dest.writeInt(mUid); dest.writeTypedList(mEntries); } protected PackageOps(@NonNull Parcel in) { mPackageName = in.readString(); mUid = in.readInt(); mEntries = new ArrayList<>(); in.readTypedList(mEntries, OpEntry.CREATOR); } public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { @NonNull @Override public PackageOps createFromParcel(Parcel source) { return new PackageOps(source); } @NonNull @Override public PackageOps[] newArray(int size) { return new PackageOps[size]; } }; } public static class OpEntry implements Parcelable { private final AppOpsManagerHidden.OpEntry mOpEntry; public OpEntry(Parcelable opEntry) { mOpEntry = Refine.unsafeCast(opEntry); } protected OpEntry(Parcel in) { mOpEntry = ParcelCompat.readParcelable(in, AppOpsManagerHidden.OpEntry.class.getClassLoader(), AppOpsManagerHidden.OpEntry.class); } public static final Creator CREATOR = new Creator() { @NonNull @Override public OpEntry createFromParcel(Parcel in) { return new OpEntry(in); } @NonNull @Override public OpEntry[] newArray(int size) { return new OpEntry[size]; } }; public int getOp() { return mOpEntry.getOp(); } @NonNull public String getName() { return opToName(getOp()); } @Nullable public String getPermission() { return opToPermission(getOp()); } @Mode public int getMode() { return mOpEntry.getMode(); } @Mode public int getDefaultMode() { return opToDefaultMode(getOp()); } public long getTime() { return getLastAccessTime(OP_FLAGS_ALL); } public long getLastAccessTime(@OpFlags int flags) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { return mOpEntry.getLastAccessTime(flags); } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) { return mOpEntry.getLastAccessTime(); } return mOpEntry.getTime(); } public long getLastAccessForegroundTime(@OpFlags int flags) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { return mOpEntry.getLastAccessForegroundTime(flags); } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) { return mOpEntry.getLastAccessForegroundTime(); } else return mOpEntry.getTime(); } public long getLastAccessBackgroundTime(@OpFlags int flags) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { return mOpEntry.getLastAccessBackgroundTime(flags); } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) { return mOpEntry.getLastAccessBackgroundTime(); } else return mOpEntry.getTime(); } public long getLastAccessTime(@UidState int fromUidState, @UidState int toUidState, @OpFlags int flags) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { return mOpEntry.getLastAccessTime(fromUidState, toUidState, flags); } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) { return mOpEntry.getLastTimeFor(fromUidState); } else return mOpEntry.getTime(); } public long getRejectTime() { return getLastRejectTime(OP_FLAGS_ALL); } public long getLastRejectTime(@OpFlags int flags) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { return mOpEntry.getLastRejectTime(flags); } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) { return mOpEntry.getLastRejectTime(); } else return mOpEntry.getRejectTime(); } public long getLastRejectForegroundTime(@OpFlags int flags) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { return mOpEntry.getLastRejectForegroundTime(flags); } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) { return mOpEntry.getLastRejectForegroundTime(); } else return mOpEntry.getRejectTime(); } public long getLastRejectBackgroundTime(@OpFlags int flags) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { return mOpEntry.getLastRejectBackgroundTime(flags); } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) { return mOpEntry.getLastRejectBackgroundTime(); } else return mOpEntry.getRejectTime(); } public long getLastRejectTime(@UidState int fromUidState, @UidState int toUidState, @OpFlags int flags) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { return mOpEntry.getLastRejectTime(fromUidState, toUidState, flags); } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) { return mOpEntry.getLastRejectTimeFor(fromUidState); } else return mOpEntry.getRejectTime(); } public boolean isRunning() { return mOpEntry.isRunning(); } public long getDuration() { return getLastDuration(MAX_PRIORITY_UID_STATE, MIN_PRIORITY_UID_STATE, OP_FLAGS_ALL); } @RequiresApi(Build.VERSION_CODES.R) public long getLastDuration(@OpFlags int flags) { return mOpEntry.getLastDuration(flags); } public long getLastForegroundDuration(@OpFlags int flags) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { return mOpEntry.getLastForegroundDuration(flags); } else return mOpEntry.getDuration(); } public long getLastBackgroundDuration(@OpFlags int flags) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { return mOpEntry.getLastBackgroundDuration(flags); } else return mOpEntry.getDuration(); } public long getLastDuration(@UidState int fromUidState, @UidState int toUidState, @OpFlags int flags) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { return mOpEntry.getLastDuration(fromUidState, toUidState, flags); } else return mOpEntry.getDuration(); } // Deprecated in R @Deprecated @RequiresApi(Build.VERSION_CODES.M) public int getProxyUid() { return mOpEntry.getProxyUid(); } // Deprecated in R @Deprecated public int getProxyUid(@UidState int uidState, @OpFlags int flags) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { return mOpEntry.getProxyUid(uidState, flags); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { return mOpEntry.getProxyUid(); } return 0; } @Deprecated @RequiresApi(Build.VERSION_CODES.M) @Nullable public String getProxyPackageName() { return mOpEntry.getProxyPackageName(); } // Deprecated in R @Deprecated @Nullable public String getProxyPackageName(@UidState int uidState, @OpFlags int flags) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { return mOpEntry.getProxyPackageName(uidState, flags); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { return mOpEntry.getProxyPackageName(); } return null; } // TODO(24/12/20): Get proxy info (From API 30) @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeParcelable(mOpEntry, flags); } } public static int getModeFromOpEntriesOrDefault(int op, @Nullable List opEntries) { if (op <= OP_NONE || op >= _NUM_OP || opEntries == null) { return AppOpsManager.MODE_IGNORED; } for (OpEntry opEntry : opEntries) { if (opEntry.getOp() == op) { return opEntry.getMode(); } } return opToDefaultMode(op); } @NonNull public static List getConfiguredOpsForPackage(@NonNull AppOpsManagerCompat appOpsManager, @NonNull String packageName, int uid) throws RemoteException { List packageOpsList = appOpsManager.getOpsForPackage(uid, packageName, null); if (packageOpsList.size() == 1) { return packageOpsList.get(0).getOps(); } return Collections.emptyList(); } private final IAppOpsService mAppOpsService; public AppOpsManagerCompat() { mAppOpsService = IAppOpsService.Stub.asInterface(ProxyBinder.getService(Context.APP_OPS_SERVICE)); } /** * Get the mode of operation of the given package or uid. This denotes the actual working state which is not * necessarily the same mode set using {@link #setMode(int, int, String, int)}. * * @param op One of the OP_* * @param uid User ID for the package(s) * @param packageName Name of the package * @return One of the MODE_* */ @AppOpsManagerCompat.Mode public int checkOperation(int op, int uid, String packageName) throws RemoteException { return mAppOpsService.checkOperation(op, uid, packageName); } /** * Same as {@link AppOpsManager#checkOpNoThrow(String, int, String)}. To be used with App Manager itself. */ @AppOpsManagerCompat.Mode public int checkOpNoThrow(int op, int uid, String packageName) { try { int mode = mAppOpsService.checkOperation(op, uid, packageName); return mode == AppOpsManager.MODE_FOREGROUND ? AppOpsManager.MODE_ALLOWED : mode; } catch (RemoteException e) { return ExUtils.rethrowFromSystemServer(e); } } @RequiresPermission(ManifestCompat.permission.GET_APP_OPS_STATS) public List getOpsForPackage(int uid, String packageName, @Nullable int[] ops) throws RemoteException { // Check using uid mode and package mode, override ops in package mode from uid mode List opEntries = new ArrayList<>(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { try { addAllRelevantOpEntriesWithNoOverride(opEntries, mAppOpsService.getUidOps(uid, ops)); } catch (NullPointerException e) { Log.e("AppOpsManagerCompat", "Could not get app ops for UID %d", e, uid); } } addAllRelevantOpEntriesWithNoOverride(opEntries, mAppOpsService.getOpsForPackage(uid, packageName, ops)); return Collections.singletonList(new AppOpsManagerCompat.PackageOps(packageName, uid, opEntries)); } @RequiresPermission(ManifestCompat.permission.GET_APP_OPS_STATS) @NonNull public List getPackagesForOps(int[] ops) throws RemoteException { List opsForPackage = mAppOpsService.getPackagesForOps(ops); List packageOpsList = new ArrayList<>(); if (opsForPackage != null) { for (Parcelable o : opsForPackage) { AppOpsManagerCompat.PackageOps packageOps = opsConvert(Refine.unsafeCast(o)); packageOpsList.add(packageOps); } } return packageOpsList; } @RequiresPermission("android.permission.MANAGE_APP_OPS_MODES") public void setMode(int op, int uid, String packageName, @AppOpsManagerCompat.Mode int mode) throws RemoteException { if (AppOpsManagerCompat.isMiuiOp(op) || Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { // Only package mode works in MIUI-only app ops and before Android M mAppOpsService.setMode(op, uid, packageName, mode); } else { // Set UID mode mAppOpsService.setUidMode(op, uid, mode); } } @RequiresPermission("android.permission.MANAGE_APP_OPS_MODES") public void resetAllModes(@UserIdInt int reqUserId, @NonNull String reqPackageName) throws RemoteException { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { mAppOpsService.resetAllModes(reqUserId, reqPackageName); } } private static void addAllRelevantOpEntriesWithNoOverride(final List opEntries, @Nullable final List opsForPackage) { if (opsForPackage != null) { for (Parcelable o : opsForPackage) { AppOpsManagerCompat.PackageOps packageOps = opsConvert(Refine.unsafeCast(o)); for (AppOpsManagerCompat.OpEntry opEntry : packageOps.getOps()) { if (!opEntries.contains(opEntry)) { opEntries.add(opEntry); } } } } } @NonNull private static AppOpsManagerCompat.PackageOps opsConvert(@NonNull AppOpsManagerHidden.PackageOps packageOps) { String packageName = packageOps.getPackageName(); int uid = packageOps.getUid(); List opEntries = new ArrayList<>(); for (Parcelable opEntry : packageOps.getOps()) { opEntries.add(new AppOpsManagerCompat.OpEntry(opEntry)); } return new AppOpsManagerCompat.PackageOps(packageName, uid, opEntries); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/compat/ApplicationInfoCompat.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.compat; import android.content.pm.ApplicationInfo; import android.content.pm.ApplicationInfoHidden; import android.content.pm.PackageManager; import android.os.Build; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import dev.rikka.tools.refine.Refine; import io.github.muntashirakon.io.Paths; public final class ApplicationInfoCompat { /** * Value for {@code #privateFlags}: true if the application is hidden via restrictions and for * most purposes is considered as not installed. */ public static final int PRIVATE_FLAG_HIDDEN = 1; /** * Value for {@code #privateFlags}: set to true if the application * has reported that it is heavy-weight, and thus can not participate in * the normal application lifecycle. * *

Comes from the * android.R.styleable#AndroidManifestApplication_cantSaveState * attribute of the <application> tag. */ public static final int PRIVATE_FLAG_CANT_SAVE_STATE = 1 << 1; /** * Value for {@code #privateFlags}: set to {@code true} if the application * is permitted to hold privileged permissions. */ public static final int PRIVATE_FLAG_PRIVILEGED = 1 << 3; /** * Value for {@code #privateFlags}: {@code true} if the application has any IntentFiler * with some data URI using HTTP or HTTPS with an associated VIEW action. */ public static final int PRIVATE_FLAG_HAS_DOMAIN_URLS = 1 << 4; /** * When set, the default data storage directory for this app is pointed at * the device-protected location. */ public static final int PRIVATE_FLAG_DEFAULT_TO_DEVICE_PROTECTED_STORAGE = 1 << 5; /** * When set, assume that all components under the given app are direct boot * aware, unless otherwise specified. */ public static final int PRIVATE_FLAG_DIRECT_BOOT_AWARE = 1 << 6; /** * Value for {@code #privateFlags}: {@code true} if the application is installed * as instant app. */ public static final int PRIVATE_FLAG_INSTANT = 1 << 7; /** * When set, at least one component inside this application is direct boot * aware. */ public static final int PRIVATE_FLAG_PARTIALLY_DIRECT_BOOT_AWARE = 1 << 8; /** * When set, signals that the application is required for the system user and should not be * uninstalled. */ public static final int PRIVATE_FLAG_REQUIRED_FOR_SYSTEM_USER = 1 << 9; /** * When set, the application explicitly requested that its activities be resizeable by default. * {@code android.R.styleable#AndroidManifestActivity_resizeableActivity} */ public static final int PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_RESIZEABLE = 1 << 10; /** * When set, the application explicitly requested that its activities *not* be resizeable by * default. * {@code android.R.styleable#AndroidManifestActivity_resizeableActivity} */ public static final int PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_UNRESIZEABLE = 1 << 11; /** * The application isn't requesting explicitly requesting for its activities to be resizeable or * non-resizeable by default. So, we are making it activities resizeable by default based on the * target SDK version of the app. * {@code android.R.styleable#AndroidManifestActivity_resizeableActivity} *

* NOTE: This only affects apps with target SDK >= N where the resizeableActivity attribute was * introduced. It shouldn't be confused with {@code ActivityInfo#RESIZE_MODE_FORCE_RESIZEABLE} * where certain pre-N apps are forced to the resizeable. */ public static final int PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_RESIZEABLE_VIA_SDK_VERSION = 1 << 12; /** * Value for {@code #privateFlags}: {@code true} means the OS should go ahead and * run full-data backup operations for the app even when it is in a * foreground-equivalent run state. Defaults to {@code false} if unspecified. */ public static final int PRIVATE_FLAG_BACKUP_IN_FOREGROUND = 1 << 13; /** * Value for {@code #privateFlags}: {@code true} means this application * contains a static shared library. Defaults to {@code false} if unspecified. */ public static final int PRIVATE_FLAG_STATIC_SHARED_LIBRARY = 1 << 14; /** * Value for {@code #privateFlags}: When set, the application will only have its splits loaded * if they are required to load a component. Splits can be loaded on demand using the * {@code Context#createContextForSplit(String)} API. */ public static final int PRIVATE_FLAG_ISOLATED_SPLIT_LOADING = 1 << 15; /** * Value for {@code #privateFlags}: When set, the application was installed as * a virtual preload. */ public static final int PRIVATE_FLAG_VIRTUAL_PRELOAD = 1 << 16; /** * Value for {@code #privateFlags}: whether this app is pre-installed on the * OEM partition of the system image. */ public static final int PRIVATE_FLAG_OEM = 1 << 17; /** * Value for {@code #privateFlags}: whether this app is pre-installed on the * vendor partition of the system image. */ public static final int PRIVATE_FLAG_VENDOR = 1 << 18; /** * Value for {@code #privateFlags}: whether this app is pre-installed on the * product partition of the system image. */ public static final int PRIVATE_FLAG_PRODUCT = 1 << 19; /** * Value for {@code #privateFlags}: whether this app is signed with the * platform key. */ public static final int PRIVATE_FLAG_SIGNED_WITH_PLATFORM_KEY = 1 << 20; /** * Value for {@code #privateFlags}: whether this app is pre-installed on the * system_ext partition of the system image. */ public static final int PRIVATE_FLAG_SYSTEM_EXT = 1 << 21; /** * Indicates whether this package requires access to non-SDK APIs. * Only system apps and tests are allowed to use this property. */ public static final int PRIVATE_FLAG_USES_NON_SDK_API = 1 << 22; /** * Indicates whether this application can be profiled by the shell user, * even when running on a device that is running in user mode. */ public static final int PRIVATE_FLAG_PROFILEABLE_BY_SHELL = 1 << 23; /** * Indicates whether this package requires access to non-SDK APIs. * Only system apps and tests are allowed to use this property. */ public static final int PRIVATE_FLAG_HAS_FRAGILE_USER_DATA = 1 << 24; /** * Indicates whether this application wants to use the embedded dex in the APK, rather than * extracted or locally compiled variants. This keeps the dex code protected by the APK * signature. Such apps will always run in JIT mode (same when they are first installed), and * the system will never generate ahead-of-time compiled code for them. Depending on the app's * workload, there may be some run time performance change, noteably the cold start time. */ public static final int PRIVATE_FLAG_USE_EMBEDDED_DEX = 1 << 25; /** * Value for {@code #privateFlags}: indicates whether this application's data will be cleared * on a failed restore. * *

Comes from the * android.R.styleable#AndroidManifestApplication_allowClearUserDataOnFailedRestore attribute * of the <application> tag. */ public static final int PRIVATE_FLAG_ALLOW_CLEAR_USER_DATA_ON_FAILED_RESTORE = 1 << 26; /** * Value for {@code #privateFlags}: true if the application allows its audio playback * to be captured by other apps. */ public static final int PRIVATE_FLAG_ALLOW_AUDIO_PLAYBACK_CAPTURE = 1 << 27; /** * Indicates whether this package is in fact a runtime resource overlay. */ public static final int PRIVATE_FLAG_IS_RESOURCE_OVERLAY = 1 << 28; /** * Value for {@code #privateFlags}: If {@code true} this app requests * full external storage access. The request may not be honored due to * policy or other reasons. */ public static final int PRIVATE_FLAG_REQUEST_LEGACY_EXTERNAL_STORAGE = 1 << 29; /** * Value for {@code #privateFlags}: whether this app is pre-installed on the * ODM partition of the system image. */ public static final int PRIVATE_FLAG_ODM = 1 << 30; /** * Value for {@code #privateFlags}: If {@code true} this app allows heap tagging. * {@code com.android.server.am.ProcessList#NATIVE_HEAP_POINTER_TAGGING} */ public static final int PRIVATE_FLAG_ALLOW_NATIVE_HEAP_POINTER_TAGGING = 1 << 31; @IntDef(flag = true, value = { PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_RESIZEABLE, PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_RESIZEABLE_VIA_SDK_VERSION, PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_UNRESIZEABLE, PRIVATE_FLAG_BACKUP_IN_FOREGROUND, PRIVATE_FLAG_CANT_SAVE_STATE, PRIVATE_FLAG_DEFAULT_TO_DEVICE_PROTECTED_STORAGE, PRIVATE_FLAG_DIRECT_BOOT_AWARE, PRIVATE_FLAG_HAS_DOMAIN_URLS, PRIVATE_FLAG_HIDDEN, PRIVATE_FLAG_INSTANT, PRIVATE_FLAG_IS_RESOURCE_OVERLAY, PRIVATE_FLAG_ISOLATED_SPLIT_LOADING, PRIVATE_FLAG_OEM, PRIVATE_FLAG_PARTIALLY_DIRECT_BOOT_AWARE, PRIVATE_FLAG_USE_EMBEDDED_DEX, PRIVATE_FLAG_PRIVILEGED, PRIVATE_FLAG_PRODUCT, PRIVATE_FLAG_SYSTEM_EXT, PRIVATE_FLAG_PROFILEABLE_BY_SHELL, PRIVATE_FLAG_REQUIRED_FOR_SYSTEM_USER, PRIVATE_FLAG_SIGNED_WITH_PLATFORM_KEY, PRIVATE_FLAG_STATIC_SHARED_LIBRARY, PRIVATE_FLAG_VENDOR, PRIVATE_FLAG_VIRTUAL_PRELOAD, PRIVATE_FLAG_HAS_FRAGILE_USER_DATA, PRIVATE_FLAG_ALLOW_CLEAR_USER_DATA_ON_FAILED_RESTORE, PRIVATE_FLAG_ALLOW_AUDIO_PLAYBACK_CAPTURE, PRIVATE_FLAG_REQUEST_LEGACY_EXTERNAL_STORAGE, PRIVATE_FLAG_ODM, PRIVATE_FLAG_ALLOW_NATIVE_HEAP_POINTER_TAGGING, }) @Retention(RetentionPolicy.SOURCE) public @interface ApplicationInfoPrivateFlags { } /** * Represents the default policy. The actual policy used will depend on other properties of * the application, e.g. the target SDK version. */ public static final int HIDDEN_API_ENFORCEMENT_DEFAULT = -1; /** * No API enforcement; the app can access the entire internal private API. Only for use by * system apps. */ public static final int HIDDEN_API_ENFORCEMENT_DISABLED = 0; /** * No API enforcement, but enable the detection logic and warnings. Observed behaviour is the * same as {@link #HIDDEN_API_ENFORCEMENT_DISABLED} but you may see warnings in the log when * APIs are accessed. */ public static final int HIDDEN_API_ENFORCEMENT_JUST_WARN = 1; /** * Dark grey list enforcement. Enforces the dark grey and black lists */ public static final int HIDDEN_API_ENFORCEMENT_ENABLED = 2; /** * Blacklist enforcement only. */ public static final int HIDDEN_API_ENFORCEMENT_BLACK = 3; @ApplicationInfoPrivateFlags public static int getPrivateFlags(@NonNull ApplicationInfo info) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { return Refine.unsafeCast(info).privateFlags; } return 0; } @SuppressWarnings("deprecation") public static String getSeInfo(@NonNull ApplicationInfo info) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { return Refine.unsafeCast(info).seInfo + Refine.unsafeCast(info).seInfoUser; } else return Refine.unsafeCast(info).seinfo; } @Nullable public static String getPrimaryCpuAbi(@NonNull ApplicationInfo info) { return Refine.unsafeCast(info).primaryCpuAbi; } @Nullable public static String getZygotePreloadName(@NonNull ApplicationInfo info) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { return Refine.unsafeCast(info).zygotePreloadName; } return null; } public static int getHiddenApiEnforcementPolicy(@NonNull ApplicationInfo info) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { return Refine.unsafeCast(info).getHiddenApiEnforcementPolicy(); } return HIDDEN_API_ENFORCEMENT_DISABLED; } public static boolean isSystemApp(@NonNull ApplicationInfo info) { return (info.flags & ApplicationInfo.FLAG_SYSTEM) != 0; } public static boolean isStopped(@NonNull ApplicationInfo info) { return (info.flags & ApplicationInfo.FLAG_STOPPED) != 0; } public static boolean isInstalled(@NonNull ApplicationInfo info) { return (info.flags & ApplicationInfo.FLAG_INSTALLED) != 0 && info.processName != null && Paths.exists(info.publicSourceDir); } public static boolean isOnlyDataInstalled(@NonNull ApplicationInfo info) { return (info.flags & ApplicationInfo.FLAG_INSTALLED) == 0 && !(info.processName != null && Paths.exists(info.publicSourceDir)); } public static boolean isTestOnly(@NonNull ApplicationInfo info) { return (info.flags & ApplicationInfo.FLAG_TEST_ONLY) != 0; } public static boolean isSuspended(@NonNull ApplicationInfo info) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { return (info.flags & ApplicationInfo.FLAG_SUSPENDED) != 0; } // Not supported return false; } public static boolean isHidden(@NonNull ApplicationInfo info) { return (getPrivateFlags(info) & PRIVATE_FLAG_HIDDEN) != 0; } public static boolean isStaticSharedLibrary(@NonNull ApplicationInfo info) { // Android 8+ return (getPrivateFlags(info) & PRIVATE_FLAG_STATIC_SHARED_LIBRARY) != 0; } public static boolean isPrivileged(@NonNull ApplicationInfo info) { return (getPrivateFlags(info) & PRIVATE_FLAG_PRIVILEGED) != 0; } public static boolean hasDomainUrls(@NonNull ApplicationInfo info) { return (getPrivateFlags(info) & PRIVATE_FLAG_HAS_DOMAIN_URLS) != 0; } /** * {@link ApplicationInfo#loadLabel(PackageManager)} can throw NPE for uninstalled apps in unprivileged mode. * * @return App label or package name if an error is occurred. */ @NonNull public static CharSequence loadLabelSafe(@NonNull ApplicationInfo info, @NonNull PackageManager pm) { if (Paths.exists(info.publicSourceDir)) { return info.loadLabel(pm); } return info.packageName; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/compat/BackupCompat.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.compat; import android.annotation.UserIdInt; import android.app.backup.IBackupManager; import android.os.Build; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.UserHandleHidden; import androidx.annotation.NonNull; import java.util.ArrayList; import java.util.List; import aosp.libcore.util.EmptyArray; import io.github.muntashirakon.AppManager.ipc.ProxyBinder; import io.github.muntashirakon.AppManager.utils.ArrayUtils; import io.github.muntashirakon.AppManager.utils.ExUtils; /** * A complete recreation of the `bu` command (i.e. com.android.commands.bu.Backup class) with support for setting a * file location. Although the help page of the command include an -f switch for file, it actually does not work with * the command and only intended for ADB itself. */ public final class BackupCompat { private BackupCompat() { } /** * @see IBackupManager#setBackupEnabledForUser(int, boolean) */ public static void setBackupEnabledForUser(@UserIdInt int userId, boolean isEnabled) { try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { getBackupManager().setBackupEnabledForUser(userId, isEnabled); } else { getBackupManager().setBackupEnabled(isEnabled); } } catch (RemoteException e) { ExUtils.rethrowFromSystemServer(e); } } /** * @see IBackupManager#isBackupEnabledForUser(int) */ public static boolean isBackupEnabledForUser(@UserIdInt int userId) { try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { return getBackupManager().isBackupEnabledForUser(userId); } if (UserHandleHidden.myUserId() == userId) { return getBackupManager().isBackupEnabled(); } // Multiuser backup only available since Android 10 return false; } catch (RemoteException e) { return ExUtils.rethrowFromSystemServer(e); } } public static boolean setBackupPassword(String currentPw, String newPw) { try { return getBackupManager().setBackupPassword(currentPw, newPw); } catch (RemoteException e) { return ExUtils.rethrowFromSystemServer(e); } } public static boolean hasBackupPassword() { try { return getBackupManager().hasBackupPassword(); } catch (RemoteException e) { return ExUtils.rethrowFromSystemServer(e); } } @SuppressWarnings("deprecation") public static void adbBackup(@UserIdInt int userId, ParcelFileDescriptor fd, boolean includeApks, boolean includeObbs, boolean includeShared, boolean doWidgets, boolean allApps, boolean allIncludesSystem, boolean doCompress, boolean doKeyValue, String[] packageNames) throws RemoteException { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { getBackupManager().adbBackup(userId, fd, includeApks, includeObbs, includeShared, doWidgets, allApps, allIncludesSystem, doCompress, doKeyValue, packageNames); } else { if (UserHandleHidden.myUserId() != userId) { throw new RemoteException("Backup only allowed for current user"); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { getBackupManager().adbBackup(fd, includeApks, includeObbs, includeShared, doWidgets, allApps, allIncludesSystem, doCompress, doKeyValue, packageNames); } else { getBackupManager().fullBackup(fd, includeApks, includeObbs, includeShared, doWidgets, allApps, allIncludesSystem, doCompress, packageNames); } } } @SuppressWarnings("deprecation") public static void adbRestore(@UserIdInt int userId, ParcelFileDescriptor fd) throws RemoteException { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { getBackupManager().adbRestore(userId, fd); } else { if (UserHandleHidden.myUserId() != userId) { throw new RemoteException("Backup only allowed for current user"); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { getBackupManager().adbRestore(fd); } else getBackupManager().fullRestore(fd); } } @SuppressWarnings("deprecation") public static boolean isAppEligibleForBackupForUser(@UserIdInt int userId, String packageName) { try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { return getBackupManager().isAppEligibleForBackupForUser(userId, packageName); } else { if (UserHandleHidden.myUserId() != userId) { // Multiuser support unavailable return false; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { return getBackupManager().isAppEligibleForBackup(packageName); } // In API 23 and earlier, set it to eligible by default return true; } } catch (RemoteException e) { return ExUtils.rethrowFromSystemServer(e); } } @SuppressWarnings("deprecation") @NonNull public static String[] filterAppsEligibleForBackupForUser(@UserIdInt int userId, @NonNull String[] packages) { IBackupManager backupManager = getBackupManager(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { return ArrayUtils.defeatNullable(ExUtils.exceptionAsNull(() -> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { backupManager.filterAppsEligibleForBackupForUser(userId, packages); } else { backupManager.filterAppsEligibleForBackup(packages); } return null; })); } if (UserHandleHidden.myUserId() != userId) { // Multiuser support unavailable return EmptyArray.STRING; } // Check individually List eligibleApps = new ArrayList<>(packages.length); for (String packageName : packages) { boolean isEligible; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { isEligible = Boolean.TRUE.equals(ExUtils.exceptionAsNull(() -> backupManager.isAppEligibleForBackup(packageName))); } else isEligible = true; if (isEligible) { eligibleApps.add(packageName); } } return eligibleApps.toArray(new String[0]); } public static IBackupManager getBackupManager() { return IBackupManager.Stub.asInterface(ProxyBinder.getService("backup")); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/compat/BinderCompat.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.compat; import android.os.Build; import android.os.IBinder; import android.os.IBinderHidden; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ShellCallback; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import java.io.FileDescriptor; import dev.rikka.tools.refine.Refine; public final class BinderCompat { /** * Execute a shell command on this object. This may be performed asynchrously from the caller; * the implementation must always call resultReceiver when finished. * * @param in The raw file descriptor that an input data stream can be read from. * @param out The raw file descriptor that normal command messages should be written to. * @param err The raw file descriptor that command error messages should be written to. * @param args Command-line arguments. * @param shellCallback Optional callback to the caller's shell to perform operations in it. * @param resultReceiver Called when the command has finished executing, with the result code. */ @SuppressWarnings("deprecation") @RequiresApi(Build.VERSION_CODES.N) public static void shellCommand(@NonNull IBinder binder, @NonNull FileDescriptor in, @NonNull FileDescriptor out, @NonNull FileDescriptor err, @NonNull String[] args, @Nullable ShellCallback shellCallback, @NonNull ResultReceiver resultReceiver) throws RemoteException { IBinderHidden binderHidden = Refine.unsafeCast(binder); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { binderHidden.shellCommand(in, out, err, args, shellCallback, resultReceiver); } else binderHidden.shellCommand(in, out, err, args, resultReceiver); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/compat/BiometricAuthenticatorsCompat.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.compat; import android.os.Build; import androidx.biometric.BiometricManager.Authenticators; public class BiometricAuthenticatorsCompat { public static final class Builder { private boolean mAllowWeak = false; private boolean mAllowStrong = false; private boolean mAllowDeviceCredential = false; private boolean mDeviceCredentialOnly = false; public Builder() { } public Builder allowEverything(boolean allow) { mAllowWeak = allow; mAllowDeviceCredential = allow; return this; } public Builder allowWeakBiometric(boolean allow) { mAllowWeak = allow; return this; } public Builder allowStrongBiometric(boolean allow) { mAllowStrong = allow; return this; } public Builder allowDeviceCredential(boolean allow) { mAllowDeviceCredential = allow; return this; } public Builder deviceCredentialOnly(boolean only) { mDeviceCredentialOnly = only; return this; } public int build() { if (mDeviceCredentialOnly) { return getDeviceCredentialOnlyFlags(); } int flags; if (mAllowWeak) { flags = Authenticators.BIOMETRIC_WEAK; } else if (mAllowStrong) { flags = Authenticators.BIOMETRIC_STRONG; } else flags = 0; if (mAllowDeviceCredential) { if (flags == 0) { return getDeviceCredentialOnlyFlags(); } if (flags == Authenticators.BIOMETRIC_STRONG && ( Build.VERSION.SDK_INT < Build.VERSION_CODES.P || Build.VERSION.SDK_INT > Build.VERSION_CODES.Q)) { flags = Authenticators.BIOMETRIC_WEAK; } return flags | Authenticators.DEVICE_CREDENTIAL; } return flags; } private int getDeviceCredentialOnlyFlags() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { return Authenticators.DEVICE_CREDENTIAL; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { return Authenticators.BIOMETRIC_WEAK | Authenticators.DEVICE_CREDENTIAL; } return Authenticators.BIOMETRIC_STRONG | Authenticators.DEVICE_CREDENTIAL; } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/compat/ClearDataObserver.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.compat; import android.content.pm.IPackageDataObserver; public class ClearDataObserver extends IPackageDataObserver.Stub { private boolean mCompleted; private boolean mSuccessful; @Override public void onRemoveCompleted(String packageName, boolean succeeded) { synchronized (this) { mCompleted = true; mSuccessful = succeeded; notifyAll(); } } public boolean isCompleted() { return mCompleted; } public boolean isSuccessful() { return mSuccessful; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/compat/ConnectivityManagerCompat.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.compat; import android.annotation.SuppressLint; import android.content.Context; import android.net.ConnectivityManagerHidden; import android.net.IConnectivityManager; import android.os.Build; import android.os.RemoteException; import androidx.annotation.IntDef; import androidx.annotation.RequiresApi; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import io.github.muntashirakon.AppManager.ipc.ProxyBinder; public class ConnectivityManagerCompat { @SuppressLint("UniqueConstants") @Retention(RetentionPolicy.SOURCE) @IntDef(value = { ConnectivityManagerHidden.FIREWALL_CHAIN_DOZABLE, ConnectivityManagerHidden.FIREWALL_CHAIN_STANDBY, ConnectivityManagerHidden.FIREWALL_CHAIN_POWERSAVE, ConnectivityManagerHidden.FIREWALL_CHAIN_RESTRICTED, ConnectivityManagerHidden.FIREWALL_CHAIN_LOW_POWER_STANDBY, ConnectivityManagerHidden.FIREWALL_CHAIN_LOCKDOWN_VPN, ConnectivityManagerHidden.FIREWALL_CHAIN_BACKGROUND, ConnectivityManagerHidden.FIREWALL_CHAIN_OEM_DENY_1, ConnectivityManagerHidden.FIREWALL_CHAIN_OEM_DENY_2, ConnectivityManagerHidden.FIREWALL_CHAIN_OEM_DENY_3, }) @RequiresApi(Build.VERSION_CODES.TIRAMISU) public @interface FirewallChain { } @Retention(RetentionPolicy.SOURCE) @IntDef(value = { ConnectivityManagerHidden.FIREWALL_RULE_DEFAULT, ConnectivityManagerHidden.FIREWALL_RULE_ALLOW, ConnectivityManagerHidden.FIREWALL_RULE_DENY }) @RequiresApi(Build.VERSION_CODES.TIRAMISU) public @interface FirewallRule { } @RequiresApi(Build.VERSION_CODES.TIRAMISU) public static void setUidFirewallRule(@FirewallChain int chain, int uid, @FirewallRule int rule) throws RemoteException { getConnectivityManager().setUidFirewallRule(chain, uid, rule); } @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) @FirewallRule public static int getUidFirewallRule(@FirewallChain int chain, int uid) throws RemoteException { return getConnectivityManager().getUidFirewallRule(chain, uid); } @RequiresApi(Build.VERSION_CODES.TIRAMISU) public static void setFirewallChainEnabled(@FirewallChain int chain, boolean enable) throws RemoteException { getConnectivityManager().setFirewallChainEnabled(chain, enable); } @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public static boolean getFirewallChainEnabled(@FirewallChain int chain) throws RemoteException { return getConnectivityManager().getFirewallChainEnabled(chain); } @RequiresApi(Build.VERSION_CODES.TIRAMISU) public static void replaceFirewallChain(@FirewallChain int chain, int[] uids) throws RemoteException { getConnectivityManager().replaceFirewallChain(chain, uids); } private static IConnectivityManager getConnectivityManager() { return IConnectivityManager.Stub.asInterface(ProxyBinder.getService(Context.CONNECTIVITY_SERVICE)); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/compat/DeviceIdleManagerCompat.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.compat; import android.os.Build; import android.os.IDeviceIdleController; import android.os.RemoteException; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.annotation.RequiresPermission; import io.github.muntashirakon.AppManager.ipc.ProxyBinder; import io.github.muntashirakon.AppManager.utils.ExUtils; public final class DeviceIdleManagerCompat { @RequiresPermission(ManifestCompat.permission.DEVICE_POWER) public static boolean disableBatteryOptimization(@NonNull String packageName) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { try { getDeviceIdleController().addPowerSaveWhitelistApp(packageName); return true; // returns true when the package isn't installed } catch (RemoteException e) { ExUtils.rethrowFromSystemServer(e); } } return false; } @RequiresPermission(ManifestCompat.permission.DEVICE_POWER) public static boolean enableBatteryOptimization(@NonNull String packageName) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { try { getDeviceIdleController().removePowerSaveWhitelistApp(packageName); return true; } catch (RemoteException e) { ExUtils.rethrowFromSystemServer(e); } catch (UnsupportedOperationException e) { // System whitelisted app e.printStackTrace(); } } return false; } public static boolean isBatteryOptimizedApp(@NonNull String packageName) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { try { IDeviceIdleController controller = getDeviceIdleController(); return !controller.isPowerSaveWhitelistExceptIdleApp(packageName) && !controller.isPowerSaveWhitelistApp(packageName); } catch (RemoteException e) { ExUtils.rethrowFromSystemServer(e); } } // Not supported return true; } @RequiresApi(Build.VERSION_CODES.M) private static IDeviceIdleController getDeviceIdleController() { return IDeviceIdleController.Stub.asInterface(ProxyBinder.getService("deviceidle")); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/compat/DomainVerificationManagerCompat.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.compat; import android.content.Context; import android.content.pm.PackageManager; import android.content.pm.verify.domain.DomainVerificationUserState; import android.content.pm.verify.domain.IDomainVerificationManager; import android.os.Build; import android.os.RemoteException; import android.os.ServiceSpecificException; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.RequiresPermission; import io.github.muntashirakon.AppManager.ipc.ProxyBinder; @RequiresApi(Build.VERSION_CODES.S) public class DomainVerificationManagerCompat { @Nullable public static DomainVerificationUserState getDomainVerificationUserState(String packageName, int userId) { try { return getDomainVerificationManager().getDomainVerificationUserState(packageName, userId); } catch (Throwable ignore) { } return null; } @RequiresPermission(ManifestCompat.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION) public static void setDomainVerificationLinkHandlingAllowed(String packageName, boolean allowed, int userId) throws RemoteException, PackageManager.NameNotFoundException { try { getDomainVerificationManager().setDomainVerificationLinkHandlingAllowed(packageName, allowed, userId); } catch (ServiceSpecificException e) { int serviceSpecificErrorCode = e.errorCode; if (packageName == null) { packageName = e.getMessage(); } if (serviceSpecificErrorCode == 1) { throw new PackageManager.NameNotFoundException(packageName); } throw e; } } public static IDomainVerificationManager getDomainVerificationManager() { return IDomainVerificationManager.Stub.asInterface(ProxyBinder.getService(Context.DOMAIN_VERIFICATION_SERVICE)); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/compat/InputManagerCompat.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.compat; import android.content.Context; import android.hardware.input.IInputManager; import android.hardware.input.InputManagerHidden; import android.os.Build; import android.os.RemoteException; import android.os.SystemClock; import android.view.InputDevice; import android.view.InputEvent; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.ViewConfiguration; import androidx.annotation.NonNull; import androidx.annotation.RequiresPermission; import io.github.muntashirakon.AppManager.ipc.ProxyBinder; // Based on https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/services/core/java/com/android/server/input/InputShellCommand.java;l=350;drc=0b80090e02814093f2187c2ce7e64f87cb917edc public class InputManagerCompat { @RequiresPermission(ManifestCompat.permission.INJECT_EVENTS) public static boolean sendKeyEvent(int keyCode, boolean longpress) { long now = SystemClock.uptimeMillis(); KeyEvent event = new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, InputDevice.SOURCE_UNKNOWN); boolean success = true; success &= injectKeyEvent(event); if (longpress) { sleep(ViewConfiguration.getLongPressTimeout()); // Some long press behavior would check the event time, we set a new event time here. long nextEventTime = now + ViewConfiguration.getLongPressTimeout(); success &=injectKeyEvent(KeyEvent.changeTimeRepeat(event, nextEventTime, 1, KeyEvent.FLAG_LONG_PRESS)); } success &= injectKeyEvent(KeyEvent.changeAction(event, KeyEvent.ACTION_UP)); return success; } @RequiresPermission(ManifestCompat.permission.INJECT_EVENTS) public static boolean injectKeyEvent(KeyEvent event) { return injectInputEvent(event, InputManagerHidden.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH, -1); } @RequiresPermission(ManifestCompat.permission.INJECT_EVENTS) public static boolean injectInputEvent(@NonNull InputEvent event, int mode, int targetUid) { IInputManager inputManager = getInputManager(); try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { return inputManager.injectInputEventToTarget(event, mode, targetUid); } else return inputManager.injectInputEvent(event, mode); } catch (RemoteException e) { return false; } } /** * Puts the thread to sleep for the provided time. * * @param milliseconds The time to sleep in milliseconds. */ private static void sleep(long milliseconds) { try { Thread.sleep(milliseconds); } catch (InterruptedException e) { throw new RuntimeException(e); } } private static IInputManager getInputManager() { return IInputManager.Stub.asInterface(ProxyBinder.getService(Context.INPUT_SERVICE)); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/compat/InstallSourceInfoCompat.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.compat; import android.content.pm.InstallSourceInfo; import android.content.pm.PackageManager; import android.content.pm.SigningInfo; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.core.os.ParcelCompat; public class InstallSourceInfoCompat implements Parcelable { @Nullable private final String mInitiatingPackageName; @RequiresApi(Build.VERSION_CODES.P) @Nullable private SigningInfo mInitiatingPackageSigningInfo; @Nullable private final String mOriginatingPackageName; @Nullable private final String mInstallingPackageName; @Nullable private CharSequence mInitiatingPackageLabel; @Nullable private CharSequence mOriginatingPackageLabel; @Nullable private CharSequence mInstallingPackageLabel; @RequiresApi(Build.VERSION_CODES.R) public InstallSourceInfoCompat(@Nullable InstallSourceInfo installSourceInfo) { if (installSourceInfo != null) { mInitiatingPackageName = installSourceInfo.getInitiatingPackageName(); mInitiatingPackageSigningInfo = installSourceInfo.getInitiatingPackageSigningInfo(); mOriginatingPackageName = installSourceInfo.getOriginatingPackageName(); mInstallingPackageName = installSourceInfo.getInstallingPackageName(); } else { mInitiatingPackageName = null; mOriginatingPackageName = null; mInstallingPackageName = null; } } public InstallSourceInfoCompat(@Nullable String installingPackageName) { mInitiatingPackageName = null; mOriginatingPackageName = null; mInstallingPackageName = installingPackageName; } @Override public int describeContents() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && mInitiatingPackageSigningInfo != null) { return mInitiatingPackageSigningInfo.describeContents(); } return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeString(mInitiatingPackageName); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { dest.writeParcelable(mInitiatingPackageSigningInfo, flags); } dest.writeString(mOriginatingPackageName); dest.writeString(mInstallingPackageName); } private InstallSourceInfoCompat(Parcel source) { mInitiatingPackageName = source.readString(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { mInitiatingPackageSigningInfo = ParcelCompat.readParcelable(source, SigningInfo.class.getClassLoader(), SigningInfo.class); } mOriginatingPackageName = source.readString(); mInstallingPackageName = source.readString(); } public void setInitiatingPackageLabel(@Nullable CharSequence label) { mInitiatingPackageLabel = label; } @Nullable public CharSequence getInitiatingPackageLabel() { return mInitiatingPackageLabel; } public void setOriginatingPackageLabel(@Nullable CharSequence label) { mOriginatingPackageLabel = label; } @Nullable public CharSequence getOriginatingPackageLabel() { return mOriginatingPackageLabel; } public void setInstallingPackageLabel(@Nullable CharSequence label) { mInstallingPackageLabel = label; } @Nullable public CharSequence getInstallingPackageLabel() { return mInstallingPackageLabel; } /** * The name of the package that requested the installation, or null if not available. *

* This is normally the same as the installing package name. If the installing package name * is changed, for example by calling * {@link PackageManager#setInstallerPackageName(String, String)}, the initiating package name * remains unchanged. It continues to identify the actual package that performed the install * or update. *

* Null may be returned if the app was not installed by a package (e.g. a system app or an app * installed via adb) or if the initiating package has itself been uninstalled. */ @Nullable public String getInitiatingPackageName() { return mInitiatingPackageName; } /** * Information about the signing certificates used to sign the initiating package, if available. */ @RequiresApi(Build.VERSION_CODES.P) @Nullable public SigningInfo getInitiatingPackageSigningInfo() { return mInitiatingPackageSigningInfo; } /** * The name of the package on behalf of which the initiating package requested the installation, * or null if not available. *

* For example if a downloaded APK is installed via the Package Installer this could be the * app that performed the download. This value is provided by the initiating package and not * verified by the framework. *

* Note that the {@code InstallSourceInfo} returned by * {@link PackageManager#getInstallSourceInfo(String)} will not have this information * available unless the calling application holds the INSTALL_PACKAGES permission. */ @Nullable public String getOriginatingPackageName() { return mOriginatingPackageName; } /** * The name of the package responsible for the installation (the installer of record), or null * if not available. * Note that this may differ from the initiating package name and can be modified via * {@link PackageManager#setInstallerPackageName(String, String)}. *

* Null may be returned if the app was not installed by a package (e.g. a system app or an app * installed via adb) or if the installing package has itself been uninstalled. */ @Nullable public String getInstallingPackageName() { return mInstallingPackageName; } @NonNull public static final Parcelable.Creator CREATOR = new Creator() { @Override public InstallSourceInfoCompat createFromParcel(Parcel source) { return new InstallSourceInfoCompat(source); } @Override public InstallSourceInfoCompat[] newArray(int size) { return new InstallSourceInfoCompat[size]; } }; } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/compat/IntegerCompat.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.compat; import androidx.annotation.NonNull; public class IntegerCompat { /** * Return a 0x prefixed signed hex. */ @NonNull public static String toSignedHex(int signedInt) { StringBuilder sb = new StringBuilder(); sb.append(Integer.toString(signedInt, 16)); sb.insert(sb.charAt(0) == '-' ? 1 : 0, "0x"); return sb.toString(); } /** * Return a 0x prefixed unsigned hex. */ @NonNull public static String toUnsignedHex(int signedInt) { return "0x" + Integer.toHexString(signedInt); } /** * Same as {@link Integer#decode(String)} except it allows decoding both signed and unsigned values */ public static int decode(@NonNull String nm) throws NumberFormatException { int radix = 10; int index = 0; if (nm.length() == 0) { throw new NumberFormatException("Zero length string"); } char firstChar = nm.charAt(0); // Handle sign, if present if (firstChar == '-') { // First character is a signed character, use regular decoding return Integer.decode(nm); } else if (firstChar == '+') { index++; } // Handle radix specifier, if present if (nm.startsWith("0x", index) || nm.startsWith("0X", index)) { index += 2; radix = 16; } else if (nm.startsWith("#", index)) { index++; radix = 16; } else if (nm.startsWith("0", index) && nm.length() > 1 + index) { index++; radix = 8; } if (nm.startsWith("-", index) || nm.startsWith("+", index)) { throw new NumberFormatException("Sign character in wrong position"); } return Integer.parseUnsignedInt(nm.substring(index), radix); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/compat/ManifestCompat.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.compat; import android.os.Build; import androidx.annotation.RequiresApi; public final class ManifestCompat { public static final class permission { public static final String TERMUX_RUN_COMMAND = "com.termux.permission.RUN_COMMAND"; @RequiresApi(Build.VERSION_CODES.Q) public static final String ADJUST_RUNTIME_PERMISSIONS_POLICY = "android.permission.ADJUST_RUNTIME_PERMISSIONS_POLICY"; public static final String BACKUP = "android.permission.BACKUP"; @RequiresApi(Build.VERSION_CODES.O) public static final String CHANGE_OVERLAY_PACKAGES = "android.permission.CHANGE_OVERLAY_PACKAGES"; public static final String CLEAR_APP_USER_DATA = "android.permission.CLEAR_APP_USER_DATA"; @RequiresApi(Build.VERSION_CODES.N) public static final String CREATE_USERS = "android.permission.CREATE_USERS"; public static final String DEVICE_POWER = "android.permission.DEVICE_POWER"; public static final String FORCE_STOP_PACKAGES = "android.permission.FORCE_STOP_PACKAGES"; public static final String GET_APP_OPS_STATS = "android.permission.GET_APP_OPS_STATS"; @RequiresApi(Build.VERSION_CODES.TIRAMISU) public static final String GET_HISTORICAL_APP_OPS_STATS = "android.permission.GET_HISTORICAL_APP_OPS_STATS"; public static final String GET_RUNTIME_PERMISSIONS = "android.permission.GET_RUNTIME_PERMISSIONS"; public static final String GRANT_RUNTIME_PERMISSIONS = "android.permission.GRANT_RUNTIME_PERMISSIONS"; public static final String INJECT_EVENTS = "android.permission.INJECT_EVENTS"; @RequiresApi(Build.VERSION_CODES.Q) public static final String INSTALL_EXISTING_PACKAGES = "com.android.permission.INSTALL_EXISTING_PACKAGES"; public static final String INTERACT_ACROSS_USERS = "android.permission.INTERACT_ACROSS_USERS"; public static final String INTERACT_ACROSS_USERS_FULL = "android.permission.INTERACT_ACROSS_USERS_FULL"; @RequiresApi(Build.VERSION_CODES.P) public static final String INTERNAL_DELETE_CACHE_FILES = "android.permission.INTERNAL_DELETE_CACHE_FILES"; @RequiresApi(Build.VERSION_CODES.M) public static final String KILL_UID = "android.permission.KILL_UID"; @RequiresApi(Build.VERSION_CODES.N) public static final String MANAGE_APP_OPS_RESTRICTIONS = "android.permission.MANAGE_APP_OPS_RESTRICTIONS"; @RequiresApi(Build.VERSION_CODES.P) public static final String MANAGE_APP_OPS_MODES = "android.permission.MANAGE_APP_OPS_MODES"; @RequiresApi(Build.VERSION_CODES.Q) public static final String MANAGE_APPOPS = "android.permission.MANAGE_APPOPS"; public static final String MANAGE_NETWORK_POLICY = "android.permission.MANAGE_NETWORK_POLICY"; @RequiresApi(Build.VERSION_CODES.S) public static final String MANAGE_NOTIFICATION_LISTENERS = "android.permission.MANAGE_NOTIFICATION_LISTENERS"; @RequiresApi(Build.VERSION_CODES.P) public static final String MANAGE_SENSORS = "android.permission.MANAGE_SENSORS"; public static final String MANAGE_USERS = "android.permission.MANAGE_USERS"; public static final String READ_PRIVILEGED_PHONE_STATE = "android.permission.READ_PRIVILEGED_PHONE_STATE"; public static final String REAL_GET_TASKS = "android.permission.REAL_GET_TASKS"; public static final String REVOKE_RUNTIME_PERMISSIONS = "android.permission.REVOKE_RUNTIME_PERMISSIONS"; public static final String START_ANY_ACTIVITY = "android.permission.START_ANY_ACTIVITY"; @RequiresApi(Build.VERSION_CODES.P) public static final String SUSPEND_APPS = "android.permission.SUSPEND_APPS"; public static final String UPDATE_APP_OPS_STATS = "android.permission.UPDATE_APP_OPS_STATS"; @RequiresApi(Build.VERSION_CODES.S) public static final String UPDATE_DOMAIN_VERIFICATION_USER_SELECTION = "android.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION"; @RequiresApi(Build.VERSION_CODES.P) public static final String WATCH_APPOPS = "android.permission.WATCH_APPOPS"; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/compat/NetworkPolicyManagerCompat.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.compat; import android.content.Context; import android.net.INetworkPolicyManager; import android.net.NetworkPolicyManager; import android.os.RemoteException; import android.os.UserHandleHidden; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.RequiresPermission; import androidx.collection.ArrayMap; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Field; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.ipc.ProxyBinder; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.utils.ExUtils; public final class NetworkPolicyManagerCompat { public static final String TAG = NetworkPolicyManagerCompat.class.getSimpleName(); /* * The policies below are taken from LineageOS * Source: https://github.com/LineageOS/android_frameworks_base/blob/lineage-18.1/core/java/android/net/NetworkPolicyManager.java */ /** * Reject network usage on Wi-Fi network. {@code POLICY_REJECT_ON_WLAN} up to Lineage 17.1 (Android 10) */ public static final int POLICY_LOS_REJECT_WIFI = 1 << 15; /** * Reject network usage on cellular network. {@code POLICY_REJECT_ON_DATA} up to Lineage 17.1 (Android 10) */ public static final int POLICY_LOS_REJECT_CELLULAR = 1 << 16; /** * Reject network usage on virtual private network. {@code POLICY_REJECT_ON_VPN} up to Lineage 17.1 (Android 10) */ public static final int POLICY_LOS_REJECT_VPN = 1 << 17; /** * Reject network usage on all networks. {@code POLICY_NETWORK_ISOLATED} up to Lineage 17.1 (Android 10) */ public static final int POLICY_LOS_REJECT_ALL = 1 << 18; // The following are taken from Motorola device (Android 12) public static final int POLICY_MOTO_REJECT_METERED = 1 << 1; public static final int POLICY_MOTO_REJECT_BACKGROUND = 1 << 5; public static final int POLICY_MOTO_REJECT_ALL = 1 << 6; // The following are taken from Samsung device (Android 10) public static final int POLICY_ONE_UI_ALLOW_METERED_IN_ROAMING = 1001; public static final int POLICY_ONE_UI_ALLOW_WHITELIST_IN_ROAMING = 1002; @IntDef(flag = true, value = { NetworkPolicyManager.POLICY_NONE, NetworkPolicyManager.POLICY_REJECT_METERED_BACKGROUND, NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND, // Lineage OS POLICY_LOS_REJECT_WIFI, POLICY_LOS_REJECT_CELLULAR, POLICY_LOS_REJECT_VPN, POLICY_LOS_REJECT_ALL, // Motorola POLICY_MOTO_REJECT_METERED, POLICY_MOTO_REJECT_BACKGROUND, POLICY_MOTO_REJECT_ALL, // Samsung POLICY_ONE_UI_ALLOW_METERED_IN_ROAMING, POLICY_ONE_UI_ALLOW_WHITELIST_IN_ROAMING, }) @Retention(RetentionPolicy.SOURCE) public @interface NetPolicy { } private static final ArrayMap sNetworkPolicies = new ArrayMap() { { for (Field field : NetworkPolicyManager.class.getFields()) { if (field.getName().startsWith("POLICY_")) { try { put(field.getInt(null), field.getName()); } catch (IllegalAccessException ignore) { } } } } }; @NetPolicy @RequiresPermission(ManifestCompat.permission.MANAGE_NETWORK_POLICY) public static int getUidPolicy(int uid) { try { return getNetPolicyManager().getUidPolicy(uid); } catch (RemoteException e) { return ExUtils.rethrowFromSystemServer(e); } } @RequiresPermission(ManifestCompat.permission.MANAGE_NETWORK_POLICY) public static void setUidPolicy(int uid, int policies) { if (UserHandleHidden.isApp(uid)) { try { getNetPolicyManager().setUidPolicy(uid, policies); } catch (RemoteException e) { ExUtils.rethrowFromSystemServer(e); } } else { Log.w(TAG, "Cannot set policy %d to uid %d", policies, uid); } } @NonNull public static ArrayMap getReadablePolicies(@NonNull Context context, int policies) { ArrayMap readablePolicies = new ArrayMap<>(); if (policies == 0) { readablePolicies.put(NetworkPolicyManager.POLICY_NONE, context.getString(R.string.none)); return readablePolicies; } for (int i = 0; i < sNetworkPolicies.size(); ++i) { int policy = sNetworkPolicies.keyAt(i); if (!hasPolicy(policies, policy)) { continue; } String policyName = sNetworkPolicies.valueAt(i); String readablePolicyName = getReadablePolicyName(context, policy, policyName); readablePolicies.put(policy, readablePolicyName); } return readablePolicies; } @NonNull public static ArrayMap getAllReadablePolicies(@NonNull Context context) { ArrayMap readablePolicies = new ArrayMap<>(); for (int i = 0; i < sNetworkPolicies.size(); ++i) { int policy = sNetworkPolicies.keyAt(i); String policyName = sNetworkPolicies.valueAt(i); String readablePolicyName = getReadablePolicyName(context, policy, policyName); readablePolicies.put(policy, readablePolicyName); } return readablePolicies; } private static INetworkPolicyManager getNetPolicyManager() { return INetworkPolicyManager.Stub.asInterface(ProxyBinder.getService("netpolicy")); } private static boolean hasPolicy(int policies, int policy) { return (policies & policy) != 0; } private static String getReadablePolicyName(@NonNull Context context, int policy, @NonNull String policyName) { switch (policy) { case NetworkPolicyManager.POLICY_NONE: return context.getString(R.string.none); case NetworkPolicyManager.POLICY_REJECT_METERED_BACKGROUND: return context.getString(R.string.netpolicy_reject_metered_background_data); case NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND: return context.getString(R.string.netpolicy_allow_metered_background_data); case POLICY_LOS_REJECT_WIFI: if (policyName.equals("POLICY_REJECT_ON_WLAN") || policyName.equals("POLICY_REJECT_WIFI")) { return context.getString(R.string.netpolicy_reject_wifi_data); } break; case POLICY_LOS_REJECT_CELLULAR: if (policyName.equals("POLICY_REJECT_ON_DATA") || policyName.equals("POLICY_REJECT_CELLULAR")) { return context.getString(R.string.netpolicy_reject_cellular_data); } break; case POLICY_LOS_REJECT_VPN: if (policyName.equals("POLICY_REJECT_ON_VPN") || policyName.equals("POLICY_REJECT_VPN")) { return context.getString(R.string.netpolicy_reject_vpn_data); } break; case POLICY_LOS_REJECT_ALL: if (policyName.equals("POLICY_NETWORK_ISOLATED") || policyName.equals("POLICY_REJECT_ALL")) { return context.getString(R.string.netpolicy_disable_network_access); } break; case POLICY_MOTO_REJECT_METERED: if (policyName.equals("POLICY_REJECT_METERED")) { return context.getString(R.string.netpolicy_reject_metered_data); } break; case POLICY_MOTO_REJECT_BACKGROUND: if (policyName.equals("POLICY_REJECT_BACKGROUND")) { return context.getString(R.string.netpolicy_reject_background_data); } break; case POLICY_MOTO_REJECT_ALL: if (policyName.equals("POLICY_REJECT_ALL")) { return context.getString(R.string.netpolicy_disable_network_access); } break; } return context.getString(R.string.unknown_net_policy, policyName, policy); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/compat/NetworkStatsCompat.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.compat; import android.net.INetworkStatsService; import android.net.INetworkStatsSession; import android.net.NetworkStats; import android.net.NetworkTemplate; import android.os.Build; import android.os.RemoteException; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.settings.Ops; import io.github.muntashirakon.AppManager.users.Users; public class NetworkStatsCompat implements AutoCloseable { private final NetworkTemplate mTemplate; private final long mStartTime; private final long mEndTime; @Nullable private INetworkStatsSession mSession; private int mIndex; @Nullable private NetworkStats mSummary; @Nullable private NetworkStats.Entry mSummaryEntry; NetworkStatsCompat(@NonNull NetworkTemplate template, int flags, long startTime, long endTime, @NonNull INetworkStatsService statsService) throws RemoteException, SecurityException { mTemplate = template; mStartTime = startTime; mEndTime = endTime; int callingUid = Users.getSelfOrRemoteUid(); String callingPackage = SelfPermissions.getCallingPackage(callingUid); if (callingUid == Ops.ROOT_UID) { mSession = statsService.openSession(); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { mSession = statsService.openSessionForUsageStats(flags, callingPackage); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { mSession = statsService.openSessionForUsageStats(callingPackage); } else mSession = statsService.openSession(); } public boolean hasNextEntry() { return mSummary != null && mIndex < mSummary.size(); } @Nullable public NetworkStats.Entry getNextEntry(boolean recycle) { if (mSummary == null) { return null; } mSummaryEntry = mSummary.getValues(mIndex, recycle ? mSummaryEntry : null); ++mIndex; return mSummaryEntry; } void startSummaryEnumeration() throws RemoteException { if (mSession != null) { mSummary = mSession.getSummaryForAllUid(mTemplate, mStartTime, mEndTime, false); } mIndex = 0; } @Override public void close() { if (mSession != null) { try { mSession.close(); } catch (RemoteException e) { e.printStackTrace(); // Otherwise, meh } } mSession = null; } @Override protected void finalize() throws Throwable { try { close(); } finally { super.finalize(); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/compat/NetworkStatsManagerCompat.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.compat; import android.Manifest; import android.content.Context; import android.net.INetworkStatsService; import android.net.NetworkCapabilities; import android.net.NetworkTemplate; import android.os.Build; import android.os.RemoteException; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.RequiresPermission; import io.github.muntashirakon.AppManager.ipc.ProxyBinder; public class NetworkStatsManagerCompat { @NonNull @RequiresApi(Build.VERSION_CODES.M) @RequiresPermission(Manifest.permission.PACKAGE_USAGE_STATS) public static NetworkStatsCompat querySummary(int networkType, @Nullable String subscriberId, long startTime, long endTime) throws RemoteException, SecurityException { INetworkStatsService statsService = INetworkStatsService.Stub.asInterface(ProxyBinder.getService(Context.NETWORK_STATS_SERVICE)); NetworkTemplate template = createTemplate(networkType, subscriberId); NetworkStatsCompat networkStats = new NetworkStatsCompat(template, 0, startTime, endTime, statsService); networkStats.startSummaryEnumeration(); return networkStats; } @NonNull private static NetworkTemplate createTemplate(int networkType, @Nullable String subscriberId) { final NetworkTemplate template; switch (networkType) { case NetworkCapabilities.TRANSPORT_CELLULAR: template = subscriberId == null ? NetworkTemplate.buildTemplateMobileWildcard() : NetworkTemplate.buildTemplateMobileAll(subscriberId); break; case NetworkCapabilities.TRANSPORT_WIFI: template = TextUtils.isEmpty(subscriberId) ? NetworkTemplate.buildTemplateWifiWildcard() : new NetworkTemplate(NetworkTemplate.MATCH_WIFI, subscriberId, null); break; default: throw new IllegalArgumentException("Cannot create template for network type " + networkType + "'."); } return template; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/compat/OverlayManagerCompact.java ================================================ package io.github.muntashirakon.AppManager.compat; import android.content.om.IOverlayManager; import android.os.Build; import androidx.annotation.RequiresApi; import androidx.annotation.RequiresPermission; import io.github.muntashirakon.AppManager.ipc.ProxyBinder; @RequiresApi(Build.VERSION_CODES.O) public class OverlayManagerCompact { @RequiresPermission(ManifestCompat.permission.CHANGE_OVERLAY_PACKAGES) public static IOverlayManager getOverlayManager() { return IOverlayManager.Stub.asInterface(ProxyBinder.getService("overlay")); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/compat/PackageInfoCompat2.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.compat; import android.content.pm.PackageInfo; import android.content.pm.PackageInfoHidden; import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.util.Optional; import dev.rikka.tools.refine.Refine; import io.github.muntashirakon.AppManager.utils.ExUtils; public class PackageInfoCompat2 { @Nullable public static String getOverlayTarget(@NonNull PackageInfo packageInfo) { return Refine.unsafeCast(packageInfo).overlayTarget; } @Nullable public static String getTargetOverlayableName(@NonNull PackageInfo packageInfo) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { return Refine.unsafeCast(packageInfo).targetOverlayableName; } return null; } @Nullable public static String getOverlayCategory(@NonNull PackageInfo packageInfo) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { return Refine.unsafeCast(packageInfo).overlayCategory; } return null; } public static int getOverlayPriority(@NonNull PackageInfo packageInfo) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { return Refine.unsafeCast(packageInfo).overlayPriority; } return 0; // MAX priority } public static boolean isStaticOverlayPackage(@NonNull PackageInfo packageInfo) { PackageInfoHidden info = Refine.unsafeCast(packageInfo); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { return info.isStaticOverlayPackage(); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { return Optional.ofNullable(ExUtils.exceptionAsNull(() -> info.isStaticOverlay)) .orElse((info.overlayFlags & PackageInfoHidden.FLAG_OVERLAY_STATIC) != 0); } // Static is by default return true; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/compat/PackageManagerCompat.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.compat; import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT; import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED; import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED; import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER; import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED; import static android.content.pm.PackageManager.DONT_KILL_APP; import static android.content.pm.PackageManager.SYNCHRONOUS; import android.Manifest; import android.annotation.SuppressLint; import android.annotation.UserIdInt; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.IPackageInstaller; import android.content.pm.IPackageManager; import android.content.pm.IPackageManagerN; import android.content.pm.LauncherActivityInfo; import android.content.pm.LauncherApps; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; import android.content.pm.PermissionInfo; import android.content.pm.ProviderInfo; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.pm.SuspendDialogInfo; import android.os.BadParcelableException; import android.os.Build; import android.os.DeadObjectException; import android.os.RemoteException; import android.os.SystemClock; import android.os.UserHandle; import android.os.UserHandleHidden; import android.util.AndroidException; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.RequiresPermission; import androidx.annotation.WorkerThread; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; import dev.rikka.tools.refine.Refine; import io.github.muntashirakon.AppManager.ipc.ProxyBinder; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.types.UserPackagePair; import io.github.muntashirakon.AppManager.users.Users; import io.github.muntashirakon.AppManager.utils.BroadcastUtils; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.ExUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.Utils; public final class PackageManagerCompat { public static final String TAG = PackageManagerCompat.class.getSimpleName(); public static final int MATCH_STATIC_SHARED_AND_SDK_LIBRARIES = 0x04000000; public static final int GET_SIGNING_CERTIFICATES; public static final int GET_SIGNING_CERTIFICATES_APK; public static final int MATCH_DISABLED_COMPONENTS; public static final int MATCH_UNINSTALLED_PACKAGES; static { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { GET_SIGNING_CERTIFICATES = PackageManager.GET_SIGNING_CERTIFICATES; } else { //noinspection deprecation GET_SIGNING_CERTIFICATES = PackageManager.GET_SIGNATURES; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { GET_SIGNING_CERTIFICATES_APK = PackageManager.GET_SIGNING_CERTIFICATES; } else { //noinspection deprecation GET_SIGNING_CERTIFICATES_APK = PackageManager.GET_SIGNATURES; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { MATCH_DISABLED_COMPONENTS = PackageManager.MATCH_DISABLED_COMPONENTS; MATCH_UNINSTALLED_PACKAGES = PackageManager.MATCH_UNINSTALLED_PACKAGES; } else { //noinspection deprecation MATCH_DISABLED_COMPONENTS = PackageManager.GET_DISABLED_COMPONENTS; //noinspection deprecation MATCH_UNINSTALLED_PACKAGES = PackageManager.GET_UNINSTALLED_PACKAGES; } } @IntDef({ COMPONENT_ENABLED_STATE_DEFAULT, COMPONENT_ENABLED_STATE_ENABLED, COMPONENT_ENABLED_STATE_DISABLED, COMPONENT_ENABLED_STATE_DISABLED_USER, COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED, }) @Retention(RetentionPolicy.SOURCE) public @interface EnabledState { } @IntDef(flag = true, value = { DONT_KILL_APP, SYNCHRONOUS }) @Retention(RetentionPolicy.SOURCE) public @interface EnabledFlags { } private static final int NEEDED_FLAGS = MATCH_UNINSTALLED_PACKAGES | MATCH_STATIC_SHARED_AND_SDK_LIBRARIES; @WorkerThread @NonNull public static List getInstalledPackages(int flags, @UserIdInt int userId) { IPackageManager pm = getPackageManager(); // Here we've compromised performance to fix issues in some devices where Binder transaction limit is too small. List refPackages = getInstalledPackagesInternal(pm, flags & NEEDED_FLAGS, userId); List packageInfoList = getInstalledPackagesInternal(pm, flags, userId); if (packageInfoList.size() == refPackages.size()) { // Everything's loaded correctly return packageInfoList; } if (packageInfoList.size() > refPackages.size()) { // Should never happen Set pkgsFromPkgInfo = new HashSet<>(packageInfoList.size()); Set pkgsFromAppInfo = new HashSet<>(refPackages.size()); for (PackageInfo info : packageInfoList) pkgsFromPkgInfo.add(info.packageName); for (PackageInfo info : refPackages) pkgsFromAppInfo.add(info.packageName); pkgsFromPkgInfo.removeAll(pkgsFromAppInfo); Log.i(TAG, "Loaded extra packages: " + pkgsFromPkgInfo.toString()); throw new IllegalStateException("Retrieved " + packageInfoList.size() + " packages out of " + refPackages.size() + " applications which is impossible"); } Log.w(TAG, "Could not fetch installed packages for user %d using getInstalledPackages(), using workaround", userId); packageInfoList = new ArrayList<>(refPackages.size()); for (int i = 0; i < refPackages.size(); ++i) { if (ThreadUtils.isInterrupted()) { break; } String packageName = refPackages.get(i).packageName; try { packageInfoList.add(getPackageInfo(pm, packageName, flags, userId)); } catch (Exception ex) { Log.e(TAG, "Could not retrieve package info for " + packageName + " and user " + userId); continue; } if (i % 100 == 0) { // Prevent DeadObjectException SystemClock.sleep(300); } } return packageInfoList; } @SuppressWarnings("deprecation") private static List getInstalledPackagesInternal(@NonNull IPackageManager pm, int flags, @UserIdInt int userId) { try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { return pm.getInstalledPackages((long) flags, userId).getList(); } return pm.getInstalledPackages(flags, userId).getList(); } catch (RemoteException e) { return ExUtils.rethrowFromSystemServer(e); } catch (BadParcelableException e) { Log.w(TAG, "Could not retrieve all packages for user " + userId, e); return Collections.emptyList(); } } @WorkerThread public static List getInstalledApplications(int flags, @UserIdInt int userId) throws RemoteException { return getInstalledApplications(getPackageManager(), flags, userId); } @SuppressLint("NewApi") @SuppressWarnings("deprecation") @WorkerThread public static List getInstalledApplications(@NonNull IPackageManager pm, int flags, @UserIdInt int userId) throws RemoteException { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { return pm.getInstalledApplications((long) flags, userId).getList(); } return pm.getInstalledApplications(flags, userId).getList(); } @NonNull public static PackageInfo getPackageInfo(@NonNull String packageName, int flags, @UserIdInt int userId) throws RemoteException, PackageManager.NameNotFoundException { return getPackageInfo(getPackageManager(), packageName, flags, userId); } @NonNull private static PackageInfo getPackageInfo(@NonNull IPackageManager pm, @NonNull String packageName, int flags, @UserIdInt int userId) throws RemoteException, PackageManager.NameNotFoundException { PackageInfo info = null; try { info = getPackageInfoInternal(pm, packageName, flags, userId); } catch (DeadObjectException e) { Log.w(TAG, "Could not fetch info for package %s and user %d with flags 0x%X, using workaround", e, packageName, userId, flags); } if (info == null) { // The app might not be loaded properly due parcel size limit, try to load components separately. // first check the existence of the package int strippedFlags = flags & ~(PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES | PackageManager.GET_PROVIDERS | PackageManager.GET_RECEIVERS | PackageManager.GET_PERMISSIONS); info = getPackageInfoInternal(pm, packageName, strippedFlags, userId); if (info == null) { // At this point, it should return package info. // Returning null denotes that it failed again even after the major flags have been stripped. throw new PackageManager.NameNotFoundException(String.format("Could not retrieve info for package %s with flags 0x%X for user %d", packageName, strippedFlags, userId)); } // Load info for major flags ActivityInfo[] activities = null; if ((flags & PackageManager.GET_ACTIVITIES) != 0) { int newFlags = flags & ~(PackageManager.GET_SERVICES | PackageManager.GET_PROVIDERS | PackageManager.GET_RECEIVERS | PackageManager.GET_PERMISSIONS); PackageInfo info1 = getPackageInfoInternal(pm, packageName, newFlags, userId); if (info1 != null) activities = info1.activities; } ServiceInfo[] services = null; if ((flags & PackageManager.GET_SERVICES) != 0) { int newFlags = flags & ~(PackageManager.GET_ACTIVITIES | PackageManager.GET_PROVIDERS | PackageManager.GET_RECEIVERS | PackageManager.GET_PERMISSIONS); PackageInfo info1 = getPackageInfoInternal(pm, packageName, newFlags, userId); if (info1 != null) services = info1.services; } ProviderInfo[] providers = null; if ((flags & PackageManager.GET_PROVIDERS) != 0) { int newFlags = flags & ~(PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES | PackageManager.GET_RECEIVERS | PackageManager.GET_PERMISSIONS); PackageInfo info1 = getPackageInfoInternal(pm, packageName, newFlags, userId); if (info1 != null) providers = info1.providers; } ActivityInfo[] receivers = null; if ((flags & PackageManager.GET_RECEIVERS) != 0) { int newFlags = flags & ~(PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES | PackageManager.GET_PROVIDERS | PackageManager.GET_PERMISSIONS); PackageInfo info1 = getPackageInfoInternal(pm, packageName, newFlags, userId); if (info1 != null) receivers = info1.receivers; } PermissionInfo[] permissions = null; if ((flags & PackageManager.GET_PERMISSIONS) != 0) { int newFlags = flags & ~(PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES | PackageManager.GET_PROVIDERS | PackageManager.GET_RECEIVERS); PackageInfo info1 = getPackageInfoInternal(pm, packageName, newFlags, userId); if (info1 != null) permissions = info1.permissions; } info.activities = activities; info.services = services; info.providers = providers; info.receivers = receivers; info.permissions = permissions; } // Info should never be null here, but it's checked anyway. return Objects.requireNonNull(info); } @SuppressWarnings("deprecation") @NonNull public static ApplicationInfo getApplicationInfo(String packageName, int flags, @UserIdInt int userId) throws RemoteException, PackageManager.NameNotFoundException { IPackageManager pm = getPackageManager(); ApplicationInfo applicationInfo; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { applicationInfo = pm.getApplicationInfo(packageName, (long) flags, userId); } else applicationInfo = pm.getApplicationInfo(packageName, flags, userId); if (applicationInfo == null) { throw new PackageManager.NameNotFoundException("Package " + packageName + " not found."); } return applicationInfo; } @Nullable public static String getInstallerPackageName(@NonNull String packageName, @UserIdInt int userId) { try { InstallSourceInfoCompat installSource = getInstallSourceInfo(packageName, userId); if (installSource.getInstallingPackageName() != null) { return installSource.getInstallingPackageName(); } return installSource.getInitiatingPackageName(); } catch (RemoteException | SecurityException e) { return null; } } @SuppressWarnings("deprecation") @NonNull public static InstallSourceInfoCompat getInstallSourceInfo(@NonNull String packageName, @UserIdInt int userId) throws RemoteException { IPackageManager pm = getPackageManager(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { return new InstallSourceInfoCompat(pm.getInstallSourceInfo(packageName, userId)); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { return new InstallSourceInfoCompat(pm.getInstallSourceInfo(packageName)); } String installerPackageName = null; try { installerPackageName = getPackageManager().getInstallerPackageName(packageName); } catch (IllegalArgumentException e) { String message = e.getMessage(); if (message != null && message.startsWith("Unknown package:")) { throw new RemoteException(message); } } return new InstallSourceInfoCompat(installerPackageName); } @Nullable public static Intent getLaunchIntentForPackage(@NonNull String packageName, @UserIdInt int userId) { Context context = ContextUtils.getContext(); if (userId == UserHandleHidden.myUserId()) { PackageManager pm = context.getPackageManager(); return Utils.isTv(context) ? pm.getLeanbackLaunchIntentForPackage(packageName) : pm.getLaunchIntentForPackage(packageName); } UserHandle userHandle = Users.getUserHandle(userId); if (userHandle == null) { // No supported user present return null; } LauncherApps launcherApps = (LauncherApps) context.getSystemService(Context.LAUNCHER_APPS_SERVICE); try { if (!launcherApps.isPackageEnabled(packageName, userHandle)) { // Package not enabled return null; } } catch (SecurityException e) { Log.w(TAG, "Could not retrieve enable state of " + packageName + " for user " + userHandle, e); return null; } List activityInfoList = launcherApps.getActivityList(packageName, userHandle); if (activityInfoList.isEmpty()) { // No activities return null; } // Return the first openable activity LauncherActivityInfo info = activityInfoList.get(0); return new Intent(Intent.ACTION_MAIN) .addCategory(Intent.CATEGORY_LAUNCHER) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) .setComponent(info.getComponentName()); } @SuppressLint("NewApi") @SuppressWarnings("deprecation") @NonNull public static List queryIntentActivities(@NonNull Context context, @NonNull Intent intent, int flags, @UserIdInt int userId) throws RemoteException { IPackageManager pm = getPackageManager(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { IPackageManagerN pmN = Refine.unsafeCast(pm); ParceledListSlice resolveInfoList; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { resolveInfoList = pmN.queryIntentActivities(intent, intent.resolveTypeIfNeeded(context.getContentResolver()), (long) flags, userId); } else { resolveInfoList = pmN.queryIntentActivities(intent, intent.resolveTypeIfNeeded(context.getContentResolver()), flags, userId); } return resolveInfoList.getList(); } else { return pm.queryIntentActivities(intent, intent.resolveTypeIfNeeded(context.getContentResolver()), flags, userId); } } @EnabledState public static int getComponentEnabledSetting(ComponentName componentName, @UserIdInt int userId) throws SecurityException, IllegalArgumentException { try { return getPackageManager().getComponentEnabledSetting(componentName, userId); } catch (RemoteException e) { return ExUtils.rethrowFromSystemServer(e); } } @SuppressWarnings("deprecation") @RequiresPermission(value = Manifest.permission.CHANGE_COMPONENT_ENABLED_STATE) public static void setComponentEnabledSetting(ComponentName componentName, @EnabledState int newState, @EnabledFlags int flags, @UserIdInt int userId) throws RemoteException { IPackageManager pm = getPackageManager(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { String callingPackage = SelfPermissions.getCallingPackage(Users.getSelfOrRemoteUid()); pm.setComponentEnabledSetting(componentName, newState, flags, userId, callingPackage); } else pm.setComponentEnabledSetting(componentName, newState, flags, userId); if (userId != UserHandleHidden.myUserId()) { BroadcastUtils.sendPackageAltered(ContextUtils.getContext(), new String[]{componentName.getPackageName()}); } } @RequiresPermission(value = Manifest.permission.CHANGE_COMPONENT_ENABLED_STATE) public static void setApplicationEnabledSetting(String packageName, @EnabledState int newState, @EnabledFlags int flags, @UserIdInt int userId) throws SecurityException, IllegalArgumentException { try { getPackageManager().setApplicationEnabledSetting(packageName, newState, flags, userId, null); if (userId != UserHandleHidden.myUserId()) { BroadcastUtils.sendPackageAltered(ContextUtils.getContext(), new String[]{packageName}); } } catch (RemoteException e) { ExUtils.rethrowFromSystemServer(e); } } public static int getApplicationEnabledSetting(String packageName, @UserIdInt int userId) throws SecurityException, IllegalArgumentException { try { return getPackageManager().getApplicationEnabledSetting(packageName, userId); } catch (RemoteException e) { return ExUtils.rethrowFromSystemServer(e); } } @SuppressWarnings("deprecation") @RequiresApi(Build.VERSION_CODES.N) @RequiresPermission(allOf = {"android.permission.SUSPEND_APPS", ManifestCompat.permission.MANAGE_USERS}) public static void suspendPackages(String[] packageNames, @UserIdInt int userId, boolean suspend) throws RemoteException { String callingPackage = SelfPermissions.getCallingPackage(Users.getSelfOrRemoteUid()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { try { getPackageManager().setPackagesSuspendedAsUser(packageNames, suspend, null, null, null, 0, callingPackage, 0, userId); } catch (NoSuchMethodError e) { getPackageManager().setPackagesSuspendedAsUser(packageNames, suspend, null, null, (SuspendDialogInfo) null, callingPackage, userId); } } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { getPackageManager().setPackagesSuspendedAsUser(packageNames, suspend, null, null, (SuspendDialogInfo) null, callingPackage, userId); } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) { getPackageManager().setPackagesSuspendedAsUser(packageNames, suspend, null, null, (String) null, callingPackage, userId); } else { getPackageManager().setPackagesSuspendedAsUser(packageNames, suspend, userId); } if (userId != UserHandleHidden.myUserId()) { BroadcastUtils.sendPackageAltered(ContextUtils.getContext(), packageNames); } } public static boolean isPackageSuspended(String packageName, @UserIdInt int userId) throws RemoteException { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { return getPackageManager().isPackageSuspendedForUser(packageName, userId); } return false; } @RequiresPermission(ManifestCompat.permission.MANAGE_USERS) public static void hidePackage(String packageName, @UserIdInt int userId, boolean hide) throws RemoteException { if (SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.MANAGE_USERS)) { boolean hidden = getPackageManager().setApplicationHiddenSettingAsUser(packageName, hide, userId); if (userId != UserHandleHidden.myUserId()) { if (hidden) { if (hide) { BroadcastUtils.sendPackageRemoved(ContextUtils.getContext(), new String[]{packageName}); } else { BroadcastUtils.sendPackageAdded(ContextUtils.getContext(), new String[]{packageName}); } } } } else { throw new RemoteException("Missing required permission: android.permission.MANAGE_USERS."); } } public static boolean isPackageHidden(String packageName, @UserIdInt int userId) throws RemoteException { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { try { // Find using private flags ApplicationInfo info = getApplicationInfo(packageName, PackageManagerCompat.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES, userId); return ApplicationInfoCompat.isHidden(info); } catch (PackageManager.NameNotFoundException ignore) { } } if (SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.MANAGE_USERS)) { return getPackageManager().getApplicationHiddenSettingAsUser(packageName, userId); } // Otherwise, there is no way to detect if the package is hidden return false; } @SuppressWarnings("deprecation") @RequiresPermission(anyOf = { Manifest.permission.INSTALL_PACKAGES, "com.android.permission.INSTALL_EXISTING_PACKAGES" }) public static int installExistingPackageAsUser(@NonNull String packageName, @UserIdInt int userId, int installFlags, int installReason, @Nullable List whiteListedPermissions) throws RemoteException { int returnCode; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { returnCode = getPackageManager().installExistingPackageAsUser(packageName, userId, installFlags, installReason, whiteListedPermissions); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { returnCode = getPackageManager().installExistingPackageAsUser(packageName, userId, installFlags, installReason); } else returnCode = getPackageManager().installExistingPackageAsUser(packageName, userId); if (userId != UserHandleHidden.myUserId()) { BroadcastUtils.sendPackageAdded(ContextUtils.getContext(), new String[]{packageName}); } return returnCode; } @RequiresPermission(ManifestCompat.permission.CLEAR_APP_USER_DATA) public static void clearApplicationUserData(@NonNull UserPackagePair pair) throws AndroidException { IPackageManager pm = getPackageManager(); ClearDataObserver obs = new ClearDataObserver(); pm.clearApplicationUserData(pair.getPackageName(), obs, pair.getUserId()); //noinspection SynchronizationOnLocalVariableOrMethodParameter synchronized (obs) { while (!obs.isCompleted()) { try { obs.wait(500); } catch (InterruptedException ignore) { } } } if (!obs.isSuccessful()) { throw new AndroidException("Could not clear data of package " + pair); } BroadcastUtils.sendPackageAltered(ContextUtils.getContext(), new String[]{pair.getPackageName()}); } @RequiresPermission(ManifestCompat.permission.CLEAR_APP_USER_DATA) public static boolean clearApplicationUserData(@NonNull String packageName, @UserIdInt int userId) { try { clearApplicationUserData(new UserPackagePair(packageName, userId)); return true; } catch (AndroidException | SecurityException e) { e.printStackTrace(); return false; } } @RequiresPermission(allOf = { Manifest.permission.DELETE_CACHE_FILES, "android.permission.INTERNAL_DELETE_CACHE_FILES" }) public static void deleteApplicationCacheFilesAsUser(UserPackagePair pair) throws AndroidException { IPackageManager pm = getPackageManager(); ClearDataObserver obs = new ClearDataObserver(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { pm.deleteApplicationCacheFilesAsUser(pair.getPackageName(), pair.getUserId(), obs); } else pm.deleteApplicationCacheFiles(pair.getPackageName(), obs); //noinspection SynchronizationOnLocalVariableOrMethodParameter synchronized (obs) { while (!obs.isCompleted()) { try { obs.wait(500); } catch (InterruptedException ignore) { } } } if (!obs.isSuccessful()) { throw new AndroidException("Could not clear cache of package " + pair); } BroadcastUtils.sendPackageAltered(ContextUtils.getContext(), new String[]{pair.getPackageName()}); } @RequiresPermission(allOf = { Manifest.permission.DELETE_CACHE_FILES, "android.permission.INTERNAL_DELETE_CACHE_FILES" }) public static boolean deleteApplicationCacheFilesAsUser(String packageName, int userId) { try { deleteApplicationCacheFilesAsUser(new UserPackagePair(packageName, userId)); return true; } catch (AndroidException | SecurityException e) { e.printStackTrace(); return false; } } @RequiresPermission(ManifestCompat.permission.FORCE_STOP_PACKAGES) public static void forceStopPackage(String packageName, int userId) throws SecurityException { try { ActivityManagerCompat.getActivityManager().forceStopPackage(packageName, userId); BroadcastUtils.sendPackageAltered(ContextUtils.getContext(), new String[]{packageName}); } catch (RemoteException e) { ExUtils.rethrowFromSystemServer(e); } } @NonNull public static IPackageInstaller getPackageInstaller() throws RemoteException { return IPackageInstaller.Stub.asInterface(new ProxyBinder(getPackageManager().getPackageInstaller().asBinder())); } @SuppressWarnings("deprecation") @RequiresPermission(Manifest.permission.CLEAR_APP_CACHE) public static void freeStorageAndNotify(@Nullable String volumeUuid, long freeStorageSize, @StorageManagerCompat.AllocateFlags int storageFlags) throws RemoteException { IPackageManager pm; ClearDataObserver obs = new ClearDataObserver(); if (SelfPermissions.checkSelfPermission(Manifest.permission.CLEAR_APP_CACHE)) { // Clear cache using unprivileged method: Special case for Android Lollipop pm = getUnprivilegedPackageManager(); } else if (SelfPermissions.checkSelfOrRemotePermission(Manifest.permission.CLEAR_APP_CACHE)) { // Use privileged mode pm = getPackageManager(); } else { // Clear one by one // Special case: IPackageManager#freeStorageAndNotify cannot be used before Android Oreo because Shell does // not have the permission android.permission.CLEAR_APP_CACHE boolean hasPermission; if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { hasPermission = SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.INTERNAL_DELETE_CACHE_FILES); } else { hasPermission = SelfPermissions.checkSelfOrRemotePermission(Manifest.permission.DELETE_CACHE_FILES); } if (!hasPermission) { // Does not have enough permission return; } if (!SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.INTERACT_ACROSS_USERS_FULL)) { int userId = UserHandleHidden.myUserId(); for (ApplicationInfo info : getInstalledApplications(MATCH_STATIC_SHARED_AND_SDK_LIBRARIES, userId)) { deleteApplicationCacheFilesAsUser(info.packageName, userId); } return; } for (int userId : Users.getUsersIds()) { for (ApplicationInfo info : getInstalledApplications(MATCH_STATIC_SHARED_AND_SDK_LIBRARIES, userId)) { deleteApplicationCacheFilesAsUser(info.packageName, userId); } } return; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { pm.freeStorageAndNotify(volumeUuid, freeStorageSize, storageFlags, obs); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { pm.freeStorageAndNotify(volumeUuid, freeStorageSize, obs); } else { pm.freeStorageAndNotify(freeStorageSize, obs); } //noinspection SynchronizationOnLocalVariableOrMethodParameter synchronized (obs) { while (!obs.isCompleted()) { try { obs.wait(1_000); } catch (InterruptedException ignore) { } } } } @SuppressLint("NewApi") @SuppressWarnings("deprecation") private static PackageInfo getPackageInfoInternal(IPackageManager pm, String packageName, int flags, @UserIdInt int userId) throws RemoteException { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { return pm.getPackageInfo(packageName, (long) flags, userId); } return pm.getPackageInfo(packageName, flags, userId); } public static IPackageManager getPackageManager() { return IPackageManager.Stub.asInterface(ProxyBinder.getService("package")); } public static IPackageManager getUnprivilegedPackageManager() { return IPackageManager.Stub.asInterface(ProxyBinder.getUnprivilegedService("package")); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/compat/PermissionCompat.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.compat; import static io.github.muntashirakon.AppManager.compat.VirtualDeviceManagerCompat.PERSISTENT_DEVICE_ID_DEFAULT; import android.annotation.SuppressLint; import android.annotation.UserIdInt; import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; import android.content.pm.IPackageManagerN; import android.content.pm.PermissionGroupInfo; import android.content.pm.PermissionInfo; import android.content.pm.permission.SplitPermissionInfoParcelable; import android.os.Build; import android.os.RemoteException; import android.permission.IPermissionManager; import android.util.SparseArray; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.RequiresPermission; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Collections; import java.util.List; import dev.rikka.tools.refine.Refine; import io.github.muntashirakon.AppManager.ipc.ProxyBinder; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.ExUtils; public final class PermissionCompat { public static final int FLAG_PERMISSION_NONE = 0; /** * Permission flag: The permission is set in its current state * by the user and apps can still request it at runtime. */ @RequiresApi(Build.VERSION_CODES.M) public static final int FLAG_PERMISSION_USER_SET = 1; /** * Permission flag: The permission is set in its current state * by the user and it is fixed, i.e. apps can no longer request * this permission. */ @RequiresApi(Build.VERSION_CODES.M) public static final int FLAG_PERMISSION_USER_FIXED = 1 << 1; /** * Permission flag: The permission is set in its current state * by device policy and neither apps nor the user can change * its state. */ @RequiresApi(Build.VERSION_CODES.M) public static final int FLAG_PERMISSION_POLICY_FIXED = 1 << 2; /** * Permission flag: The permission is set in a granted state but * access to resources it guards is restricted by other means to * enable revoking a permission on legacy apps that do not support * runtime permissions. If this permission is upgraded to runtime * because the app was updated to support runtime permissions, the * the permission will be revoked in the upgrade process. * * @deprecated Renamed to {@link #FLAG_PERMISSION_REVOKED_COMPAT}. */ @Deprecated @RequiresApi(Build.VERSION_CODES.M) public static final int FLAG_PERMISSION_REVOKE_ON_UPGRADE = 1 << 3; /** * Permission flag: The permission is set in its current state * because the app is a component that is a part of the system. */ @RequiresApi(Build.VERSION_CODES.M) public static final int FLAG_PERMISSION_SYSTEM_FIXED = 1 << 4; /** * Permission flag: The permission is granted by default because it * enables app functionality that is expected to work out-of-the-box * for providing a smooth user experience. For example, the phone app * is expected to have the phone permission. */ @RequiresApi(Build.VERSION_CODES.M) public static final int FLAG_PERMISSION_GRANTED_BY_DEFAULT = 1 << 5; /** * Permission flag: The permission has to be reviewed before any of * the app components can run. */ @RequiresApi(Build.VERSION_CODES.N) public static final int FLAG_PERMISSION_REVIEW_REQUIRED = 1 << 6; /** * Permission flag: The permission has not been explicitly requested by * the app but has been added automatically by the system. Revoke once * the app does explicitly request it. */ @RequiresApi(Build.VERSION_CODES.Q) public static final int FLAG_PERMISSION_REVOKE_WHEN_REQUESTED = 1 << 7; /** * Permission flag: The permission's usage should be made highly visible to the user * when granted. */ @RequiresApi(Build.VERSION_CODES.Q) public static final int FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED = 1 << 8; /** * Permission flag: The permission's usage should be made highly visible to the user * when denied. */ @RequiresApi(Build.VERSION_CODES.Q) public static final int FLAG_PERMISSION_USER_SENSITIVE_WHEN_DENIED = 1 << 9; /** * Permission flag: The permission is restricted but the app is exempt * from the restriction and is allowed to hold this permission in its * full form and the exemption is provided by the installer on record. */ @RequiresApi(Build.VERSION_CODES.Q) public static final int FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT = 1 << 11; /** * Permission flag: The permission is restricted but the app is exempt * from the restriction and is allowed to hold this permission in its * full form and the exemption is provided by the system due to its * permission policy. */ @RequiresApi(Build.VERSION_CODES.Q) public static final int FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT = 1 << 12; /** * Permission flag: The permission is restricted but the app is exempt * from the restriction and is allowed to hold this permission and the * exemption is provided by the system when upgrading from an OS version * where the permission was not restricted to an OS version where the * permission is restricted. */ @RequiresApi(Build.VERSION_CODES.Q) public static final int FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT = 1 << 13; /** * Permission flag: The permission is disabled but may be granted. If * disabled the data protected by the permission should be protected * by a no-op (empty list, default error, etc) instead of crashing the * client. */ @RequiresApi(Build.VERSION_CODES.Q) public static final int FLAG_PERMISSION_APPLY_RESTRICTION = 1 << 14; /** * Permission flag: The permission is granted because the application holds a role. */ @RequiresApi(Build.VERSION_CODES.Q) public static final int FLAG_PERMISSION_GRANTED_BY_ROLE = 1 << 15; /** * Permission flag: The permission should have been revoked but is kept granted for * compatibility. The data protected by the permission should be protected by a no-op (empty * list, default error, etc) instead of crashing the client. The permission will be revoked if * the app is upgraded to supports it. */ @RequiresApi(Build.VERSION_CODES.R) public static final int FLAG_PERMISSION_REVOKED_COMPAT = FLAG_PERMISSION_REVOKE_ON_UPGRADE; /** * Permission flag: The permission is one-time and should be revoked automatically on app * inactivity */ @RequiresApi(Build.VERSION_CODES.R) public static final int FLAG_PERMISSION_ONE_TIME = 1 << 16; /** * Permission flag: Whether permission was revoked by auto-revoke. */ @RequiresApi(Build.VERSION_CODES.R) public static final int FLAG_PERMISSION_AUTO_REVOKED = 1 << 17; /** * Permission flags: Reserved for use by the permission controller. The platform and any * packages besides the permission controller should not assume any definition about these * flags. */ @RequiresApi(Build.VERSION_CODES.R) public static final int FLAGS_PERMISSION_RESERVED_PERMISSION_CONTROLLER = 1 << 28 | 1 << 29 | 1 << 30 | 1 << 31; /** * Permission flags: Bitwise or of all permission flags allowing an * exemption for a restricted permission. */ @RequiresApi(Build.VERSION_CODES.Q) public static final int FLAGS_PERMISSION_RESTRICTION_ANY_EXEMPT = FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT | FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT | FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT; /** * Mask for all permission flags. */ @PermissionFlags public static final int MASK_PERMISSION_FLAGS_ALL; static { int allPerms = FLAG_PERMISSION_NONE; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { allPerms |= FLAG_PERMISSION_USER_SET | FLAG_PERMISSION_USER_FIXED | FLAG_PERMISSION_POLICY_FIXED | FLAG_PERMISSION_REVOKE_ON_UPGRADE | FLAG_PERMISSION_SYSTEM_FIXED | FLAG_PERMISSION_GRANTED_BY_DEFAULT; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { allPerms |= FLAG_PERMISSION_REVIEW_REQUIRED; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { allPerms |= FLAG_PERMISSION_REVOKE_WHEN_REQUESTED | FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED | FLAG_PERMISSION_USER_SENSITIVE_WHEN_DENIED | FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT | FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT | FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT | FLAG_PERMISSION_APPLY_RESTRICTION | FLAG_PERMISSION_GRANTED_BY_ROLE; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { allPerms |= FLAG_PERMISSION_REVOKED_COMPAT | FLAG_PERMISSION_ONE_TIME | FLAG_PERMISSION_AUTO_REVOKED; } MASK_PERMISSION_FLAGS_ALL = allPerms; } /** * Permission flags set when granting or revoking a permission. */ @SuppressLint("NewApi") @RequiresApi(Build.VERSION_CODES.M) @IntDef(flag = true, value = { FLAG_PERMISSION_NONE, FLAG_PERMISSION_USER_SET, FLAG_PERMISSION_USER_FIXED, FLAG_PERMISSION_POLICY_FIXED, // FLAG_PERMISSION_REVOKE_ON_UPGRADE, FLAG_PERMISSION_SYSTEM_FIXED, FLAG_PERMISSION_GRANTED_BY_DEFAULT, FLAG_PERMISSION_REVIEW_REQUIRED, FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED, FLAG_PERMISSION_USER_SENSITIVE_WHEN_DENIED, FLAG_PERMISSION_REVOKE_WHEN_REQUESTED, FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT, FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT, FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT, FLAG_PERMISSION_APPLY_RESTRICTION, FLAG_PERMISSION_GRANTED_BY_ROLE, FLAG_PERMISSION_REVOKED_COMPAT, FLAG_PERMISSION_ONE_TIME, FLAG_PERMISSION_AUTO_REVOKED }) @Retention(RetentionPolicy.SOURCE) public @interface PermissionFlags { } @SuppressWarnings("deprecation") @RequiresPermission(anyOf = { ManifestCompat.permission.GET_RUNTIME_PERMISSIONS, ManifestCompat.permission.GRANT_RUNTIME_PERMISSIONS, ManifestCompat.permission.REVOKE_RUNTIME_PERMISSIONS, }) @PermissionFlags public static int getPermissionFlags(@NonNull String permissionName, @NonNull String packageName, @UserIdInt int userId) throws SecurityException { try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { IPermissionManager permissionManager = getPermissionManager(); try { return permissionManager.getPermissionFlags(packageName, permissionName, userId); } catch (NoSuchMethodError e) { try { return permissionManager.getPermissionFlags(packageName, permissionName, ContextUtils.getContext().getDeviceId(), userId); } catch (NoSuchMethodError e2) { return permissionManager.getPermissionFlags(packageName, permissionName, PERSISTENT_DEVICE_ID_DEFAULT, userId); } } } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { return getPermissionManager().getPermissionFlags(packageName, permissionName, userId); } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { return getPermissionManager().getPermissionFlags(permissionName, packageName, userId); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { return PackageManagerCompat.getPackageManager().getPermissionFlags(permissionName, packageName, userId); } } catch (RemoteException e) { return ExUtils.rethrowFromSystemServer(e); } return FLAG_PERMISSION_NONE; } /** * Replace a set of flags with another or {@code 0}. Requires {@link ManifestCompat.permission#ADJUST_RUNTIME_PERMISSIONS_POLICY} * when checkAdjustPolicyFlagPermission is {@code true} and flagMask has {@link #FLAG_PERMISSION_POLICY_FIXED}. * * @param flagMask The flags to be replaced * @param flagValues The new flags to set (is a subset of flagMask) * @see PermissionFlagsTest.java */ @SuppressWarnings("deprecation") @RequiresPermission(anyOf = { ManifestCompat.permission.GET_RUNTIME_PERMISSIONS, ManifestCompat.permission.GRANT_RUNTIME_PERMISSIONS, ManifestCompat.permission.REVOKE_RUNTIME_PERMISSIONS, }) public static void updatePermissionFlags(@NonNull String permissionName, @NonNull String packageName, @PermissionFlags int flagMask, @PermissionFlags int flagValues, boolean checkAdjustPolicyFlagPermission, @UserIdInt int userId) throws RemoteException { IPackageManager pm = PackageManagerCompat.getPackageManager(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { IPermissionManager permissionManager = getPermissionManager(); try { permissionManager.updatePermissionFlags(packageName, permissionName, flagMask, flagValues, checkAdjustPolicyFlagPermission, userId); } catch (NoSuchMethodError e) { try { permissionManager.updatePermissionFlags(packageName, permissionName, flagMask, flagValues, checkAdjustPolicyFlagPermission, ContextUtils.getContext().getDeviceId(), userId); } catch (NoSuchMethodError e2) { permissionManager.updatePermissionFlags(packageName, permissionName, flagMask, flagValues, checkAdjustPolicyFlagPermission, PERSISTENT_DEVICE_ID_DEFAULT, userId); } } } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { getPermissionManager().updatePermissionFlags(packageName, permissionName, flagMask, flagValues, checkAdjustPolicyFlagPermission, userId); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { getPermissionManager().updatePermissionFlags(permissionName, packageName, flagMask, flagValues, checkAdjustPolicyFlagPermission, userId); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { pm.updatePermissionFlags(permissionName, packageName, flagMask, flagValues, checkAdjustPolicyFlagPermission, userId); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { pm.updatePermissionFlags(permissionName, packageName, flagMask, flagValues, userId); } } /** * Grant a permission. May also require {@link ManifestCompat.permission#ADJUST_RUNTIME_PERMISSIONS_POLICY}. */ @SuppressWarnings("deprecation") @RequiresPermission(ManifestCompat.permission.GRANT_RUNTIME_PERMISSIONS) public static void grantPermission(@NonNull String packageName, @NonNull String permissionName, @UserIdInt int userId) throws RemoteException { IPackageManager pm = PackageManagerCompat.getPackageManager(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { IPermissionManager permissionManager = getPermissionManager(); try { permissionManager.grantRuntimePermission(packageName, permissionName, userId); } catch (NoSuchMethodError e) { try { permissionManager.grantRuntimePermission(packageName, permissionName, ContextUtils.getContext().getDeviceId(), userId); } catch (NoSuchMethodError e2) { permissionManager.grantRuntimePermission(packageName, permissionName, PERSISTENT_DEVICE_ID_DEFAULT, userId); } } } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { getPermissionManager().grantRuntimePermission(packageName, permissionName, userId); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { pm.grantRuntimePermission(packageName, permissionName, userId); } else { pm.grantPermission(packageName, permissionName); } } /** * Revoke a permission. May also require {@link ManifestCompat.permission#ADJUST_RUNTIME_PERMISSIONS_POLICY}. */ @RequiresPermission(ManifestCompat.permission.REVOKE_RUNTIME_PERMISSIONS) public static void revokePermission(@NonNull String packageName, @NonNull String permissionName, @UserIdInt int userId) throws RemoteException { revokePermission(packageName, permissionName, userId, null); } /** * Revoke a permission. May also require {@link ManifestCompat.permission#ADJUST_RUNTIME_PERMISSIONS_POLICY}. */ @SuppressWarnings("deprecation") @RequiresPermission(ManifestCompat.permission.REVOKE_RUNTIME_PERMISSIONS) public static void revokePermission(@NonNull String packageName, @NonNull String permissionName, @UserIdInt int userId, @Nullable String reason) throws RemoteException { IPackageManager pm = PackageManagerCompat.getPackageManager(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { IPermissionManager permissionManager = getPermissionManager(); try { permissionManager.revokeRuntimePermission(packageName, permissionName, userId, reason); } catch (NoSuchMethodError e) { try { permissionManager.revokeRuntimePermission(packageName, permissionName, ContextUtils.getContext().getDeviceId(), userId, reason); } catch (NoSuchMethodError e2) { permissionManager.revokeRuntimePermission(packageName, permissionName, PERSISTENT_DEVICE_ID_DEFAULT, userId, reason); } } } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { getPermissionManager().revokeRuntimePermission(packageName, permissionName, userId, reason); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { pm.revokeRuntimePermission(packageName, permissionName, userId); } else { pm.revokePermission(packageName, permissionName); } } @SuppressWarnings("deprecation") public static int checkPermission(@NonNull String permissionName, @NonNull String packageName, @UserIdInt int userId) { IPackageManager pm = PackageManagerCompat.getPackageManager(); try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { return pm.checkPermission(permissionName, packageName, userId); } else { return pm.checkPermission(permissionName, packageName); } } catch (RemoteException e) { return ExUtils.rethrowFromSystemServer(e); } } @SuppressWarnings({"deprecation", "ConstantConditions"}) @Nullable public static PermissionInfo getPermissionInfo(String permissionName, String packageName, int flags) throws RemoteException { IPackageManager pm = PackageManagerCompat.getPackageManager(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { return getPermissionManager().getPermissionInfo(permissionName, packageName, flags); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { return pm.getPermissionInfo(permissionName, packageName, flags); } else return pm.getPermissionInfo(permissionName, flags); } @SuppressWarnings("deprecation") @NonNull public static PermissionGroupInfo getPermissionGroupInfo(String groupName, int flags) throws RemoteException { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { return getPermissionManager().getPermissionGroupInfo(groupName, flags); } else return PackageManagerCompat.getPackageManager().getPermissionGroupInfo(groupName, flags); } @SuppressWarnings("deprecation") public static List queryPermissionsByGroup(String groupName, int flags) throws RemoteException { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { return getPermissionManager().queryPermissionsByGroup(groupName, flags).getList(); } else { IPackageManager pm = PackageManagerCompat.getPackageManager(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { IPackageManagerN pmN = Refine.unsafeCast(pm); return pmN.queryPermissionsByGroup(groupName, flags).getList(); } else return pm.queryPermissionsByGroup(groupName, flags); } } public static List getSplitPermissions() throws RemoteException { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { return getPermissionManager().getSplitPermissions(); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { return PackageManagerCompat.getPackageManager().getSplitPermissions(); } return Collections.emptyList(); } public static boolean getCheckAdjustPolicyFlagPermission(@NonNull ApplicationInfo info) { return info.targetSdkVersion >= Build.VERSION_CODES.Q; } @NonNull public static IPermissionManager getPermissionManager() { return IPermissionManager.Stub.asInterface(ProxyBinder.getService("permissionmgr")); } @SuppressLint("WrongConstant") @NonNull public static SparseArray getPermissionFlagsWithString(@PermissionFlags int flags) { SparseArray permissionFlagsWithString = new SparseArray<>(); for (int i = 0; i < 18; ++i) { if ((flags & (1 << i)) != 0) { permissionFlagsWithString.put(1 << i, permissionFlagToString((1 << i))); } } return permissionFlagsWithString; } @SuppressLint("NewApi") @NonNull public static String permissionFlagToString(@PermissionFlags int flag) { switch (flag) { case FLAG_PERMISSION_GRANTED_BY_DEFAULT: return "GRANTED_BY_DEFAULT"; case FLAG_PERMISSION_POLICY_FIXED: return "POLICY_FIXED"; case FLAG_PERMISSION_SYSTEM_FIXED: return "SYSTEM_FIXED"; case FLAG_PERMISSION_USER_SET: return "USER_SET"; case FLAG_PERMISSION_USER_FIXED: return "USER_FIXED"; case FLAG_PERMISSION_REVIEW_REQUIRED: return "REVIEW_REQUIRED"; case FLAG_PERMISSION_REVOKE_WHEN_REQUESTED: return "REVOKE_WHEN_REQUESTED"; case FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED: return "USER_SENSITIVE_WHEN_GRANTED"; case FLAG_PERMISSION_USER_SENSITIVE_WHEN_DENIED: return "USER_SENSITIVE_WHEN_DENIED"; case FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT: return "RESTRICTION_INSTALLER_EXEMPT"; case FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT: return "RESTRICTION_SYSTEM_EXEMPT"; case FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT: return "RESTRICTION_UPGRADE_EXEMPT"; case FLAG_PERMISSION_APPLY_RESTRICTION: return "APPLY_RESTRICTION"; case FLAG_PERMISSION_GRANTED_BY_ROLE: return "GRANTED_BY_ROLE"; case FLAG_PERMISSION_REVOKED_COMPAT: return "REVOKED_COMPAT"; case FLAG_PERMISSION_ONE_TIME: return "ONE_TIME"; case FLAG_PERMISSION_AUTO_REVOKED: return "AUTO_REVOKED"; case FLAG_PERMISSION_NONE: default: return Integer.toString(flag); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/compat/ProcessCompat.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.compat; import android.os.RemoteException; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.io.File; import java.io.IOException; import io.github.muntashirakon.AppManager.ipc.LocalServices; import io.github.muntashirakon.AppManager.ipc.RemoteProcess; import io.github.muntashirakon.AppManager.ipc.RemoteProcessImpl; public final class ProcessCompat { /** * Defines the start of a range of UIDs (and GIDs), going from this * number to {@link #LAST_APPLICATION_UID} that are reserved for assigning * to applications. */ public static final int FIRST_APPLICATION_UID = android.os.Process.FIRST_APPLICATION_UID; /** * Last of application-specific UIDs starting at * {@link #FIRST_APPLICATION_UID}. */ public static final int LAST_APPLICATION_UID = android.os.Process.LAST_APPLICATION_UID; /** * First uid used for fully isolated sandboxed processes spawned from an app zygote */ public static final int FIRST_APP_ZYGOTE_ISOLATED_UID = 90000; /** * Last uid used for fully isolated sandboxed processes spawned from an app zygote */ public static final int LAST_APP_ZYGOTE_ISOLATED_UID = 98999; /** * First uid used for fully isolated sandboxed processes (with no permissions of their own) */ public static final int FIRST_ISOLATED_UID = 99000; /** * Last uid used for fully isolated sandboxed processes (with no permissions of their own) */ public static final int LAST_ISOLATED_UID = 99999; public static Process exec(@Nullable String[] cmd, @Nullable String[] env, @Nullable File dir) throws IOException { if (LocalServices.alive()) { try { return new RemoteProcess(LocalServices.getAmService().newProcess(cmd, env, dir == null ? null : dir.getAbsolutePath())); } catch (RemoteException e) { throw new IOException(e); } } return new RemoteProcess(new RemoteProcessImpl(Runtime.getRuntime().exec(cmd, env, dir))); } public static Process exec(String[] cmd, String[] env) throws IOException { return exec(cmd, env, null); } public static Process exec(String[] cmd) throws IOException { return exec(cmd, null, null); } public static boolean isAlive(@NonNull Process process) { try { process.exitValue(); return false; } catch (IllegalArgumentException e) { return true; } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/compat/SensorServiceCompat.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.compat; import android.annotation.UserIdInt; import android.os.Build; import android.os.IBinder; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.annotation.RequiresPermission; import java.io.IOException; import io.github.muntashirakon.AppManager.ipc.ProxyBinder; import io.github.muntashirakon.AppManager.utils.BinderShellExecutor; @RequiresApi(Build.VERSION_CODES.P) public final class SensorServiceCompat { @RequiresPermission(ManifestCompat.permission.MANAGE_SENSORS) public static boolean isSensorEnabled(@NonNull String packageName, @UserIdInt int userId) { String[] command; if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) { command = new String[]{"get-uid-state", packageName, "--user", String.valueOf(userId)}; } else command = new String[]{"get-uid-state", packageName}; try { BinderShellExecutor.ShellResult result = BinderShellExecutor.execute(getSensorService(), command); return "active".equals(result.getStdout().trim()); } catch (IOException e) { e.printStackTrace(); } return true; } @RequiresPermission(ManifestCompat.permission.MANAGE_SENSORS) public static void enableSensor(@NonNull String packageName, @UserIdInt int userId, boolean enable) throws IOException { String state = enable ? "active" : "idle"; String[] command; if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) { command = new String[]{"set-uid-state", packageName, state, "--user", String.valueOf(userId)}; } else command = new String[]{"set-uid-state", packageName, state}; BinderShellExecutor.ShellResult result = BinderShellExecutor.execute(getSensorService(), command); if (result.getResultCode() != 0) { throw new IOException("Could not " + (enable ? "enable" : "disable") + " sensor."); } } @RequiresPermission(ManifestCompat.permission.MANAGE_SENSORS) public static void resetSensor(@NonNull String packageName, @UserIdInt int userId) throws IOException { String[] command; if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) { command = new String[]{"reset-uid-state", packageName, "--user", String.valueOf(userId)}; } else command = new String[]{"reset-uid-state", packageName}; BinderShellExecutor.ShellResult result = BinderShellExecutor.execute(getSensorService(), command); if (result.getResultCode() != 0) { throw new IOException("Could not reset sensor."); } } @NonNull private static IBinder getSensorService() { return ProxyBinder.getService("sensorservice"); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/compat/StorageManagerCompat.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.compat; import static io.github.muntashirakon.io.IoUtils.DEFAULT_BUFFER_SIZE; import android.content.Context; import android.os.Build; import android.os.Handler; import android.os.ParcelFileDescriptor; import android.os.Process; import android.os.storage.StorageManager; import android.os.storage.StorageManagerHidden; import android.os.storage.StorageVolume; import android.system.ErrnoException; import android.system.OsConstants; import android.util.Log; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Locale; import dev.rikka.tools.refine.Refine; import io.github.muntashirakon.AppManager.self.SelfPermissions; // Copyright 2018 Fung Gwo // Copyright 2021 Muntashir Al-Islam // Modified from https://gist.github.com/fython/924f8d9019bca75d22de116bb69a54a1 public final class StorageManagerCompat { private static final String TAG = StorageManagerCompat.class.getSimpleName(); /** * Flag indicating that a disk space allocation request should be allowed to * clear up to all reserved disk space. */ public static final int FLAG_ALLOCATE_DEFY_ALL_RESERVED = 1 << 1; /** * Flag indicating that a disk space allocation request should be allowed to * clear up to half of all reserved disk space. */ public static final int FLAG_ALLOCATE_DEFY_HALF_RESERVED = 1 << 2; @IntDef(flag = true, value = { FLAG_ALLOCATE_DEFY_ALL_RESERVED, FLAG_ALLOCATE_DEFY_HALF_RESERVED, }) @Retention(RetentionPolicy.SOURCE) public @interface AllocateFlags { } @NonNull public static StorageManager from(@NonNull Context context) { return (StorageManager) context.getSystemService(Context.STORAGE_SERVICE); } private StorageManagerCompat() { } @NonNull public static StorageVolume[] getVolumeList(@NonNull Context context, int userId, int flags) throws SecurityException { if (!SelfPermissions.checkCrossUserPermission(userId, false, Process.myUid())) { return new StorageVolume[0]; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { return StorageManagerHidden.getVolumeList(userId, flags); } else { StorageVolume[] volumes = Refine.unsafeCast(from(context)).getVolumeList(); if (volumes != null) { return volumes; } } return new StorageVolume[0]; } @NonNull public static ParcelFileDescriptor openProxyFileDescriptor(int mode, @NonNull ProxyFileDescriptorCallbackCompat callback) throws IOException, UnsupportedOperationException { // We cannot use StorageManager#openProxyFileDescriptor directly due to its limitation on how callbacks are handled ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createReliablePipe(); if ((mode & ParcelFileDescriptor.MODE_READ_ONLY) != 0) { // Reading requested i.e. we have to read from our side and write it to the target callback.mHandler.post(() -> { try (ParcelFileDescriptor.AutoCloseOutputStream os = new ParcelFileDescriptor.AutoCloseOutputStream(pipe[1])) { long totalSize = callback.onGetSize(); long currOffset = 0; byte[] buf = new byte[DEFAULT_BUFFER_SIZE]; int size; while ((size = callback.onRead(currOffset, DEFAULT_BUFFER_SIZE, buf)) > 0) { os.write(buf, 0, size); currOffset += size; } if (totalSize > 0 && currOffset != totalSize) { throw new IOException(String.format(Locale.ROOT, "Could not read the whole resource (total = %d, read = %d)", totalSize, currOffset)); } } catch (IOException | ErrnoException e) { Log.e(TAG, "Failed to read file.", e); try { pipe[1].closeWithError(e.getMessage()); } catch (IOException exc) { Log.e(TAG, "Can't even close PFD with error.", exc); } } finally { callback.onRelease(); } }); return pipe[0]; } else if ((mode & ParcelFileDescriptor.MODE_WRITE_ONLY) != 0) { // Writing requested i.e. we have to read from the target and write it to our side callback.mHandler.post(() -> { try (ParcelFileDescriptor.AutoCloseInputStream is = new ParcelFileDescriptor.AutoCloseInputStream(pipe[0])) { long currOffset = 0; byte[] buf = new byte[DEFAULT_BUFFER_SIZE]; int size; while ((size = is.read(buf)) != -1) { callback.onWrite(currOffset, size, buf); currOffset += size; } long totalSize = callback.onGetSize(); if (totalSize > 0 && currOffset != totalSize) { throw new IOException(String.format(Locale.ROOT, "Could not write the whole resource (total = %d, read = %d)", totalSize, currOffset)); } } catch (IOException | ErrnoException e) { Log.e(TAG, "Failed to write file.", e); try { pipe[0].closeWithError(e.getMessage()); } catch (IOException exc) { Log.e(TAG, "Can't even close PFD with error.", exc); } } finally { callback.onRelease(); } }); return pipe[1]; } else { // Should never happen. pipe[0].close(); pipe[1].close(); Log.e(TAG, "Mode " + mode + " is not supported."); throw new UnsupportedOperationException("Mode " + mode + " is not supported."); } } public static abstract class ProxyFileDescriptorCallbackCompat { private final Handler mHandler; public ProxyFileDescriptorCallbackCompat(@NonNull Handler callbackHandler) { mHandler = callbackHandler; } /** * Returns size of bytes provided by the file descriptor. * * @return Size of bytes. * @throws ErrnoException Containing E constants in OsConstants. */ public long onGetSize() throws ErrnoException { throw new ErrnoException("onGetSize", OsConstants.EBADF); } /** * Provides bytes read from file descriptor. * It needs to return exact requested size of bytes unless it reaches file end. * * @param offset Offset in bytes from the file head specifying where to read bytes. If a seek * operation is conducted on the file descriptor, then a read operation is requested, the * offset refrects the proper position of requested bytes. * @param size Size for read bytes. * @param data Byte array to store read bytes. * @return Size of bytes returned by the function. * @throws ErrnoException Containing E constants in OsConstants. */ public int onRead(long offset, int size, byte[] data) throws ErrnoException { throw new ErrnoException("onRead", OsConstants.EBADF); } /** * Handles bytes written to file descriptor. * * @param offset Offset in bytes from the file head specifying where to write bytes. If a seek * operation is conducted on the file descriptor, then a write operation is requested, the * offset refrects the proper position of requested bytes. * @param size Size for write bytes. * @param data Byte array to be written to somewhere. * @return Size of bytes processed by the function. * @throws ErrnoException Containing E constants in OsConstants. */ public int onWrite(long offset, int size, byte[] data) throws ErrnoException { throw new ErrnoException("onWrite", OsConstants.EBADF); } /** * Ensures all the written data are stored in permanent storage device. * For example, if it has data stored in on memory cache, it needs to flush data to storage * device. * * @throws ErrnoException Containing E constants in OsConstants. */ public void onFsync() throws ErrnoException { throw new ErrnoException("onFsync", OsConstants.EINVAL); } /** * Invoked after the file is closed. */ protected void onRelease() { } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/compat/SubscriptionManagerCompat.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.compat; import android.os.Build; import android.os.RemoteException; import android.telephony.SubscriptionInfo; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import com.android.internal.telephony.IPhoneSubInfo; import com.android.internal.telephony.ISub; import java.util.List; import io.github.muntashirakon.AppManager.ipc.ProxyBinder; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.users.Users; import io.github.muntashirakon.AppManager.utils.ExUtils; public class SubscriptionManagerCompat { public static final String TAG = SubscriptionManagerCompat.class.getSimpleName(); @SuppressWarnings("deprecation") @RequiresApi(Build.VERSION_CODES.LOLLIPOP_MR1) @Nullable public static List getActiveSubscriptionInfoList() { try { ISub sub = getSub(); int uid = Users.getSelfOrRemoteUid(); String callingPackage = SelfPermissions.getCallingPackage(uid); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { try { return sub.getActiveSubscriptionInfoList(callingPackage, null); } catch (NoSuchMethodError e) { // Google Pixel return sub.getActiveSubscriptionInfoList(callingPackage, null, true); } } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { return sub.getActiveSubscriptionInfoList(callingPackage, null); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { return sub.getActiveSubscriptionInfoList(callingPackage); } return sub.getActiveSubscriptionInfoList(); } catch (RemoteException e) { return ExUtils.rethrowFromSystemServer(e); } } @SuppressWarnings("deprecation") @Nullable public static String getSubscriberIdForSubscriber(long subId) { try { IPhoneSubInfo sub = getPhoneSubInfo(); int uid = Users.getSelfOrRemoteUid(); String callingPackage = SelfPermissions.getCallingPackage(uid); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { return sub.getSubscriberIdForSubscriber((int) subId, callingPackage, null); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { return sub.getSubscriberIdForSubscriber((int) subId, callingPackage); } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP_MR1) { return sub.getSubscriberIdForSubscriber((int) subId); } return sub.getSubscriberIdForSubscriber(subId); } catch (RemoteException e) { return ExUtils.rethrowFromSystemServer(e); } catch (NullPointerException ignore) { } return null; } @NonNull private static ISub getSub() { return ISub.Stub.asInterface(ProxyBinder.getService("isub")); } @NonNull private static IPhoneSubInfo getPhoneSubInfo() { return IPhoneSubInfo.Stub.asInterface(ProxyBinder.getService("iphonesubinfo")); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/compat/ThumbnailUtilsCompat.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.compat; import static android.media.MediaMetadataRetriever.METADATA_KEY_DURATION; import static android.media.MediaMetadataRetriever.OPTION_CLOSEST_SYNC; import android.content.ContentResolver; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.media.MediaMetadataRetriever; import android.media.ThumbnailUtils; import android.net.Uri; import android.os.Build; import android.os.CancellationSignal; import android.util.Size; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.io.IOException; import java.util.Objects; public class ThumbnailUtilsCompat { /** * Create a thumbnail for given audio file. *

* This method should only be used for files that you have direct access to; * if you'd like to work with media hosted outside your app, consider using * {@link ContentResolver#loadThumbnail(Uri, Size, CancellationSignal)} * which enables remote providers to efficiently cache and invalidate * thumbnails. * * @param context The Context to use when resolving the audio Uri. * @param uri The audio Uri. * @param size The desired thumbnail size. * @throws IOException If any trouble was encountered while generating or loading the thumbnail, or if * {@link CancellationSignal#cancel()} was invoked. */ public static @NonNull Bitmap createAudioThumbnail(@NonNull Context context, @NonNull Uri uri, @NonNull Size size, @Nullable CancellationSignal signal) throws IOException { // Checkpoint before going deeper if (signal != null) signal.throwIfCanceled(); try (MediaMetadataRetriever retriever = new MediaMetadataRetriever()) { retriever.setDataSource(context, uri); final byte[] raw = retriever.getEmbeddedPicture(); if (raw != null) { Bitmap bitmap = BitmapFactory.decodeByteArray(raw, 0, raw.length); return getThumbnail(bitmap, size, true); } } catch (RuntimeException e) { throw new IOException("Failed to create thumbnail", e); } throw new IOException("No album art found"); } /** * Create a thumbnail for given video file. *

* This method should only be used for files that you have direct access to; * if you'd like to work with media hosted outside your app, consider using * {@link ContentResolver#loadThumbnail(Uri, Size, CancellationSignal)} * which enables remote providers to efficiently cache and invalidate * thumbnails. * * @param context The Context to use when resolving the video Uri. * @param uri The video file. * @param size The desired thumbnail size. * @throws IOException If any trouble was encountered while generating or * loading the thumbnail, or if * {@link CancellationSignal#cancel()} was invoked. */ public static @NonNull Bitmap createVideoThumbnail(@NonNull Context context, @NonNull Uri uri, @NonNull Size size, @Nullable CancellationSignal signal) throws IOException { // Checkpoint before going deeper if (signal != null) signal.throwIfCanceled(); try (MediaMetadataRetriever mmr = new MediaMetadataRetriever()) { mmr.setDataSource(context, uri); // Try to retrieve thumbnail from metadata final byte[] raw = mmr.getEmbeddedPicture(); if (raw != null) { Bitmap bitmap = BitmapFactory.decodeByteArray(raw, 0, raw.length); return getThumbnail(bitmap, size, true); } final MediaMetadataRetriever.BitmapParams params; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { params = new MediaMetadataRetriever.BitmapParams(); params.setPreferredConfig(Bitmap.Config.ARGB_8888); } else params = null; // Fall back to middle of video // Note: METADATA_KEY_DURATION unit is in ms, not us. final long thumbnailTimeUs = Long.parseLong(mmr.extractMetadata(METADATA_KEY_DURATION)) * 1000 / 2; // If we're okay with something larger than native format, just // return a frame without up-scaling it if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { return getThumbnail(mmr.getFrameAtTime(thumbnailTimeUs, OPTION_CLOSEST_SYNC, params), size, false); } else { return getThumbnail(mmr.getFrameAtTime(thumbnailTimeUs, OPTION_CLOSEST_SYNC), size, false); } } catch (RuntimeException e) { throw new IOException("Failed to create thumbnail", e); } } private static Bitmap getThumbnail(@NonNull Bitmap bitmap, @NonNull Size size, boolean recycle) { return ThumbnailUtils.extractThumbnail(Objects.requireNonNull(bitmap), size.getWidth(), size.getHeight(), recycle ? ThumbnailUtils.OPTIONS_RECYCLE_INPUT : 0); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/compat/UriCompat.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.compat; import android.net.Uri; import androidx.annotation.Nullable; public final class UriCompat { /** * Index of a component which was not found. */ private final static int NOT_FOUND = -1; /** * Encodes a value it wasn't already encoded. * * @param value string to encode * @param allow characters to allow * @return encoded value */ @Nullable public static String encodeIfNotEncoded(@Nullable String value, @Nullable String allow) { if (value == null) return null; if (isEncoded(value, allow)) return value; return Uri.encode(value, allow); } /** * Returns true if the given string is already encoded to safe characters. * * @param value string to check * @param allow characters to allow * @return true if the string is already encoded or false if it should be encoded */ private static boolean isEncoded(@Nullable String value, @Nullable String allow) { if (value == null) return true; for (int index = 0; index < value.length(); index++) { char c = value.charAt(index); // Allow % because that's the prefix for an encoded character. This method will fail // for decoded strings whose onlyinvalid character is %, but it's assumed that % alone // cannot cause malicious behavior in the framework. if (!isAllowed(c, allow) && c != '%') { return false; } } return true; } /** * Returns true if the given character is allowed. * * @param c character to check * @param allow characters to allow * @return true if the character is allowed or false if it should be * encoded */ private static boolean isAllowed(char c, @Nullable String allow) { return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || "_-!.~'()*".indexOf(c) != NOT_FOUND || (allow != null && allow.indexOf(c) != NOT_FOUND); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/compat/UsageStatsManagerCompat.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.compat; import android.annotation.UserIdInt; import android.app.usage.IUsageStatsManager; import android.app.usage.UsageEvents; import android.content.Context; import android.os.Build; import android.os.RemoteException; import android.os.UserHandleHidden; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresPermission; import java.util.ArrayList; import java.util.Collections; import java.util.List; import io.github.muntashirakon.AppManager.ipc.ProxyBinder; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.users.Users; import io.github.muntashirakon.AppManager.utils.ArrayUtils; import io.github.muntashirakon.AppManager.utils.BroadcastUtils; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.ExUtils; public final class UsageStatsManagerCompat { private static final String SYS_USAGE_STATS_SERVICE = "usagestats"; private static final String USAGE_STATS_SERVICE_NAME; static { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { USAGE_STATS_SERVICE_NAME = Context.USAGE_STATS_SERVICE; } else { USAGE_STATS_SERVICE_NAME = SYS_USAGE_STATS_SERVICE; } } @RequiresPermission("android.permission.PACKAGE_USAGE_STATS") @Nullable public static UsageEvents queryEvents(long beginTime, long endTime, int userId) { try { IUsageStatsManager usm = getUsageStatsManager(); String callingPackage = SelfPermissions.getCallingPackage(Users.getSelfOrRemoteUid()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { return usm.queryEventsForUser(beginTime, endTime, userId, callingPackage); } return usm.queryEvents(beginTime, endTime, callingPackage); } catch (RemoteException e) { return ExUtils.rethrowFromSystemServer(e); } } /** * Note: This method should only be used when sorted entries are required as the operations done * here are expensive. */ @RequiresPermission("android.permission.PACKAGE_USAGE_STATS") @NonNull public static List queryEventsSorted(long beginTime, long endTime, int userId, int[] filterEvents) { List filteredEvents = new ArrayList<>(); UsageEvents events = queryEvents(beginTime, endTime, userId); if (events != null) { while (events.hasNextEvent()) { UsageEvents.Event event = new UsageEvents.Event(); events.getNextEvent(event); if (ArrayUtils.contains(filterEvents, event.getEventType())) { filteredEvents.add(event); } } Collections.sort(filteredEvents, (o1, o2) -> -Long.compare(o1.getTimeStamp(), o2.getTimeStamp())); } return filteredEvents; } public static void setAppInactive(String packageName, @UserIdInt int userId, boolean inactive) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { try { getUsageStatsManager().setAppInactive(packageName, inactive, userId); if (userId != UserHandleHidden.myUserId()) { BroadcastUtils.sendPackageAltered(ContextUtils.getContext(), new String[]{packageName}); } } catch (RemoteException e) { ExUtils.rethrowFromSystemServer(e); } } } @SuppressWarnings("deprecation") public static boolean isAppInactive(String packageName, @UserIdInt int userId) { IUsageStatsManager usm = getUsageStatsManager(); try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { String callingPackage = SelfPermissions.getCallingPackage(Users.getSelfOrRemoteUid()); return usm.isAppInactive(packageName, userId, callingPackage); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { return usm.isAppInactive(packageName, userId); } } catch (RemoteException e) { return ExUtils.rethrowFromSystemServer(e); } // Unsupported Android version: return false return false; } public static IUsageStatsManager getUsageStatsManager() { return IUsageStatsManager.Stub.asInterface(ProxyBinder.getService(USAGE_STATS_SERVICE_NAME)); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/compat/VirtualDeviceManagerCompat.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.compat; import android.content.Context; import android.os.Build; import androidx.annotation.RequiresApi; @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public final class VirtualDeviceManagerCompat { public static final String PERSISTENT_DEVICE_ID_DEFAULT = "default:" + Context.DEVICE_ID_DEFAULT; } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/crypto/AESCrypto.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.crypto; import androidx.annotation.CallSuper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import org.bouncycastle.crypto.engines.AESEngine; import org.bouncycastle.crypto.io.CipherInputStream; import org.bouncycastle.crypto.io.CipherOutputStream; import org.bouncycastle.crypto.modes.GCMBlockCipher; import org.bouncycastle.crypto.modes.GCMModeCipher; import org.bouncycastle.crypto.params.AEADParameters; import org.bouncycastle.crypto.params.KeyParameter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import javax.crypto.SecretKey; import javax.security.auth.DestroyFailedException; import io.github.muntashirakon.AppManager.backup.CryptoUtils; import io.github.muntashirakon.AppManager.crypto.ks.KeyStoreManager; import io.github.muntashirakon.AppManager.crypto.ks.SecretKeyCompat; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.io.IoUtils; import io.github.muntashirakon.io.Path; public class AESCrypto implements Crypto { public static final String TAG = "AESCrypto"; public static final String AES_EXT = ".aes"; public static final String AES_KEY_ALIAS = "backup_aes"; public static final int GCM_IV_SIZE_BYTES = 12; public static final int MAC_SIZE_BITS_OLD = 32; public static final int MAC_SIZE_BITS = 128; private final SecretKey mSecretKey; private final byte[] mIv; @CryptoUtils.Mode private final String mParentMode; private int mMacSizeBits = MAC_SIZE_BITS; public AESCrypto(@NonNull byte[] iv) throws CryptoException { this(iv, CryptoUtils.MODE_AES, null); } @NonNull @Override public String getModeName() { return mParentMode; } protected AESCrypto(@NonNull byte[] iv, @NonNull @CryptoUtils.Mode String mode, @Nullable byte[] encryptedAesKey) throws CryptoException { mIv = iv; mParentMode = mode; switch (mParentMode) { case CryptoUtils.MODE_AES: try { KeyStoreManager keyStoreManager = KeyStoreManager.getInstance(); mSecretKey = keyStoreManager.getSecretKey(AES_KEY_ALIAS); if (mSecretKey == null) { throw new CryptoException("No SecretKey with alias " + AES_KEY_ALIAS); } } catch (Exception e) { throw new CryptoException(e); } break; case CryptoUtils.MODE_RSA: // Hybrid encryption using RSA if (encryptedAesKey == null) { // No encryption key provided, generate one mSecretKey = RSACrypto.generateAesKey(); } else { // Encryption key provided mSecretKey = RSACrypto.decryptAesKey(encryptedAesKey); } break; case CryptoUtils.MODE_ECC: // Hybrid encryption using ECC if (encryptedAesKey == null) { // No encryption key provided, generate one mSecretKey = ECCCrypto.generateAesKey(); } else { // Encryption key provided mSecretKey = ECCCrypto.decryptAesKey(encryptedAesKey); } break; default: throw new CryptoException("Unsupported mode " + mParentMode); } } public void setMacSizeBits(int macSizeBits) { if (macSizeBits == MAC_SIZE_BITS || macSizeBits == MAC_SIZE_BITS_OLD) { mMacSizeBits = macSizeBits; } } @NonNull private AEADParameters getParams() { // We need to generate it dynamically due to MAC size issues return new AEADParameters(new KeyParameter(mSecretKey.getEncoded()), mMacSizeBits, mIv); } @CallSuper @NonNull protected byte[] getEncryptedAesKey() throws CryptoException { if (mParentMode.equals(CryptoUtils.MODE_RSA)) { return RSACrypto.encryptAesKey(mSecretKey); } if (mParentMode.equals(CryptoUtils.MODE_ECC)) { return ECCCrypto.encryptAesKey(mSecretKey); } // Invalid mode throw new CryptoException("Not in RSA or ECC mode"); } @WorkerThread @Override public void encrypt(@NonNull Path[] inputFiles, @NonNull Path[] outputFiles) throws IOException { handleFiles(true, inputFiles, outputFiles); } @Override public void encrypt(@NonNull InputStream unencryptedStream, @NonNull OutputStream encryptedStream) throws IOException { // Init cipher GCMModeCipher cipher = GCMBlockCipher.newInstance(AESEngine.newInstance()); cipher.init(true, getParams()); // Convert unencrypted stream to encrypted stream try (OutputStream cipherOS = new CipherOutputStream(encryptedStream, cipher)) { IoUtils.copy(unencryptedStream, cipherOS); } } @WorkerThread @Override public void decrypt(@NonNull Path[] inputFiles, @NonNull Path[] outputFiles) throws IOException { handleFiles(false, inputFiles, outputFiles); } @Override public void decrypt(@NonNull InputStream encryptedStream, @NonNull OutputStream unencryptedStream) throws IOException { // Init cipher GCMModeCipher cipher = GCMBlockCipher.newInstance(AESEngine.newInstance()); cipher.init(false, getParams()); // Convert encrypted stream to unencrypted stream try (InputStream cipherIS = new CipherInputStream(encryptedStream, cipher)) { IoUtils.copy(cipherIS, unencryptedStream); } } @WorkerThread private void handleFiles(boolean forEncryption, @NonNull Path[] inputFiles, @NonNull Path[] outputFiles) throws IOException { // `files` is never null here if (inputFiles.length == 0) { Log.d(TAG, "No files to de/encrypt"); return; } if (inputFiles.length != outputFiles.length) { throw new IOException("The number of input and output files are not the same."); } // Init cipher GCMModeCipher cipher = GCMBlockCipher.newInstance(AESEngine.newInstance()); cipher.init(forEncryption, getParams()); // Encrypt/decrypt files for (int i = 0; i < inputFiles.length; i++) { Path inputPath = inputFiles[i]; Path outputPath = outputFiles[i]; Log.i(TAG, "Input: %s\nOutput: %s", inputPath, outputPath); try (InputStream is = inputPath.openInputStream(); OutputStream os = outputPath.openOutputStream()) { if (forEncryption) { try (OutputStream cipherOS = new CipherOutputStream(os, cipher)) { IoUtils.copy(is, cipherOS); } } else { // Cipher.DECRYPT_MODE try (InputStream cipherIS = new CipherInputStream(is, cipher)) { IoUtils.copy(cipherIS, os); } } } } // Total success } @Override public void close() { try { SecretKeyCompat.destroy(mSecretKey); } catch (DestroyFailedException e) { e.printStackTrace(); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/crypto/Crypto.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.crypto; import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import io.github.muntashirakon.AppManager.backup.CryptoUtils; import io.github.muntashirakon.io.Path; public interface Crypto extends Closeable { @NonNull @CryptoUtils.Mode String getModeName(); @WorkerThread void encrypt(@NonNull Path[] inputFiles, @NonNull Path[] outputFiles) throws IOException; @WorkerThread void encrypt(@NonNull InputStream unencryptedStream, @NonNull OutputStream encryptedStream) throws IOException; @WorkerThread void decrypt(@NonNull Path[] inputFiles, @NonNull Path[] outputFiles) throws IOException; @WorkerThread void decrypt(@NonNull InputStream encryptedStream, @NonNull OutputStream unencryptedStream) throws IOException; @Override void close(); } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/crypto/CryptoException.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.crypto; public class CryptoException extends Throwable { public CryptoException() { super(); } public CryptoException(String message) { super(message); } public CryptoException(String message, Throwable cause) { super(message, cause); } public CryptoException(Throwable cause) { super(cause); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/crypto/DummyCrypto.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.crypto; import androidx.annotation.NonNull; import java.io.InputStream; import java.io.OutputStream; import io.github.muntashirakon.AppManager.backup.CryptoUtils; import io.github.muntashirakon.io.Path; public class DummyCrypto implements Crypto { @NonNull @Override public String getModeName() { return CryptoUtils.MODE_NO_ENCRYPTION; } @Override public void encrypt(@NonNull Path[] inputFiles, @NonNull Path[] outputFiles) { // Do nothing since both are the same set of files } @Override public void encrypt(@NonNull InputStream unencryptedStream, @NonNull OutputStream encryptedStream) { // Do nothing since both are the same stream } @Override public void decrypt(@NonNull Path[] inputFiles, @NonNull Path[] outputFiles) { // Do nothing since both are the same set of files } @Override public void decrypt(@NonNull InputStream encryptedStream, @NonNull OutputStream unencryptedStream) { // Do nothing since both are the same stream } @Override public void close() { // Nothing to close } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/crypto/ECCCrypto.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.crypto; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.bouncycastle.jce.provider.BouncyCastleProvider; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import io.github.muntashirakon.AppManager.backup.CryptoUtils; import io.github.muntashirakon.AppManager.crypto.ks.KeyPair; import io.github.muntashirakon.AppManager.crypto.ks.KeyStoreManager; public class ECCCrypto extends AESCrypto { public static final String TAG = "ECCCrypto"; public static final String ECC_EXT = ".ecc"; public static final String ECC_KEY_ALIAS = "backup_ecc"; private static final String ECC_CIPHER_TYPE = "ECIES"; private static final int AES_KEY_SIZE_BITS = 256; public ECCCrypto(@NonNull byte[] iv, @Nullable byte[] encryptedAesKey) throws CryptoException { super(iv, CryptoUtils.MODE_ECC, encryptedAesKey); } @NonNull @Override public byte[] getEncryptedAesKey() throws CryptoException { return super.getEncryptedAesKey(); } @NonNull static SecretKey generateAesKey() { SecureRandom random = new SecureRandom(); byte[] key = new byte[AES_KEY_SIZE_BITS/8]; random.nextBytes(key); return new SecretKeySpec(key, "AES"); } @NonNull static SecretKey decryptAesKey(@NonNull byte[] encryptedAesKey) throws CryptoException { KeyPair keyPair; try { KeyStoreManager keyStoreManager = KeyStoreManager.getInstance(); keyPair = keyStoreManager.getKeyPair(ECC_KEY_ALIAS); if (keyPair == null) { throw new CryptoException("No KeyPair with alias " + ECC_KEY_ALIAS); } } catch (Exception e) { throw new CryptoException(e); } try { Cipher cipher = Cipher.getInstance(ECC_CIPHER_TYPE, new BouncyCastleProvider()); cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivateKey()); return new SecretKeySpec(cipher.doFinal(encryptedAesKey), "AES"); } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) { throw new CryptoException(e); } } @NonNull static byte[] encryptAesKey(@NonNull SecretKey key) throws CryptoException { KeyPair keyPair; try { KeyStoreManager keyStoreManager = KeyStoreManager.getInstance(); keyPair = keyStoreManager.getKeyPair(ECC_KEY_ALIAS); if (keyPair == null) { throw new CryptoException("No KeyPair with alias " + ECC_KEY_ALIAS); } } catch (Exception e) { throw new CryptoException(e); } try { Cipher cipher = Cipher.getInstance(ECC_CIPHER_TYPE, new BouncyCastleProvider()); cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPublicKey()); return cipher.doFinal(key.getEncoded()); } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) { throw new CryptoException(e); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/crypto/OpenPGPCrypto.java ================================================ // SPDX-License-Identifier: MIT AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.crypto; import android.app.Notification; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Handler; import android.os.Looper; import android.os.SystemClock; import androidx.annotation.AnyThread; import androidx.annotation.NonNull; import androidx.annotation.UiThread; import androidx.annotation.WorkerThread; import androidx.core.app.NotificationCompat; import androidx.core.app.PendingIntentCompat; import androidx.core.content.ContextCompat; import org.openintents.openpgp.IOpenPgpService2; import org.openintents.openpgp.OpenPgpError; import org.openintents.openpgp.util.OpenPgpApi; import org.openintents.openpgp.util.OpenPgpServiceConnection; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.backup.CryptoUtils; import io.github.muntashirakon.AppManager.intercept.IntentCompat; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.utils.NotificationUtils; import io.github.muntashirakon.io.Path; // Copyright 2018 jensstein public class OpenPGPCrypto implements Crypto { public static final String TAG = "OpenPGPCrypto"; public static final String ACTION_OPEN_PGP_INTERACTION_BEGIN = BuildConfig.APPLICATION_ID + ".action.OPEN_PGP_INTERACTION_BEGIN"; public static final String ACTION_OPEN_PGP_INTERACTION_END = BuildConfig.APPLICATION_ID + ".action.OPEN_PGP_INTERACTION_END"; public static final String GPG_EXT = ".gpg"; private OpenPgpServiceConnection mService; private boolean mSuccessFlag; private boolean mErrorFlag; private Path[] mInputFiles; private Path[] mOutputFiles; private InputStream mIs; private OutputStream mOs; @NonNull private final long[] mKeyIds; private final String mProvider; private Intent mLastIntent; private final Context mContext; private final Handler mHandler; private boolean mIsFileMode; // Whether to en/decrypt a file than an stream private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, @NonNull Intent intent) { if (intent.getAction() == null) return; switch (intent.getAction()) { case ACTION_OPEN_PGP_INTERACTION_BEGIN: break; case ACTION_OPEN_PGP_INTERACTION_END: // TODO: 17/12/21 Handle this better by using CountdownLatch new Thread(() -> { try { doAction(mLastIntent, false); } catch (IOException e) { e.printStackTrace(); } }).start(); break; } } }; @AnyThread public OpenPGPCrypto(@NonNull Context context, @NonNull String keyIdsStr) throws CryptoException { mContext = context; try { String[] keyIds = keyIdsStr.split(","); mKeyIds = new long[keyIds.length]; for (int i = 0; i < keyIds.length; ++i) mKeyIds[i] = Long.parseLong(keyIds[i]); } catch (NumberFormatException e) { throw new CryptoException(e); } mProvider = Prefs.Encryption.getOpenPgpProvider(); mHandler = new Handler(Looper.getMainLooper()); bind(); } @NonNull @Override public String getModeName() { return CryptoUtils.MODE_OPEN_PGP; } @Override public void close() { // Unbind service if (mService != null) mService.unbindFromService(); // Unregister receiver mContext.unregisterReceiver(mReceiver); } @WorkerThread @Override public void decrypt(@NonNull Path[] inputFiles, @NonNull Path[] outputFiles) throws IOException { Intent intent = new Intent(OpenPgpApi.ACTION_DECRYPT_VERIFY); handleFiles(intent, inputFiles, outputFiles); } @Override public void decrypt(@NonNull InputStream encryptedStream, @NonNull OutputStream unencryptedStream) throws IOException { Intent intent = new Intent(OpenPgpApi.ACTION_DECRYPT_VERIFY); handleStreams(intent, encryptedStream, unencryptedStream); } @WorkerThread @Override public void encrypt(@NonNull Path[] inputFiles, @NonNull Path[] outputFiles) throws IOException { Intent intent = new Intent(OpenPgpApi.ACTION_ENCRYPT); intent.putExtra(OpenPgpApi.EXTRA_KEY_IDS, mKeyIds); handleFiles(intent, inputFiles, outputFiles); } @Override public void encrypt(@NonNull InputStream unencryptedStream, @NonNull OutputStream encryptedStream) throws IOException { Intent intent = new Intent(OpenPgpApi.ACTION_ENCRYPT); intent.putExtra(OpenPgpApi.EXTRA_KEY_IDS, mKeyIds); handleStreams(intent, unencryptedStream, encryptedStream); } @WorkerThread private void handleFiles(Intent intent, @NonNull Path[] inputFiles, @NonNull Path[] outputFiles) throws IOException { mIsFileMode = true; waitForServiceBound(); mIs = null; mOs = null; mInputFiles = inputFiles; mOutputFiles = outputFiles; mLastIntent = intent; doAction(intent, true); } @WorkerThread private void handleStreams(Intent intent, @NonNull InputStream is, @NonNull OutputStream os) throws IOException { mIsFileMode = false; waitForServiceBound(); mIs = is; mOs = os; mInputFiles = new Path[0]; mOutputFiles = new Path[0]; mLastIntent = intent; doAction(intent, true); } @WorkerThread private void doAction(Intent intent, boolean waitForResult) throws IOException { if (mIsFileMode) { doActionForFiles(intent, waitForResult); } else { doActionForStream(intent, waitForResult); } } @WorkerThread private void doActionForFiles(Intent intent, boolean waitForResult) throws IOException { mErrorFlag = false; // `files` is never null here if (mInputFiles.length == 0) { Log.d(TAG, "No files to de/encrypt"); return; } if (mInputFiles.length != mOutputFiles.length) { throw new IOException("The number of input and output files are not the same."); } for (int i = 0; i < mInputFiles.length; i++) { Path inputPath = mInputFiles[i]; Path outputPath = mOutputFiles[i]; Log.i(TAG, "Input: %s\nOutput: %s", inputPath, outputPath); InputStream is = inputPath.openInputStream(); OutputStream os = outputPath.openOutputStream(); OpenPgpApi api = new OpenPgpApi(mContext, mService.getService()); Intent result = api.executeApi(intent, is, os); mHandler.post(() -> handleResult(result)); if (waitForResult) waitForResult(); if (mErrorFlag) { outputPath.delete(); throw new IOException("Error occurred during en/decryption process"); } } // Total success } @WorkerThread private void doActionForStream(Intent intent, boolean waitForResult) throws IOException { mErrorFlag = false; OpenPgpApi api = new OpenPgpApi(mContext, mService.getService()); Intent result = api.executeApi(intent, mIs, mOs); mHandler.post(() -> handleResult(result)); if (waitForResult) waitForResult(); if (mErrorFlag) { throw new IOException("Error occurred during en/decryption process"); } } private void bind() { mService = new OpenPgpServiceConnection(mContext, mProvider, new OpenPgpServiceConnection.OnBound() { @Override public void onBound(IOpenPgpService2 service) { Log.i(OpenPgpApi.TAG, "Service bound."); } @Override public void onError(Exception e) { Log.e(OpenPgpApi.TAG, "Exception on binding.", e); } } ); mService.bindToService(); // Start broadcast receiver IntentFilter filter = new IntentFilter(ACTION_OPEN_PGP_INTERACTION_BEGIN); filter.addAction(ACTION_OPEN_PGP_INTERACTION_END); ContextCompat.registerReceiver(mContext, mReceiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED); } @WorkerThread private void waitForServiceBound() throws IOException { int i = 0; while (mService.getService() == null) { if (i % 20 == 0) { Log.i(TAG, "Waiting for openpgp-api service to be bound"); } SystemClock.sleep(100); if (i > 1000) break; i++; } if (mService.getService() == null) { throw new IOException("OpenPGPService could not be bound."); } } @WorkerThread private void waitForResult() { int i = 0; while (!mSuccessFlag && !mErrorFlag) { if (i % 200 == 0) Log.i(TAG, "Waiting for user interaction"); SystemClock.sleep(100); if (i > 1000) break; i++; } } @UiThread private void handleResult(@NonNull Intent result) { mSuccessFlag = false; switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { case OpenPgpApi.RESULT_CODE_SUCCESS: Log.i(TAG, "en/decryption successful."); mSuccessFlag = true; break; case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: { Log.i(TAG, "User interaction required. Sending intent..."); Intent broadcastIntent = new Intent(OpenPGPCrypto.ACTION_OPEN_PGP_INTERACTION_BEGIN); broadcastIntent.setPackage(mContext.getPackageName()); mContext.sendBroadcast(broadcastIntent); // Intent wrapper Intent intent = new Intent(mContext, OpenPGPCryptoActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra(OpenPgpApi.RESULT_INTENT, IntentCompat.getParcelableExtra(result, OpenPgpApi.RESULT_INTENT, PendingIntent.class)); String openPGP = "Open PGP"; // We don't need a DELETE intent since the time will be expired anyway NotificationCompat.Builder builder = NotificationUtils.getHighPriorityNotificationBuilder(mContext) .setAutoCancel(true) .setDefaults(Notification.DEFAULT_ALL) .setWhen(System.currentTimeMillis()) .setSmallIcon(R.drawable.ic_default_notification) .setTicker(openPGP) .setContentTitle(openPGP) .setSubText(openPGP) .setContentText(mContext.getString(R.string.allow_open_pgp_operation)); builder.setContentIntent(PendingIntentCompat.getActivity(mContext, 0, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT, false)); NotificationUtils.displayHighPriorityNotification(mContext, builder.build()); break; } case OpenPgpApi.RESULT_CODE_ERROR: mErrorFlag = true; OpenPgpError error = IntentCompat.getParcelableExtra(result, OpenPgpApi.RESULT_ERROR, OpenPgpError.class); if (error != null) { Log.e(TAG, "handleResult: (%d) %s", error.getErrorId(), error.getMessage()); } else Log.e(TAG, "handleResult: Error occurred during en/decryption process"); break; } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/crypto/OpenPGPCryptoActivity.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.crypto; import android.app.PendingIntent; import android.content.Intent; import android.os.Bundle; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.IntentSenderRequest; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.Nullable; import org.openintents.openpgp.util.OpenPgpApi; import java.util.Objects; import io.github.muntashirakon.AppManager.BaseActivity; import io.github.muntashirakon.AppManager.intercept.IntentCompat; public class OpenPGPCryptoActivity extends BaseActivity { private final ActivityResultLauncher mConfirmationLauncher = registerForActivityResult( new ActivityResultContracts.StartIntentSenderForResult(), result -> { Intent broadcastIntent = new Intent(OpenPGPCrypto.ACTION_OPEN_PGP_INTERACTION_END); broadcastIntent.setPackage(getPackageName()); sendBroadcast(broadcastIntent); finish(); }); @Override public boolean getTransparentBackground() { return true; } @Override protected void onAuthenticated(@Nullable Bundle savedInstanceState) { if (getIntent() != null) onNewIntent(getIntent()); else finish(); } @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); PendingIntent pi = Objects.requireNonNull(IntentCompat.getParcelableExtra(intent, OpenPgpApi.RESULT_INTENT, PendingIntent.class)); mConfirmationLauncher.launch(new IntentSenderRequest.Builder(pi).build()); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/crypto/RSACrypto.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.crypto; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import io.github.muntashirakon.AppManager.backup.CryptoUtils; import io.github.muntashirakon.AppManager.crypto.ks.KeyPair; import io.github.muntashirakon.AppManager.crypto.ks.KeyStoreManager; public class RSACrypto extends AESCrypto { public static final String TAG = "RSACrypto"; public static final String RSA_EXT = ".rsa"; public static final String RSA_KEY_ALIAS = "backup_rsa"; private static final String RSA_CIPHER_TYPE = "RSA/NONE/OAEPPadding"; // 42 bytes padding private static final int AES_KEY_SIZE_BITS = 256; public RSACrypto(@NonNull byte[] iv, @Nullable byte[] encryptedAesKey) throws CryptoException { // This class extends AES crypto as RSA uses hybrid encryption super(iv, CryptoUtils.MODE_RSA, encryptedAesKey); } @NonNull @Override public byte[] getEncryptedAesKey() throws CryptoException { return super.getEncryptedAesKey(); } @NonNull static SecretKey generateAesKey() { SecureRandom random = new SecureRandom(); byte[] key = new byte[AES_KEY_SIZE_BITS/8]; random.nextBytes(key); return new SecretKeySpec(key, "AES"); } @NonNull static SecretKey decryptAesKey(@NonNull byte[] encryptedAesKey) throws CryptoException { // We only have 32/64 bytes AES key with either 256 or 512 bytes minus 42 bytes of data, // so it should work without issues KeyPair keyPair; try { KeyStoreManager keyStoreManager = KeyStoreManager.getInstance(); keyPair = keyStoreManager.getKeyPair(RSA_KEY_ALIAS); if (keyPair == null) { throw new CryptoException("No KeyPair with alias " + RSA_KEY_ALIAS); } } catch (Exception e) { throw new CryptoException(e); } try { Cipher cipher = Cipher.getInstance(RSA_CIPHER_TYPE); cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivateKey()); return new SecretKeySpec(cipher.doFinal(encryptedAesKey), "AES"); } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) { throw new CryptoException(e); } } @NonNull static byte[] encryptAesKey(@NonNull SecretKey key) throws CryptoException { // We only have 32/64 bytes AES key with either 256 or 512 bytes minus 42 bytes of data, // so it should work without issues KeyPair keyPair; try { KeyStoreManager keyStoreManager = KeyStoreManager.getInstance(); keyPair = keyStoreManager.getKeyPair(RSA_KEY_ALIAS); if (keyPair == null) { throw new CryptoException("No KeyPair with alias " + RSA_KEY_ALIAS); } } catch (Exception e) { throw new CryptoException(e); } try { Cipher cipher = Cipher.getInstance(RSA_CIPHER_TYPE); cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPublicKey()); return cipher.doFinal(key.getEncoded()); } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) { throw new CryptoException(e); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/crypto/RandomChar.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.crypto; import androidx.annotation.NonNull; import java.security.SecureRandom; import java.util.Locale; import java.util.Objects; import java.util.Random; public class RandomChar { public static final String UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; public static final String LOWERCASE = UPPERCASE.toLowerCase(Locale.ROOT); public static final String DIGITS = "0123456789"; public static final String ALPHA_NUMERIC = UPPERCASE + LOWERCASE + DIGITS; private final Random mRandom; private final char[] mSymbols; public RandomChar() { this(new SecureRandom()); } public RandomChar(@NonNull Random random) { this(random, ALPHA_NUMERIC); } public RandomChar(@NonNull Random random, @NonNull String symbols) { if (symbols.length() < 2) throw new IllegalArgumentException(); mRandom = Objects.requireNonNull(random); mSymbols = symbols.toCharArray(); } public void nextChars(@NonNull char[] chars) { for (int idx = 0; idx < chars.length; ++idx) { chars[idx] = mSymbols[mRandom.nextInt(mSymbols.length)]; } } public char nextChar() { return mSymbols[mRandom.nextInt(mSymbols.length)]; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/crypto/auth/AuthFeatureDemultiplexer.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.crypto.auth; import android.content.Intent; import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.util.Objects; import io.github.muntashirakon.AppManager.BaseActivity; import io.github.muntashirakon.AppManager.profiles.ProfileApplierActivity; public class AuthFeatureDemultiplexer extends BaseActivity { public static final String EXTRA_FEATURE = "feature"; @Override protected void onAuthenticated(@Nullable Bundle savedInstanceState) { Intent intent = getIntent(); if (!intent.hasExtra(EXTRA_AUTH) || !intent.hasExtra(EXTRA_FEATURE)) { // It does not have the required extras, ignore the request finishAndRemoveTask(); return; } handleRequest(intent); } @Override public boolean getTransparentBackground() { return true; } private void handleRequest(@NonNull Intent intent) { String auth = intent.getStringExtra(EXTRA_AUTH); String feature = intent.getStringExtra(EXTRA_FEATURE); intent.removeExtra(EXTRA_AUTH); intent.removeExtra(EXTRA_FEATURE); if (!AuthManager.getKey().equals(auth)) { // Invalid authorization key // TODO: 16/3/22 Display a nice error message finishAndRemoveTask(); return; } switch (Objects.requireNonNull(feature)) { case "profile": launchProfile(intent); break; default: throw new RuntimeException("Invalid feature: " + feature); } finish(); } public void launchProfile(@NonNull Intent intent) { String profileId = intent.getStringExtra(ProfileApplierActivity.EXTRA_PROFILE_ID); String state = intent.getStringExtra(ProfileApplierActivity.EXTRA_STATE); startActivity(ProfileApplierActivity.getAutomationIntent(getApplicationContext(), profileId, state)); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/crypto/auth/AuthManager.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.crypto.auth; import androidx.annotation.NonNull; import io.github.muntashirakon.AppManager.crypto.RandomChar; import io.github.muntashirakon.AppManager.utils.AppPref; public final class AuthManager { public static final int AUTH_KEY_SIZE = 24; @NonNull public static String getKey() { return AppPref.getString(AppPref.PrefKey.PREF_AUTHORIZATION_KEY_STR); } public static void setKey(@NonNull String key) { AppPref.set(AppPref.PrefKey.PREF_AUTHORIZATION_KEY_STR, key); } @NonNull public static String generateKey() { char[] authKey = new char[AUTH_KEY_SIZE]; new RandomChar().nextChars(authKey); return new String(authKey); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/crypto/auth/AuthManagerActivity.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.crypto.auth; import android.os.Bundle; import android.view.MenuItem; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.textfield.TextInputEditText; import com.google.android.material.textfield.TextInputLayout; import io.github.muntashirakon.AppManager.BaseActivity; import io.github.muntashirakon.AppManager.R; public class AuthManagerActivity extends BaseActivity { private TextInputLayout mAuthKeyLayout; private TextInputEditText mAuthKeyField; @Override protected void onAuthenticated(@Nullable Bundle savedInstanceState) { setContentView(R.layout.activity_auth_management); setSupportActionBar(findViewById(R.id.toolbar)); findViewById(R.id.progress_linear).setVisibility(View.GONE); mAuthKeyLayout = findViewById(R.id.auth_field); mAuthKeyField = findViewById(android.R.id.text1); mAuthKeyField.setText(AuthManager.getKey()); mAuthKeyLayout.setEndIconOnClickListener(v -> new MaterialAlertDialogBuilder(this) .setTitle(R.string.regenerate_auth_key) .setMessage(R.string.regenerate_auth_key_warning) .setNegativeButton(R.string.no, null) .setPositiveButton(R.string.yes, (dialog, which) -> { String authKey = AuthManager.generateKey(); AuthManager.setKey(authKey); mAuthKeyField.setText(authKey); }) .show()); } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == android.R.id.home) { finish(); return true; } return super.onOptionsItemSelected(item); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/crypto/ks/AesEncryptedData.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.crypto.ks; import androidx.core.util.Pair; class AesEncryptedData extends Pair { /** * Constructor for a Pair. * * @param iv the iv object in the Pair * @param encryptedData the encryptedData object in the pair */ public AesEncryptedData(byte[] iv, byte[] encryptedData) { super(iv, encryptedData); } public byte[] getIv() { return first; } public byte[] getEncryptedData() { return second; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/crypto/ks/CompatUtil.java ================================================ // SPDX-License-Identifier: Apache-2.0 AND GPL-3-or-later package io.github.muntashirakon.AppManager.crypto.ks; import android.content.Context; import android.content.SharedPreferences; import android.os.Build; import android.security.keystore.KeyGenParameterSpec; import android.security.keystore.KeyProperties; import android.util.Base64; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.io.IOException; import java.math.BigInteger; import java.nio.ByteBuffer; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.PrivateKey; import java.security.SecureRandom; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.security.spec.AlgorithmParameterSpec; import java.security.spec.RSAKeyGenParameterSpec; import java.util.Calendar; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.KeyGenerator; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.crypto.ShortBufferException; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import javax.security.auth.DestroyFailedException; import javax.security.auth.x500.X500Principal; import io.github.muntashirakon.AppManager.logs.Log; // Copyright 2021 Muntashir Al-Islam // Copyright 2018 New Vector Ltd public class CompatUtil { private static final String TAG = CompatUtil.class.getSimpleName(); private static final String ANDROID_KEY_STORE_PROVIDER = "AndroidKeyStore"; private static final String AES_GCM_CIPHER_TYPE = "AES/GCM/NoPadding"; private static final int AES_GCM_KEY_SIZE_IN_BITS = 128; private static final int AES_GCM_IV_LENGTH = 12; private static final String AES_LOCAL_PROTECTION_KEY_ALIAS = "aes_local_protection"; private static final String RSA_WRAP_LOCAL_PROTECTION_KEY_ALIAS = "rsa_wrap_local_protection"; private static final String RSA_WRAP_CIPHER_TYPE = "RSA/NONE/PKCS1Padding"; private static final String AES_WRAPPED_PROTECTION_KEY_SHARED_PREFERENCE = "aes_wrapped_local_protection"; private static final String SHARED_KEY_ANDROID_VERSION_WHEN_KEY_HAS_BEEN_GENERATED = "android_version_when_key_has_been_generated"; private static SecureRandom sPrng; /** * Returns the AES key used for local storage encryption/decryption with AES/GCM. * The key is created if it does not exist already in the keystore. * From Marshmallow, this key is generated and operated directly from the android keystore. * From KitKat and before Marshmallow, this key is stored in the application shared preferences * wrapped by a RSA key generated and operated directly from the android keystore. * * @param context the context holding the application shared preferences */ @SuppressWarnings({"deprecation", "InlinedApi"}) @NonNull private static synchronized SecretKeyAndVersion getAesGcmLocalProtectionKey(@NonNull Context context) throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException, NoSuchProviderException, InvalidAlgorithmParameterException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, UnrecoverableKeyException { KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE_PROVIDER); keyStore.load(null); Log.i(TAG, "Loading local protection key"); SharedPreferences sharedPreferences = context.getSharedPreferences("keystore", Context.MODE_PRIVATE); // Get the version of Android when the key has been generated, default to the current version of the system. // In the latter case, the key will be generated. int androidVersionWhenTheKeyHasBeenGenerated = sharedPreferences.getInt( SHARED_KEY_ANDROID_VERSION_WHEN_KEY_HAS_BEEN_GENERATED, Build.VERSION.SDK_INT); // Check if there's a key in the Android keystore (M and later) if (keyStore.containsAlias(AES_LOCAL_PROTECTION_KEY_ALIAS)) { Log.i(TAG, "AES local protection key found in keystore"); SecretKey secretKey = (SecretKey) keyStore.getKey(AES_LOCAL_PROTECTION_KEY_ALIAS, null); if (secretKey == null) { throw new KeyStoreException("Could not load AES local protection key from keystore"); } return new SecretKeyAndVersion(secretKey, androidVersionWhenTheKeyHasBeenGenerated); } // Check if a key has been created on version < M (such as, in case of an OS upgrade) SecretKey secretKey = readKeyApiL(sharedPreferences, keyStore); if (secretKey != null) { return new SecretKeyAndVersion(secretKey, androidVersionWhenTheKeyHasBeenGenerated); } // Otherwise generate key if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Log.i(TAG, "Generating AES key with keystore"); KeyGenerator generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE_PROVIDER); generator.init(new KeyGenParameterSpec.Builder(AES_LOCAL_PROTECTION_KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) .setBlockModes(KeyProperties.BLOCK_MODE_GCM) .setKeySize(AES_GCM_KEY_SIZE_IN_BITS) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) .build()); secretKey = generator.generateKey(); sharedPreferences.edit() .putInt(SHARED_KEY_ANDROID_VERSION_WHEN_KEY_HAS_BEEN_GENERATED, Build.VERSION.SDK_INT) .apply(); return new SecretKeyAndVersion(secretKey, androidVersionWhenTheKeyHasBeenGenerated); } Log.i(TAG, "Generating RSA key pair with keystore"); KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", ANDROID_KEY_STORE_PROVIDER); Calendar start = Calendar.getInstance(); Calendar end = Calendar.getInstance(); end.add(Calendar.YEAR, 10); generator.initialize(new android.security.KeyPairGeneratorSpec.Builder(context) .setAlgorithmParameterSpec(new RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4)) .setAlias(RSA_WRAP_LOCAL_PROTECTION_KEY_ALIAS) .setSubject(new X500Principal("CN=App Manager")) .setStartDate(start.getTime()) .setEndDate(end.getTime()) .setSerialNumber(BigInteger.ONE) .build()); KeyPair keyPair = generator.generateKeyPair(); Log.i(TAG, "Generating wrapped AES key"); byte[] aesKeyRaw = new byte[AES_GCM_KEY_SIZE_IN_BITS / Byte.SIZE]; getPrng().nextBytes(aesKeyRaw); secretKey = new SecretKeySpec(aesKeyRaw, "AES"); Cipher cipher = Cipher.getInstance(RSA_WRAP_CIPHER_TYPE); cipher.init(Cipher.WRAP_MODE, keyPair.getPublic()); byte[] wrappedAesKey = cipher.wrap(secretKey); sharedPreferences.edit() .putString(AES_WRAPPED_PROTECTION_KEY_SHARED_PREFERENCE, Base64.encodeToString(wrappedAesKey, 0)) .putInt(SHARED_KEY_ANDROID_VERSION_WHEN_KEY_HAS_BEEN_GENERATED, Build.VERSION.SDK_INT) .apply(); return new SecretKeyAndVersion(secretKey, androidVersionWhenTheKeyHasBeenGenerated); } /** * Read the key, which may have been stored when the OS was < M * * @param sharedPreferences shared pref * @param keyStore key store * @return the key if it exists or null */ @Nullable private static SecretKey readKeyApiL(@NonNull SharedPreferences sharedPreferences, @NonNull KeyStore keyStore) throws KeyStoreException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, UnrecoverableKeyException { String wrappedAesKeyString = sharedPreferences.getString(AES_WRAPPED_PROTECTION_KEY_SHARED_PREFERENCE, null); if (wrappedAesKeyString != null && keyStore.containsAlias(RSA_WRAP_LOCAL_PROTECTION_KEY_ALIAS)) { Log.i(TAG, "RSA + wrapped AES local protection keys found in keystore"); PrivateKey privateKey = (PrivateKey) keyStore.getKey(RSA_WRAP_LOCAL_PROTECTION_KEY_ALIAS, null); byte[] wrappedAesKey = Base64.decode(wrappedAesKeyString, 0); Cipher cipher = Cipher.getInstance(RSA_WRAP_CIPHER_TYPE); cipher.init(Cipher.UNWRAP_MODE, privateKey); return (SecretKey) cipher.unwrap(wrappedAesKey, "AES", Cipher.SECRET_KEY); } // Key does not exist return null; } /** * Returns the unique SecureRandom instance shared for all local storage encryption operations. */ @NonNull public static SecureRandom getPrng() { if (sPrng == null) { sPrng = new SecureRandom(); } return sPrng; } /** * Encrypt the given data * * @param unencryptedData The data to be encrypted * @param context The context holding the application shared preferences */ @NonNull public static AesEncryptedData getEncryptedData(@NonNull byte[] unencryptedData, @NonNull Context context) throws InvalidAlgorithmParameterException, UnrecoverableKeyException, NoSuchPaddingException, IllegalBlockSizeException, CertificateException, KeyStoreException, NoSuchAlgorithmException, IOException, NoSuchProviderException, InvalidKeyException, BadPaddingException, DestroyFailedException { SecretKeyAndVersion keyAndVersion = getAesGcmLocalProtectionKey(context); Cipher cipher = Cipher.getInstance(AES_GCM_CIPHER_TYPE); byte[] iv; if (keyAndVersion.getAndroidVersionWhenTheKeyHasBeenGenerated() >= Build.VERSION_CODES.M) { cipher.init(Cipher.ENCRYPT_MODE, keyAndVersion.getSecretKey(), getPrng()); iv = cipher.getIV(); } else { iv = new byte[AES_GCM_IV_LENGTH]; getPrng().nextBytes(iv); cipher.init(Cipher.ENCRYPT_MODE, keyAndVersion.getSecretKey(), new IvParameterSpec(iv)); } if (iv.length != AES_GCM_IV_LENGTH) { throw new InvalidAlgorithmParameterException("Invalid IV length " + iv.length); } byte[] encryptedData = cipher.doFinal(unencryptedData); SecretKeyCompat.destroy(keyAndVersion.getSecretKey()); return new AesEncryptedData(iv, encryptedData); } /** * Decrypt given data. * * @param context The context holding the application shared preferences * @param encryptedData Data to be decrypted * @return Decrypted data. */ @NonNull public static byte[] decryptData(@NonNull Context context, @NonNull byte[] encryptedData) throws NoSuchPaddingException, NoSuchAlgorithmException, CertificateException, InvalidKeyException, KeyStoreException, UnrecoverableKeyException, IllegalBlockSizeException, NoSuchProviderException, InvalidAlgorithmParameterException, IOException, ShortBufferException, BadPaddingException { ByteBuffer encryptedBuffer = ByteBuffer.wrap(encryptedData); int iv_len = encryptedBuffer.get(); if (iv_len != AES_GCM_IV_LENGTH) { throw new InvalidAlgorithmParameterException("Invalid IV length " + iv_len); } byte[] iv = new byte[AES_GCM_IV_LENGTH]; encryptedBuffer.get(iv); Cipher cipher = Cipher.getInstance(AES_GCM_CIPHER_TYPE); SecretKeyAndVersion keyAndVersion = getAesGcmLocalProtectionKey(context); AlgorithmParameterSpec spec; if (keyAndVersion.getAndroidVersionWhenTheKeyHasBeenGenerated() >= Build.VERSION_CODES.M) { spec = new GCMParameterSpec(AES_GCM_KEY_SIZE_IN_BITS, iv); } else { spec = new IvParameterSpec(iv); } cipher.init(Cipher.DECRYPT_MODE, keyAndVersion.getSecretKey(), spec); ByteBuffer decryptedBuffer = ByteBuffer.allocate(cipher.getOutputSize(encryptedBuffer.remaining())); cipher.doFinal(encryptedBuffer, decryptedBuffer); return decryptedBuffer.array(); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/crypto/ks/KeyPair.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.crypto.ks; import androidx.core.util.Pair; import java.security.PrivateKey; import java.security.PublicKey; import java.security.cert.Certificate; import javax.security.auth.DestroyFailedException; public class KeyPair extends Pair { public KeyPair(PrivateKey first, Certificate second) { super(first, second); } public PrivateKey getPrivateKey() { return first; } public PublicKey getPublicKey() { return second.getPublicKey(); } public Certificate getCertificate() { return second; } public void destroy() throws DestroyFailedException { try { first.destroy(); } catch (NoSuchMethodError ignore) { } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/crypto/ks/KeyStoreActivity.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.crypto.ks; import android.content.Intent; import android.os.Bundle; import android.text.Editable; import android.text.TextUtils; import androidx.activity.EdgeToEdge; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.self.life.BuildExpiryChecker; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.utils.Utils; import io.github.muntashirakon.dialog.TextInputDialogBuilder; public class KeyStoreActivity extends AppCompatActivity { public static final String EXTRA_ALIAS = "key"; public static final String EXTRA_KS = "ks"; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { setTheme(Prefs.Appearance.getTransparentAppTheme()); EdgeToEdge.enable(this); super.onCreate(savedInstanceState); if (Boolean.TRUE.equals(BuildExpiryChecker.buildExpired())) { // Build has expired BuildExpiryChecker.getBuildExpiredDialog(this, (dialog, which) -> processIntentAndFinish(getIntent())).show(); return; } processIntentAndFinish(getIntent()); } @Override protected void onNewIntent(@NonNull Intent intent) { super.onNewIntent(intent); processIntentAndFinish(intent); } private void processIntentAndFinish(@Nullable Intent intent) { if (intent == null) { finish(); return; } String alias = intent.getStringExtra(EXTRA_ALIAS); if (alias != null) { displayInputKeyStoreAliasPassword(alias); return; } if (intent.hasExtra(EXTRA_KS)) { AlertDialog ksDialog; if (KeyStoreManager.hasKeyStore()) { // We have a keystore but not a working password, input a password (probably due to system restore) ksDialog = KeyStoreManager.inputKeyStorePassword(this, this::finish); } else { // We neither have a KeyStore nor a password. Create a password (not necessarily a keystore) ksDialog = KeyStoreManager.generateAndDisplayKeyStorePassword(this, this::finish); } ksDialog.show(); return; } finish(); } /** * @deprecated Kept for migratory purposes only, deprecated since v2.6.3. To be removed in v3.0.0. */ @Deprecated private void displayInputKeyStoreAliasPassword(@NonNull String alias) { new TextInputDialogBuilder(this, getString(R.string.input_keystore_alias_pass, alias)) .setTitle(getString(R.string.input_keystore_alias_pass, alias)) .setHelperText(getString(R.string.input_keystore_alias_pass_description, alias)) .setPositiveButton(R.string.ok, (dialog, which, inputText, isChecked) -> savePass(KeyStoreManager.getPrefAlias(alias), inputText) ) .setCancelable(false) .setOnDismissListener(dialog -> finish()) .show(); } private void savePass(@NonNull String prefKey, @Nullable Editable rawPassword) { char[] password; if (TextUtils.isEmpty(rawPassword)) { try { password = KeyStoreManager.getInstance().getAmKeyStorePassword(); } catch (Exception e) { Log.e(KeyStoreManager.TAG, "Could not get KeyStore password", e); Intent broadcastIntent = new Intent(KeyStoreManager.ACTION_KS_INTERACTION_END); broadcastIntent.setPackage(getPackageName()); sendBroadcast(broadcastIntent); return; } } else { password = new char[rawPassword.length()]; rawPassword.getChars(0, rawPassword.length(), password, 0); } KeyStoreManager.savePass(this, prefKey, password); Utils.clearChars(password); Intent broadcastIntent = new Intent(KeyStoreManager.ACTION_KS_INTERACTION_END); broadcastIntent.setPackage(getPackageName()); sendBroadcast(broadcastIntent); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/crypto/ks/KeyStoreManager.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.crypto.ks; import android.annotation.SuppressLint; import android.app.Notification; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.text.Editable; import android.text.TextUtils; import android.util.Base64; import android.view.View; import android.widget.Button; import androidx.annotation.CheckResult; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import androidx.appcompat.app.AlertDialog; import androidx.core.app.NotificationCompat; import androidx.core.app.PendingIntentCompat; import androidx.core.content.ContextCompat; import androidx.fragment.app.FragmentActivity; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.textfield.TextInputEditText; import com.google.android.material.textfield.TextInputLayout; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.security.Key; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.UnrecoverableKeyException; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import javax.crypto.SecretKey; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.adb.AdbConnectionManager; import io.github.muntashirakon.AppManager.apk.signing.Signer; import io.github.muntashirakon.AppManager.crypto.AESCrypto; import io.github.muntashirakon.AppManager.crypto.RSACrypto; import io.github.muntashirakon.AppManager.crypto.RandomChar; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.NotificationUtils; import io.github.muntashirakon.AppManager.utils.Utils; public class KeyStoreManager { public static final String TAG = "KSManager"; public static final String AM_KEYSTORE_FILE_NAME = "am_keystore.bks"; // Java KeyStore public static final File AM_KEYSTORE_FILE; private static final String AM_KEYSTORE = "BKS"; // KeyStore.getDefaultType() == JKS private static final String PREF_AM_KEYSTORE_PREFIX = "ks_"; private static final String PREF_AM_KEYSTORE_PASS = "kspass"; private static final SharedPreferences sSharedPreferences; public static final String ACTION_KS_INTERACTION_BEGIN = BuildConfig.APPLICATION_ID + ".action.KS_INTERACTION_BEGIN"; public static final String ACTION_KS_INTERACTION_END = BuildConfig.APPLICATION_ID + ".action.KS_INTERACTION_END"; static { Context ctx = ContextUtils.getContext(); AM_KEYSTORE_FILE = new File(ctx.getFilesDir(), AM_KEYSTORE_FILE_NAME); sSharedPreferences = ctx.getSharedPreferences("keystore", Context.MODE_PRIVATE); } @SuppressLint("StaticFieldLeak") private static KeyStoreManager sInstance; public static KeyStoreManager getInstance() throws Exception { if (sInstance == null) { sInstance = new KeyStoreManager(); } return sInstance; } public static void reloadKeyStore() throws Exception { sInstance = new KeyStoreManager(); } @NonNull public static AlertDialog generateAndDisplayKeyStorePassword(@NonNull FragmentActivity activity, @Nullable Runnable dismissListener) { char[] password = new char[30]; RandomChar randomChar = new RandomChar(); randomChar.nextChars(password); savePass(activity, PREF_AM_KEYSTORE_PASS, password); return displayKeyStorePassword(activity, password, dismissListener); } @NonNull public static AlertDialog displayKeyStorePassword(@NonNull FragmentActivity activity, @NonNull char[] password, @Nullable Runnable dismissListener) { View view = activity.getLayoutInflater().inflate(R.layout.dialog_keystore_password, null); TextInputEditText editText = view.findViewById(R.id.ks_pass); editText.setText(password, 0, password.length); return new MaterialAlertDialogBuilder(activity) .setTitle(R.string.keystore) .setView(view) .setNegativeButton(R.string.close, null) .setCancelable(false) .setOnDismissListener(dialog -> { Utils.clearChars(password); if (dismissListener != null) { dismissListener.run(); } }) .create(); } @NonNull public static AlertDialog inputKeyStorePassword(@NonNull FragmentActivity activity, @Nullable Runnable dismissListener) { AtomicBoolean dismiss = new AtomicBoolean(true); View view = activity.getLayoutInflater().inflate(R.layout.dialog_keystore_password, null); TextInputEditText editText = view.findViewById(R.id.ks_pass); editText.setCursorVisible(true); view.findViewById(android.R.id.text1).setVisibility(View.GONE); TextInputLayout tv = view.findViewById(android.R.id.text2); tv.setHint(R.string.input_keystore_pass); AlertDialog alertDialog = new MaterialAlertDialogBuilder(activity) .setTitle(R.string.keystore) .setView(view) .setPositiveButton(R.string.ok, null) .setNegativeButton(R.string.delete, null) .setCancelable(false) .setOnDismissListener(dialog -> { if (dismissListener != null && dismiss.get()) { dismissListener.run(); } }) .create(); alertDialog.setOnShowListener(dialog -> { AlertDialog d = (AlertDialog) dialog; Button okButton = d.getButton(AlertDialog.BUTTON_POSITIVE); Button deleteButton = d.getButton(AlertDialog.BUTTON_NEGATIVE); okButton.setOnClickListener(v -> { Editable editable = editText.getText(); if (TextUtils.isEmpty(editable)) { editText.setError(activity.getString(R.string.keystore_pass_cannot_be_empty)); return; } //noinspection ConstantConditions char[] password = new char[editable.length()]; editable.getChars(0, editable.length(), password, 0); savePass(activity, PREF_AM_KEYSTORE_PASS, password); Utils.clearChars(password); try { getInstance(); } catch (Exception e) { // Couldn't use the password. editText.setError(activity.getString(R.string.invalid_password)); return; } d.dismiss(); }); deleteButton.setOnClickListener(v -> { AM_KEYSTORE_FILE.delete(); if (sSharedPreferences.contains(PREF_AM_KEYSTORE_PASS)) { sSharedPreferences.edit().remove(PREF_AM_KEYSTORE_PASS).apply(); } dismiss.set(false); generateAndDisplayKeyStorePassword(activity, dismissListener).show(); d.dismiss(); }); }); return alertDialog; } public static boolean hasKeyStore() { return AM_KEYSTORE_FILE.exists(); } public static boolean hasKeyStorePassword() { try { reloadKeyStore(); return true; } catch (Exception e) { return false; } } @Deprecated // To be removed in v3.0.0 @WorkerThread public static void migrateKeyStore() throws Exception { // Reset all alias password String[] aliases = new String[]{ AdbConnectionManager.ADB_KEY_ALIAS, Signer.SIGNING_KEY_ALIAS, RSACrypto.AES_KEY_ALIAS, AESCrypto.AES_KEY_ALIAS, }; KeyStoreManager ksm = KeyStoreManager.getInstance(); Key key; for (String alias : aliases) { try { if (!ksm.containsKey(alias)) continue; key = ksm.getKey(alias, null); ksm.removeItem(alias); if (key instanceof SecretKey) { ksm.addSecretKey(alias, (SecretKey) key, false); SecretKeyCompat.destroy((SecretKey) key); } else if (key instanceof PrivateKey) { KeyPair keyPair = new KeyPair((PrivateKey) key, ksm.getCertificate(alias)); ksm.addKeyPair(alias, keyPair, false); keyPair.destroy(); } else throw new NoSuchAlgorithmException(); } catch (Exception ignore) { } } } private final Context mContext; private final KeyStore mAmKeyStore; private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, @NonNull Intent intent) { if (intent.getAction() == null) return; switch (intent.getAction()) { case ACTION_KS_INTERACTION_BEGIN: break; case ACTION_KS_INTERACTION_END: releaseLock(); break; } } }; private KeyStoreManager() throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException { mContext = ContextUtils.getContext(); mAmKeyStore = getAmKeyStore(); } public void addKeyPair(String alias, @NonNull KeyPair keyPair, boolean isOverride) throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException { // Check existence of this alias in system preferences, this should be unique String prefAlias = getPrefAlias(alias); if (sSharedPreferences.contains(prefAlias) && mAmKeyStore.containsAlias(alias)) { Log.w(TAG, "Alias %s exists.", alias); if (isOverride) removeItemInternal(alias); else return; } char[] password = getAmKeyStorePassword(); PrivateKey privateKey = keyPair.getPrivateKey(); Certificate certificate = keyPair.getCertificate(); mAmKeyStore.setKeyEntry(alias, privateKey, password, new Certificate[]{certificate}); String encryptedPass = getEncryptedPassword(mContext, password); if (encryptedPass == null) { mAmKeyStore.deleteEntry(alias); throw new KeyStoreException("Password for " + alias + " could not be saved."); } sSharedPreferences.edit().putString(prefAlias, encryptedPass).apply(); try (OutputStream is = new FileOutputStream(AM_KEYSTORE_FILE)) { mAmKeyStore.store(is, password); Utils.clearChars(password); Utils.clearChars(password); } } public void addSecretKey(String alias, @NonNull SecretKey secretKey, boolean isOverride) throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException { // Check existence of this alias in system preferences, this should be unique String prefAlias = getPrefAlias(alias); if (sSharedPreferences.contains(prefAlias) && mAmKeyStore.containsAlias(alias)) { if (!isOverride) throw new KeyStoreException("Alias " + alias + " exists."); else Log.w(TAG, "Alias %s exists.", alias); } char[] password = getAmKeyStorePassword(); mAmKeyStore.setEntry(alias, new KeyStore.SecretKeyEntry(secretKey), new KeyStore.PasswordProtection(password)); String encryptedPass = getEncryptedPassword(mContext, password); if (encryptedPass == null) { mAmKeyStore.deleteEntry(alias); throw new KeyStoreException("Password for " + alias + " could not be saved."); } sSharedPreferences.edit().putString(prefAlias, encryptedPass).apply(); try (OutputStream is = new FileOutputStream(AM_KEYSTORE_FILE)) { mAmKeyStore.store(is, password); } finally { Utils.clearChars(password); Utils.clearChars(password); } } public void removeItem(String alias) throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException { removeItemInternal(alias); char[] realPassword = getAmKeyStorePassword(); try (OutputStream is = new FileOutputStream(AM_KEYSTORE_FILE)) { mAmKeyStore.store(is, realPassword); } finally { Utils.clearChars(realPassword); } } private void removeItemInternal(String alias) throws KeyStoreException { mAmKeyStore.deleteEntry(alias); String prefAlias = getPrefAlias(alias); if (sSharedPreferences.contains(prefAlias)) { sSharedPreferences.edit().remove(prefAlias).apply(); } } @Nullable private Key getKey(String alias) throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException { char[] password = getAmKeyStorePassword(); Key key = mAmKeyStore.getKey(alias, password); Utils.clearChars(password); return key; } /** * @deprecated Kept for migratory purposes only, deprecated since v2.6.3. To be removed in v3.0.0. */ @Deprecated @Nullable private Key getKey(String alias, @Nullable char[] password) throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException { if (password == null) { password = getAliasPassword(alias); } Key key = mAmKeyStore.getKey(alias, password); Utils.clearChars(password); return key; } @Nullable public SecretKey getSecretKey(String alias) throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException { Key key = getKey(alias); if (key instanceof SecretKey) { return (SecretKey) key; } throw new KeyStoreException("The alias " + alias + " does not have a KeyPair."); } @Nullable public KeyPair getKeyPair(String alias) throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException { Key key = getKey(alias); if (key instanceof PrivateKey) { return new KeyPair((PrivateKey) key, getCertificate(alias)); } throw new KeyStoreException("The alias " + alias + " does not have a KeyPair."); } @Nullable public KeyPair getKeyPairNoThrow(String alias) { try { return getKeyPair(alias); } catch (Exception e) { return null; } } public boolean containsKey(String alias) throws KeyStoreException { return mAmKeyStore.containsAlias(alias); } /** * Get the certificate associated with the alias * * @param alias The given KeyStore alias * @return Certificate associated with the alias, usually {@link X509Certificate} */ private Certificate getCertificate(String alias) throws KeyStoreException { return mAmKeyStore.getCertificate(alias); } /** * Save password in the Shared Preferences in encrypted form. * * @param prefAlias The alias after running {@link #getPrefAlias(String)} * @param password The password for the alias. {@link Utils#clearChars(char[])} must be called when done. */ static void savePass(@NonNull Context context, String prefAlias, char[] password) { sSharedPreferences.edit().putString(prefAlias, getEncryptedPassword(context, password)).apply(); } /** * Get the password decrypted by Android KeyStore. * * @param encryptedPass Encrypted password (IV length + IV + password) in base 64 format * @return The password in decrypted form. {@link Utils#clearChars(char[])} must be called when done. */ @CheckResult @Nullable private static char[] getDecryptedPassword(@NonNull Context context, @NonNull String encryptedPass) { try { byte[] encryptedBytes = Base64.decode(encryptedPass, Base64.NO_WRAP); return Utils.bytesToChars(CompatUtil.decryptData(context, encryptedBytes)); } catch (Exception e) { Log.e("KS", "Could not get decrypted password for %s", e, encryptedPass); } return null; } /** * Get the password to be encrypted using Android KeyStore. * * @param realPass The password to be encrypted. {@link Utils#clearChars(char[])} must be called when done. * @return Encrypted password (IV length + IV + password) in base 64 format */ @Nullable private static String getEncryptedPassword(@NonNull Context context, @NonNull char[] realPass) { try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) { AesEncryptedData encryptedData = CompatUtil.getEncryptedData(Utils.charsToBytes(realPass), context); bos.write((byte) encryptedData.getIv().length); bos.write(encryptedData.getIv()); bos.write(encryptedData.getEncryptedData()); return Base64.encodeToString(bos.toByteArray(), Base64.NO_WRAP); } catch (Exception e) { Log.e("KS", "Could not get encrypted password", e); } return null; } /** * Get App Manager's KeyStore. The user will be asked for a password if the KeyStore password * does not exist. If the KeyStore itself doesn't exist, it will initialize an empty KeyStore. * * @return App Manager's KeyStore */ private KeyStore getAmKeyStore() throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException { KeyStore keyStore = KeyStore.getInstance(AM_KEYSTORE); Log.w(TAG, "Using keystore %s", AM_KEYSTORE); char[] realPassword = getAmKeyStorePassword(); try { if (AM_KEYSTORE_FILE.exists()) { try (InputStream is = new FileInputStream(AM_KEYSTORE_FILE)) { keyStore.load(is, realPassword); } } else { keyStore.load(null); } } finally { Utils.clearChars(realPassword); } return keyStore; } /** * Get App Manager's KeyStore password. The password is stored in the shared preferences in an * encrypted format (the encryption/decryption is performed via AndroidKeyStore). In case the * user restores from the cache or accidentally deletes all entries from the shared pref, App * Manager will ask for KeyStore password again. * * @return KeyStore password in decrypted format. {@link Utils#clearChars(char[])} must be called when done. */ @CheckResult @NonNull public char[] getAmKeyStorePassword() throws KeyStoreException { String encryptedPass = sSharedPreferences.getString(PREF_AM_KEYSTORE_PASS, null); if (encryptedPass == null) { throw new KeyStoreException("No saved password for KeyStore."); } char[] realPassword = getDecryptedPassword(mContext, encryptedPass); if (realPassword == null) { throw new KeyStoreException("Could not decrypt encrypted password."); } return realPassword; } /** * @return Password for the given alias. {@link Utils#clearChars(char[])} must be called when done. * @deprecated Kept for migratory purposes only, deprecated since v2.6.3. To be removed in v3.0.0. */ @Deprecated @CheckResult @NonNull private char[] getAliasPassword(@NonNull String alias) throws KeyStoreException { char[] password; String prefAlias = getPrefAlias(alias); if (sSharedPreferences.contains(prefAlias)) { String encryptedPass = sSharedPreferences.getString(prefAlias, null); if (encryptedPass == null) { throw new KeyStoreException("Stored pass is empty for alias " + alias); } password = getDecryptedPassword(mContext, encryptedPass); if (password == null) { throw new KeyStoreException("Decrypted pass is empty for alias " + alias); } return password; } else { IntentFilter filter = new IntentFilter(ACTION_KS_INTERACTION_BEGIN); filter.addAction(ACTION_KS_INTERACTION_END); ContextCompat.registerReceiver(mContext, mReceiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED); Intent broadcastIntent = new Intent(ACTION_KS_INTERACTION_BEGIN); broadcastIntent.setPackage(mContext.getPackageName()); mContext.sendBroadcast(broadcastIntent); // Intent wrapper Intent intent = new Intent(mContext, KeyStoreActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra(KeyStoreActivity.EXTRA_ALIAS, alias); String ks = "AM KeyStore"; // We don't need a delete intent since the time will be expired anyway NotificationCompat.Builder builder = NotificationUtils.getHighPriorityNotificationBuilder(mContext) .setAutoCancel(true) .setDefaults(Notification.DEFAULT_ALL) .setWhen(System.currentTimeMillis()) .setSmallIcon(R.drawable.ic_default_notification) .setTicker(ks) .setContentTitle(ks) .setSubText(ks) .setContentText(mContext.getString(R.string.input_keystore_alias_pass_msg, alias)); builder.setContentIntent(PendingIntentCompat.getActivity(mContext, 0, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT, false)); NotificationUtils.displayHighPriorityNotification(mContext, builder.build()); acquireLock(); mContext.unregisterReceiver(mReceiver); return getAliasPassword(alias); } } /** * Get the formatted alias stored in the shared pref. Normally, a prefix {@link #PREF_AM_KEYSTORE_PREFIX} * is added to the alias. * * @param alias The given alias * @return Alias with {@link #PREF_AM_KEYSTORE_PREFIX} */ @NonNull static String getPrefAlias(@NonNull String alias) { return PREF_AM_KEYSTORE_PREFIX + alias; } private CountDownLatch mInteractionWatcher; private void releaseLock() { if (mInteractionWatcher != null) mInteractionWatcher.countDown(); } private void acquireLock() { mInteractionWatcher = new CountDownLatch(1); try { mInteractionWatcher.await(100, TimeUnit.SECONDS); } catch (InterruptedException e) { Log.e(TAG, "waitForResult: interrupted", e); Thread.currentThread().interrupt(); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/crypto/ks/KeyStoreUtils.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.crypto.ks; import android.content.ContentResolver; import android.content.Context; import android.net.Uri; import android.sun.misc.BASE64Decoder; import android.sun.misc.BASE64Encoder; import android.sun.security.provider.JavaKeyStoreProvider; import android.sun.security.provider.X509Factory; import android.sun.security.x509.AlgorithmId; import android.sun.security.x509.CertificateAlgorithmId; import android.sun.security.x509.CertificateExtensions; import android.sun.security.x509.CertificateIssuerName; import android.sun.security.x509.CertificateSerialNumber; import android.sun.security.x509.CertificateSubjectName; import android.sun.security.x509.CertificateValidity; import android.sun.security.x509.CertificateVersion; import android.sun.security.x509.CertificateX509Key; import android.sun.security.x509.KeyIdentifier; import android.sun.security.x509.PrivateKeyUsageExtension; import android.sun.security.x509.SubjectKeyIdentifierExtension; import android.sun.security.x509.X500Name; import android.sun.security.x509.X509CertImpl; import android.sun.security.x509.X509CertInfo; import android.text.TextUtils; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; import org.bouncycastle.asn1.x9.X9ECParameters; import org.bouncycastle.cert.X509v3CertificateBuilder; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; import org.bouncycastle.crypto.ec.CustomNamedCurves; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.spec.ECParameterSpec; import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.security.InvalidKeyException; import java.security.Key; import java.security.KeyException; import java.security.KeyFactory; import java.security.KeyPairGenerator; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.Provider; import java.security.PublicKey; import java.security.SecureRandom; import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.security.spec.DSAPrivateKeySpec; import java.security.spec.KeySpec; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.RSAPrivateCrtKeySpec; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.List; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.io.IoUtils; public class KeyStoreUtils { public static final String TAG = KeyStoreUtils.class.getSimpleName(); /** * Must be kept in sync with {@link #TYPES}. */ @IntDef({KeyType.JKS, KeyType.PKCS12, KeyType.BKS, KeyType.PK8}) public @interface KeyType { int JKS = 0; int PKCS12 = 1; int BKS = 2; int PK8 = 3; } public static final String KEY_STORE_TYPE_JKS = "JKS"; public static final String KEY_STORE_TYPE_PKCS12 = "PKCS12"; public static final String KEY_STORE_TYPE_BKS = "BKS"; private static final String[] TYPES = {KEY_STORE_TYPE_JKS, KEY_STORE_TYPE_PKCS12, KEY_STORE_TYPE_BKS}; @NonNull public static ArrayList listAliases(@NonNull Context context, @NonNull Uri ksUri, @KeyType int ksType, @Nullable char[] ksPass) throws IOException, GeneralSecurityException { String keyType = TYPES[ksType]; Log.d(TAG, "Loading keystore %s", keyType); final KeyStore ks = KeyStore.getInstance(keyType, getKeyStoreProvider(keyType)); try (InputStream is = context.getContentResolver().openInputStream(ksUri)) { if (is == null) throw new FileNotFoundException(ksUri + " does not exist."); ks.load(is, ksPass); } return Collections.list(ks.aliases()); } @NonNull public static KeyPair getKeyPair(@NonNull Context context, @NonNull Uri ksUri, @KeyType int ksType, @Nullable String ksAlias, @Nullable char[] ksPass, @Nullable char[] aliasPass) throws GeneralSecurityException, IOException { String keyType = TYPES[ksType]; Log.d(TAG, "Loading keystore %s", keyType); final KeyStore ks = KeyStore.getInstance(keyType, getKeyStoreProvider(keyType)); try (InputStream is = context.getContentResolver().openInputStream(ksUri)) { if (is == null) throw new FileNotFoundException(ksUri + " does not exist."); ks.load(is, ksPass); } if (TextUtils.isEmpty(ksAlias)) { ksAlias = ks.aliases().nextElement(); } Key key = ks.getKey(ksAlias, aliasPass); if (key instanceof PrivateKey) { X509Certificate cert = (X509Certificate) ks.getCertificate(ksAlias); return new KeyPair((PrivateKey) key, cert); } throw new KeyStoreException("The provided alias " + ksAlias + " does not exist."); } @NonNull public static KeyPair getKeyPair(@NonNull Context context, @NonNull Uri keyPath, @NonNull Uri certPath) throws GeneralSecurityException, IOException { ContentResolver cr = context.getContentResolver(); PKCS8EncodedKeySpec spec; PrivateKey privateKey; X509Certificate cert; try (InputStream pk = cr.openInputStream(keyPath)) { byte[] data = IoUtils.readFully(pk, -1, true); spec = new PKCS8EncodedKeySpec(data); } try (InputStream cer = cr.openInputStream(certPath)) { cert = (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(cer); // TODO: 22/5/21 Check algorithm type: We only support RSA and EC privateKey = KeyFactory.getInstance(cert.getPublicKey().getAlgorithm()).generatePrivate(spec); } return new KeyPair(privateKey, cert); } @NonNull public static KeyPair generateRSAKeyPair(@NonNull String formattedSubject, int keySize, long expiryDate) throws GeneralSecurityException, IOException { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); keyPairGenerator.initialize(keySize, SecureRandom.getInstance("SHA1PRNG")); java.security.KeyPair generateKeyPair = keyPairGenerator.generateKeyPair(); PublicKey publicKey = generateKeyPair.getPublic(); PrivateKey privateKey = generateKeyPair.getPrivate(); return new KeyPair(privateKey, generateRSACert(privateKey, publicKey, formattedSubject, expiryDate)); } @NonNull private static Provider getKeyStoreProvider(String keyStoreType) { switch (keyStoreType) { default: case KEY_STORE_TYPE_JKS: return new JavaKeyStoreProvider(); case KEY_STORE_TYPE_PKCS12: case KEY_STORE_TYPE_BKS: return new BouncyCastleProvider(); } } @NonNull public static KeyPair generateECCKeyPair(@NonNull String formattedSubject, long expiryDate) throws GeneralSecurityException, OperatorCreationException { X9ECParameters curve25519 = CustomNamedCurves.getByName("curve25519"); ECParameterSpec parameterSpec = new ECParameterSpec(curve25519.getCurve(), curve25519.getG(), curve25519.getN(), curve25519.getH()); KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("ECDH", new BouncyCastleProvider()); keyPairGenerator.initialize(parameterSpec, SecureRandom.getInstance("SHA1PRNG")); java.security.KeyPair generateKeyPair = keyPairGenerator.generateKeyPair(); PublicKey publicKey = generateKeyPair.getPublic(); PrivateKey privateKey = generateKeyPair.getPrivate(); return new KeyPair(privateKey, generateECDSACert(privateKey, publicKey, formattedSubject, expiryDate)); } /** * Read a PKCS #8, Base64-encrypted file as a Key instance. This is similar to * {@link CertificateFactory#generateCertificate(InputStream)} except that it generates a private key. */ public static PrivateKey generatePrivateKey(InputStream inputStream) throws IOException, GeneralSecurityException { try (BufferedReader in = new BufferedReader(new InputStreamReader(inputStream))) { String line; boolean readingKey = false; boolean pkcs8Format = false; boolean rsaFormat = false; boolean dsaFormat = false; // FIXME: Read securely using char[] StringBuilder base64EncodedKey = new StringBuilder(); while ((line = in.readLine()) != null) { line = line.trim(); if (readingKey) { switch (line) { case RSA_END_HEADER: case DSA_END_HEADER: case PKCS8_END_HEADER: readingKey = false; break; default: base64EncodedKey.append(line); break; } } else { switch (line) { case RSA_BEGIN_HEADER: readingKey = true; rsaFormat = true; break; case DSA_BEGIN_HEADER: readingKey = true; dsaFormat = true; break; case PKCS8_BEGIN_HEADER: readingKey = true; pkcs8Format = true; break; } } } if (base64EncodedKey.length() == 0) { throw new IOException("Stream does not contain an unencrypted private key."); } BASE64Decoder decoder = new BASE64Decoder(); byte[] bytes = decoder.decodeBuffer(base64EncodedKey.toString()); KeyFactory kf; KeySpec spec; if (pkcs8Format) { kf = KeyFactory.getInstance("RSA"); spec = new PKCS8EncodedKeySpec(bytes); } else if (rsaFormat) { // PKCS#1 format kf = KeyFactory.getInstance("RSA"); List rsaIntegers = new ArrayList<>(); ASN1Parse(bytes, rsaIntegers); if (rsaIntegers.size() < 8) { throw new InvalidKeyException("Stream does not appear to be a properly formatted RSA key."); } BigInteger publicExponent = rsaIntegers.get(2); BigInteger privateExponent = rsaIntegers.get(3); BigInteger modulus = rsaIntegers.get(1); BigInteger primeP = rsaIntegers.get(4); BigInteger primeQ = rsaIntegers.get(5); BigInteger primeExponentP = rsaIntegers.get(6); BigInteger primeExponentQ = rsaIntegers.get(7); BigInteger crtCoefficient = rsaIntegers.get(8); //spec = new RSAPrivateKeySpec(modulus, privateExponent); spec = new RSAPrivateCrtKeySpec(modulus, publicExponent, privateExponent, primeP, primeQ, primeExponentP, primeExponentQ, crtCoefficient); } else if (dsaFormat) { kf = KeyFactory.getInstance("DSA"); List dsaIntegers = new ArrayList<>(); ASN1Parse(bytes, dsaIntegers); if (dsaIntegers.size() < 5) { throw new InvalidKeyException("Stream does not appear to be a properly formatted DSA key"); } BigInteger privateExponent = dsaIntegers.get(1); BigInteger publicExponent = dsaIntegers.get(2); BigInteger P = dsaIntegers.get(3); BigInteger Q = dsaIntegers.get(4); BigInteger G = dsaIntegers.get(5); spec = new DSAPrivateKeySpec(privateExponent, P, Q, G); } else { throw new NoSuchAlgorithmException("Couldn't find any suitable algorithm"); } return kf.generatePrivate(spec); } } public static byte[] getPemCertificate(@NonNull Certificate certificate) throws CertificateEncodingException, IOException { BASE64Encoder encoder = new BASE64Encoder(); try (ByteArrayOutputStream os = new ByteArrayOutputStream(X509Factory.BEGIN_CERT.length() + X509Factory.BEGIN_CERT.length() + certificate.getEncoded().length + 2)) { os.write(X509Factory.BEGIN_CERT.getBytes(StandardCharsets.UTF_8)); os.write('\n'); encoder.encode(certificate.getEncoded(), os); os.write('\n'); os.write(X509Factory.END_CERT.getBytes(StandardCharsets.UTF_8)); return os.toByteArray(); } } private static final String RSA_BEGIN_HEADER = "-----BEGIN RSA PRIVATE KEY-----"; private static final String RSA_END_HEADER = "-----END RSA PRIVATE KEY-----"; private static final String PKCS8_BEGIN_HEADER = "-----BEGIN PRIVATE KEY-----"; private static final String PKCS8_END_HEADER = "-----END PRIVATE KEY-----"; private static final String DSA_BEGIN_HEADER = "-----BEGIN DSA PRIVATE KEY-----"; private static final String DSA_END_HEADER = "-----END DSA PRIVATE KEY-----"; /** * Bare-bones ASN.1 parser that can only deal with a structure that contains integers * (as I expect for the RSA private key format given in PKCS #1 and RFC 3447). * * @param b the bytes to be parsed as ASN.1 DER * @param integers an output array to which all integers encountered during the parse * will be appended in the order they're encountered. It's up to the caller to determine * which is which. */ private static void ASN1Parse(@NonNull byte[] b, List integers) throws KeyException { int pos = 0; while (pos < b.length) { byte tag = b[pos++]; int length = b[pos++]; if ((length & 0x80) != 0) { int extLen = 0; for (int i = 0; i < (length & 0x7F); i++) { extLen = (extLen << 8) | (b[pos++] & 0xFF); } length = extLen; } byte[] contents = new byte[length]; System.arraycopy(b, pos, contents, 0, length); pos += length; if (tag == 0x30) { // sequence ASN1Parse(contents, integers); } else if (tag == 0x02) { // Integer BigInteger i = new BigInteger(contents); integers.add(i); } else { throw new KeyException("Unsupported ASN.1 tag " + tag + " encountered. Is this a " + "valid RSA key?"); } } } private static X509Certificate generateECDSACert(@NonNull PrivateKey privateKey, @NonNull PublicKey publicKey, @NonNull String formattedSubject, long expiryDate) throws OperatorCreationException, CertificateException { Date notBefore = new Date(); Date notAfter = new Date(expiryDate); org.bouncycastle.asn1.x500.X500Name x500Name = new org.bouncycastle.asn1.x500.X500Name(formattedSubject); JcaContentSignerBuilder signerBuilder = new JcaContentSignerBuilder("SHA512withECDSA"); signerBuilder.setProvider(new BouncyCastleProvider()); SubjectPublicKeyInfo spki = SubjectPublicKeyInfo.getInstance(publicKey.getEncoded()); X509v3CertificateBuilder v3CertGen = new X509v3CertificateBuilder(x500Name, BigInteger.valueOf(new SecureRandom().nextInt() & Integer.MAX_VALUE), notBefore, notAfter, x500Name, spki); return new JcaX509CertificateConverter().getCertificate(v3CertGen.build(signerBuilder.build(privateKey))); } @NonNull private static X509Certificate generateRSACert(@NonNull PrivateKey privateKey, @NonNull PublicKey publicKey, @NonNull String formattedSubject, long expiryDate) throws GeneralSecurityException, IOException { String algorithmName = "SHA512withRSA"; CertificateExtensions certificateExtensions = new CertificateExtensions(); certificateExtensions.set(SubjectKeyIdentifierExtension.NAME, new SubjectKeyIdentifierExtension( new KeyIdentifier(publicKey).getIdentifier())); X500Name x500Name = new X500Name(formattedSubject); Date notBefore = new Date(); Date notAfter = new Date(expiryDate); certificateExtensions.set(PrivateKeyUsageExtension.NAME, new PrivateKeyUsageExtension(notBefore, notAfter)); CertificateValidity certificateValidity = new CertificateValidity(notBefore, notAfter); X509CertInfo x509CertInfo = new X509CertInfo(); x509CertInfo.set(X509CertInfo.VERSION, new CertificateVersion(CertificateVersion.V3)); x509CertInfo.set(X509CertInfo.SERIAL_NUMBER, new CertificateSerialNumber(new SecureRandom().nextInt() & Integer.MAX_VALUE)); x509CertInfo.set(X509CertInfo.ALGORITHM_ID, new CertificateAlgorithmId(AlgorithmId.get(algorithmName))); x509CertInfo.set(X509CertInfo.SUBJECT, new CertificateSubjectName(x500Name)); x509CertInfo.set(X509CertInfo.KEY, new CertificateX509Key(publicKey)); x509CertInfo.set(X509CertInfo.VALIDITY, certificateValidity); x509CertInfo.set(X509CertInfo.ISSUER, new CertificateIssuerName(x500Name)); x509CertInfo.set(X509CertInfo.EXTENSIONS, certificateExtensions); X509CertImpl x509CertImpl = new X509CertImpl(x509CertInfo); x509CertImpl.sign(privateKey, algorithmName); return x509CertImpl; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/crypto/ks/SecretKeyAndVersion.java ================================================ // SPDX-License-Identifier: Apache-2.0 package io.github.muntashirakon.AppManager.crypto.ks; import androidx.annotation.NonNull; import androidx.core.util.Pair; import java.util.Objects; import javax.crypto.SecretKey; /** * Tuple which contains the secret key and the version of Android when the key has been generated */ // Copyright 2018 New Vector Ltd public class SecretKeyAndVersion extends Pair { /** * @param secretKey The key * @param androidVersionWhenTheKeyHasBeenGenerated The android version when the key has been generated */ public SecretKeyAndVersion(@NonNull SecretKey secretKey, int androidVersionWhenTheKeyHasBeenGenerated) { super(Objects.requireNonNull(secretKey), androidVersionWhenTheKeyHasBeenGenerated); } /** * Get the key * * @return The key */ @NonNull public SecretKey getSecretKey() { return first; } /** * Get the android version when the key has been generated * * @return The android version when the key has been generated */ public int getAndroidVersionWhenTheKeyHasBeenGenerated() { return second; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/crypto/ks/SecretKeyCompat.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.crypto.ks; import android.annotation.SuppressLint; import androidx.annotation.NonNull; import java.lang.reflect.Field; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import javax.security.auth.DestroyFailedException; import io.github.muntashirakon.AppManager.utils.Utils; @SuppressLint("SoonBlockedPrivateApi") public final class SecretKeyCompat { static final Field KEY; static { Field key = null; try { //noinspection JavaReflectionMemberAccess key = SecretKeySpec.class.getDeclaredField("key"); key.setAccessible(true); } catch (NoSuchFieldException | SecurityException ignored) { } KEY = key; } public static void destroy(@NonNull SecretKey secretKey) throws DestroyFailedException { // We might want to use the SecretKeySpec#destroy() but it doesn't work either if (KEY != null && secretKey instanceof SecretKeySpec) { try { byte[] key = (byte[]) KEY.get(secretKey); if (key != null) { Utils.clearBytes(key); } KEY.set(secretKey, null); } catch (IllegalAccessException | IllegalArgumentException e) { DestroyFailedException dfe = new DestroyFailedException(e.toString()); dfe.setStackTrace(e.getStackTrace()); throw dfe; } } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/db/AppsDb.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.db; import androidx.annotation.NonNull; import androidx.room.Database; import androidx.room.Room; import androidx.room.RoomDatabase; import androidx.room.migration.Migration; import androidx.sqlite.db.SupportSQLiteDatabase; import io.github.muntashirakon.AppManager.db.dao.AppDao; import io.github.muntashirakon.AppManager.db.dao.BackupDao; import io.github.muntashirakon.AppManager.db.dao.FmFavoriteDao; import io.github.muntashirakon.AppManager.db.dao.FreezeTypeDao; import io.github.muntashirakon.AppManager.db.dao.LogFilterDao; import io.github.muntashirakon.AppManager.db.dao.OpHistoryDao; import io.github.muntashirakon.AppManager.db.entity.App; import io.github.muntashirakon.AppManager.db.entity.Backup; import io.github.muntashirakon.AppManager.db.entity.FmFavorite; import io.github.muntashirakon.AppManager.db.entity.FreezeType; import io.github.muntashirakon.AppManager.db.entity.LogFilter; import io.github.muntashirakon.AppManager.db.entity.OpHistory; import io.github.muntashirakon.AppManager.utils.ContextUtils; @Database(entities = {App.class, LogFilter.class, Backup.class, OpHistory.class, FmFavorite.class, FreezeType.class}, version = 7) public abstract class AppsDb extends RoomDatabase { private static AppsDb sAppsDb; public static final Migration M_2_3 = new Migration(2, 3) { @Override public void migrate(@NonNull SupportSQLiteDatabase db) { db.execSQL("CREATE TABLE IF NOT EXISTS `op_history` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` TEXT NOT NULL, `time` INTEGER NOT NULL, `data` TEXT NOT NULL, `status` TEXT NOT NULL, `extra` TEXT)"); } }; public static final Migration M_3_4 = new Migration(3, 4) { @Override public void migrate(@NonNull SupportSQLiteDatabase db) { db.execSQL("CREATE TABLE IF NOT EXISTS `fm_favorite` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `uri` TEXT NOT NULL, `init_uri` TEXT, `options` INTEGER NOT NULL, `order` INTEGER NOT NULL, `type` INTEGER NOT NULL)"); } }; public static final Migration M_4_5 = new Migration(4, 5) { @Override public void migrate(@NonNull SupportSQLiteDatabase db) { db.execSQL("CREATE TABLE IF NOT EXISTS `freeze_type` (`package_name` TEXT NOT NULL, `type` INTEGER NOT NULL, PRIMARY KEY(`package_name`))"); } }; public static final Migration M_5_6 = new Migration(5, 6) { @Override public void migrate(@NonNull SupportSQLiteDatabase db) { db.execSQL("ALTER TABLE `app` ADD COLUMN `is_only_data_installed` INTEGER NOT NULL DEFAULT 0"); } }; public static final Migration M_6_7 = new Migration(6, 7) { @Override public void migrate(@NonNull SupportSQLiteDatabase db) { db.execSQL("DROP TABLE IF EXISTS `file_hash`"); } }; public static AppsDb getInstance() { if (sAppsDb == null) { sAppsDb = Room.databaseBuilder(ContextUtils.getContext(), AppsDb.class, "apps.db") .addMigrations(M_2_3, M_3_4, M_4_5, M_5_6, M_6_7) .fallbackToDestructiveMigrationOnDowngrade() .build(); try { sAppsDb.appDao().getAll(); } catch (Throwable th) { th.printStackTrace(); } } return sAppsDb; } public abstract AppDao appDao(); public abstract BackupDao backupDao(); public abstract LogFilterDao logFilterDao(); public abstract OpHistoryDao opHistoryDao(); public abstract FmFavoriteDao fmFavoriteDao(); public abstract FreezeTypeDao freezeTypeDao(); } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/db/dao/AppDao.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.db.dao; import androidx.room.Dao; import androidx.room.Delete; import androidx.room.Insert; import androidx.room.OnConflictStrategy; import androidx.room.Query; import androidx.room.Update; import java.util.List; import io.github.muntashirakon.AppManager.db.entity.App; @Dao public interface AppDao { @Query("SELECT * FROM app") List getAll(); @Query("SELECT * FROM app WHERE is_installed = 1") List getAllInstalled(); @Query("SELECT * FROM app WHERE package_name = :packageName") List getAll(String packageName); @Query("SELECT * FROM app WHERE package_name = :packageName AND user_id = :userId") List getAll(String packageName, int userId); @Insert(onConflict = OnConflictStrategy.REPLACE) void insert(List apps); @Insert(onConflict = OnConflictStrategy.REPLACE) void insert(App app); @Update void update(App app); @Query("DELETE FROM app WHERE 1") void deleteAll(); @Delete void delete(App app); @Delete void delete(List apps); @Query("DELETE FROM app WHERE package_name = :packageName AND user_id = :userId") void delete(String packageName, int userId); } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/db/dao/BackupDao.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.db.dao; import androidx.room.Dao; import androidx.room.Delete; import androidx.room.Insert; import androidx.room.OnConflictStrategy; import androidx.room.Query; import androidx.room.Update; import java.util.List; import io.github.muntashirakon.AppManager.db.entity.Backup; @Dao public interface BackupDao { @Query("SELECT * FROM backup") List getAll(); @Query("SELECT * FROM backup WHERE package_name = :packageName") List get(String packageName); @Insert(onConflict = OnConflictStrategy.REPLACE) void insert(List backups); @Insert(onConflict = OnConflictStrategy.REPLACE) void insert(Backup backup); @Update void update(Backup backup); @Delete void delete(Backup backup); @Delete void delete(List backups); @Query("DELETE FROM backup WHERE package_name = :packageName AND backup_name = :backupName") void delete(String packageName, String backupName); @Query("DELETE FROM backup WHERE 1") void deleteAll(); } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/db/dao/FmFavoriteDao.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.db.dao; import androidx.annotation.NonNull; import androidx.room.Dao; import androidx.room.Insert; import androidx.room.OnConflictStrategy; import androidx.room.Query; import java.util.List; import io.github.muntashirakon.AppManager.db.entity.FmFavorite; @Dao public interface FmFavoriteDao { @Query("SELECT * FROM fm_favorite") List getAll(); @Insert(onConflict = OnConflictStrategy.REPLACE) long insert(@NonNull FmFavorite fmFavorite); @Query("UPDATE fm_favorite SET name = :newName WHERE id = :id") void rename(long id, @NonNull String newName); @Query("DELETE FROM fm_favorite WHERE id = :id") void delete(long id); } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/db/dao/FreezeTypeDao.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.db.dao; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.room.Dao; import androidx.room.Delete; import androidx.room.Insert; import androidx.room.OnConflictStrategy; import androidx.room.Query; import io.github.muntashirakon.AppManager.db.entity.FreezeType; import io.github.muntashirakon.AppManager.utils.FreezeUtils; @Dao public interface FreezeTypeDao { @Nullable @Query("SELECT * FROM freeze_type WHERE package_name = :packageName LIMIT 1") FreezeType get(String packageName); @Insert(onConflict = OnConflictStrategy.REPLACE) void insert(FreezeType freezeType); @Query("DELETE FROM freeze_type WHERE package_name = :packageName") void delete(String packageName); } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/db/dao/LogFilterDao.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.db.dao; import androidx.room.Dao; import androidx.room.Delete; import androidx.room.Insert; import androidx.room.OnConflictStrategy; import androidx.room.Query; import java.util.List; import io.github.muntashirakon.AppManager.db.entity.LogFilter; @Dao public interface LogFilterDao { @Query("SELECT * FROM log_filter") List getAll(); @Query("SELECT * FROM log_filter WHERE id = :id LIMIT 1") LogFilter get(long id); @Query("INSERT INTO log_filter (name) VALUES(:filterName)") long insert(String filterName); @Insert(onConflict = OnConflictStrategy.REPLACE) void insert(LogFilter logFilter); @Delete void delete(LogFilter logFilter); @Query("DELETE FROM log_filter WHERE id = :id") void delete(int id); } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/db/dao/OpHistoryDao.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.db.dao; import androidx.room.Dao; import androidx.room.Insert; import androidx.room.OnConflictStrategy; import androidx.room.Query; import java.util.List; import io.github.muntashirakon.AppManager.db.entity.OpHistory; @Dao public interface OpHistoryDao { @Query("SELECT * FROM op_history") List getAll(); @Insert(onConflict = OnConflictStrategy.REPLACE) long insert(OpHistory opHistory); @Query("DELETE FROM op_history WHERE id = :id") void delete(long id); @Query("DELETE FROM op_history WHERE 1") void deleteAll(); } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/db/entity/App.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.db.entity; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.os.UserHandleHidden; import android.util.Pair; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.pm.PackageInfoCompat; import androidx.room.ColumnInfo; import androidx.room.Entity; import java.io.Serializable; import java.util.Objects; import io.github.muntashirakon.AppManager.compat.ApplicationInfoCompat; import io.github.muntashirakon.AppManager.rules.compontents.ComponentUtils; import io.github.muntashirakon.AppManager.utils.FreezeUtils; import io.github.muntashirakon.AppManager.utils.Utils; @SuppressWarnings("NotNullFieldNotInitialized") @Entity(tableName = "app", primaryKeys = {"package_name", "user_id"}) public class App implements Serializable { @ColumnInfo(name = "package_name") @NonNull public String packageName; @ColumnInfo(name = "user_id", defaultValue = "" + UserHandleHidden.USER_NULL) public int userId; @ColumnInfo(name = "label") public String packageLabel; @ColumnInfo(name = "version_name") public String versionName; @ColumnInfo(name = "version_code") public long versionCode; @ColumnInfo(name = "flags", defaultValue = "0") public int flags; @ColumnInfo(name = "uid", defaultValue = "0") public int uid; @ColumnInfo(name = "shared_uid", defaultValue = "NULL") @Nullable public String sharedUserId; @ColumnInfo(name = "first_install_time", defaultValue = "0") public long firstInstallTime; @ColumnInfo(name = "last_update_time", defaultValue = "0") public long lastUpdateTime; @ColumnInfo(name = "target_sdk", defaultValue = "0") public int sdk; @ColumnInfo(name = "cert_name", defaultValue = "''") public String certName; @ColumnInfo(name = "cert_algo", defaultValue = "''") public String certAlgo; @ColumnInfo(name = "is_installed", defaultValue = "true") public boolean isInstalled; @ColumnInfo(name = "is_only_data_installed", defaultValue = "0") public boolean isOnlyDataInstalled; @ColumnInfo(name = "is_enabled", defaultValue = "false") public boolean isEnabled; @ColumnInfo(name = "has_activities", defaultValue = "false") public boolean hasActivities; @ColumnInfo(name = "has_splits", defaultValue = "false") public boolean hasSplits; @ColumnInfo(name = "has_keystore", defaultValue = "false") public boolean hasKeystore; @ColumnInfo(name = "uses_saf", defaultValue = "false") public boolean usesSaf; @ColumnInfo(name = "ssaid", defaultValue = "") public String ssaid; @ColumnInfo(name = "code_size", defaultValue = "0") public long codeSize; @ColumnInfo(name = "data_size", defaultValue = "0") public long dataSize; @ColumnInfo(name = "mobile_data", defaultValue = "0") public long mobileDataUsage; @ColumnInfo(name = "wifi_data", defaultValue = "0") public long wifiDataUsage; @ColumnInfo(name = "rules_count", defaultValue = "0") public int rulesCount; @ColumnInfo(name = "tracker_count", defaultValue = "0") public int trackerCount; @ColumnInfo(name = "open_count", defaultValue = "0") public int openCount; @ColumnInfo(name = "screen_time", defaultValue = "0") public long screenTime; @ColumnInfo(name = "last_usage_time", defaultValue = "0") public long lastUsageTime; @ColumnInfo(name = "last_action_time", defaultValue = "0") public long lastActionTime; public boolean isSystemApp() { return (flags & ApplicationInfo.FLAG_SYSTEM) != 0; } public boolean isDebuggable() { return (flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0; } @NonNull public static App fromPackageInfo(@NonNull Context context, @NonNull PackageInfo packageInfo) { App app = new App(); ApplicationInfo applicationInfo = packageInfo.applicationInfo; app.packageName = applicationInfo.packageName; app.uid = applicationInfo.uid; app.userId = UserHandleHidden.getUserId(app.uid); app.isInstalled = ApplicationInfoCompat.isInstalled(applicationInfo); app.isOnlyDataInstalled = ApplicationInfoCompat.isOnlyDataInstalled(applicationInfo); app.flags = applicationInfo.flags; app.isEnabled = !FreezeUtils.isFrozen(applicationInfo); app.packageLabel = ApplicationInfoCompat.loadLabelSafe(applicationInfo, context.getPackageManager()).toString(); app.sdk = applicationInfo.targetSdkVersion; app.versionName = packageInfo.versionName; app.versionCode = PackageInfoCompat.getLongVersionCode(packageInfo); app.sharedUserId = packageInfo.sharedUserId; Pair issuerAndAlgoPair = Utils.getIssuerAndAlg(packageInfo); app.certName = issuerAndAlgoPair.first; app.certAlgo = issuerAndAlgoPair.second; app.firstInstallTime = packageInfo.firstInstallTime; app.lastUpdateTime = packageInfo.lastUpdateTime; app.hasActivities = packageInfo.activities != null; app.hasSplits = applicationInfo.splitSourceDirs != null; app.rulesCount = 0; app.trackerCount = ComponentUtils.getTrackerComponentsCountForPackage(packageInfo); app.lastActionTime = System.currentTimeMillis(); return app; } @NonNull public static App fromBackup(@NonNull Backup backup) { App app = new App(); app.packageName = backup.packageName; app.uid = 0; app.userId = backup.userId; app.isInstalled = false; app.isOnlyDataInstalled = false; if (backup.isSystem) { app.flags |= ApplicationInfo.FLAG_SYSTEM; } app.isEnabled = true; app.packageLabel = backup.label; app.sdk = 0; app.versionName = backup.versionName; app.versionCode = backup.versionCode; app.sharedUserId = null; app.certName = ""; app.certAlgo = ""; app.firstInstallTime = backup.backupTime; app.lastUpdateTime = backup.backupTime; app.hasActivities = false; app.hasSplits = backup.hasSplits; app.rulesCount = 0; app.trackerCount = 0; app.lastActionTime = backup.backupTime; app.hasKeystore = backup.hasKeyStore; return app; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof App)) return false; App app = (App) o; return userId == app.userId && packageName.equals(app.packageName); } @Override public int hashCode() { return Objects.hash(packageName, userId); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/db/entity/Backup.java ================================================ //SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.db.entity; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.room.ColumnInfo; import androidx.room.Entity; import java.io.IOException; import java.util.Objects; import io.github.muntashirakon.AppManager.backup.BackupItems; import io.github.muntashirakon.AppManager.backup.BackupFlags; import io.github.muntashirakon.AppManager.backup.BackupUtils; import io.github.muntashirakon.AppManager.backup.CryptoUtils; import io.github.muntashirakon.AppManager.backup.struct.BackupMetadataV2; import io.github.muntashirakon.AppManager.backup.struct.BackupMetadataV5; import io.github.muntashirakon.AppManager.utils.TarUtils; @SuppressWarnings("NotNullFieldNotInitialized") @Entity(tableName = "backup", primaryKeys = {"backup_name", "package_name"}) public class Backup { @ColumnInfo(name = "package_name") @NonNull public String packageName; @ColumnInfo(name = "backup_name") @NonNull public String backupName; @ColumnInfo(name = "label") public String label; @ColumnInfo(name = "version_name") public String versionName; @ColumnInfo(name = "version_code") public long versionCode; @ColumnInfo(name = "is_system") public boolean isSystem; @ColumnInfo(name = "has_splits") public boolean hasSplits; @ColumnInfo(name = "has_rules") public boolean hasRules; @ColumnInfo(name = "backup_time") public long backupTime; @ColumnInfo(name = "crypto") @CryptoUtils.Mode public String crypto; @ColumnInfo(name = "meta_version") public int version; @ColumnInfo(name = "flags") public int flags; @ColumnInfo(name = "user_id") public int userId; @ColumnInfo(name = "tar_type") @TarUtils.TarType public String tarType; @ColumnInfo(name = "has_key_store") public boolean hasKeyStore; @ColumnInfo(name = "installer_app") @Nullable public String installer; @ColumnInfo(name = "info_hash") public String relativeDir; public BackupFlags getFlags() { return new BackupFlags(flags); } @NonNull public BackupItems.BackupItem getItem() throws IOException { String relativeDir; if (TextUtils.isEmpty(this.relativeDir)) { if (version >= 5) { // In backup v5 onwards, relativeDir must be set throw new IOException("relativeDir not set."); } // Relative directory needs to be inferred. relativeDir = BackupUtils.getV4RelativeDir(userId, backupName, packageName); } else relativeDir = this.relativeDir; return BackupItems.findBackupItem(relativeDir); } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Backup)) return false; Backup backup = (Backup) o; return packageName.equals(backup.packageName) && userId == backup.userId && backupName.equals(backup.backupName); } @Override public int hashCode() { return Objects.hash(packageName, userId, backupName); } @NonNull public static Backup fromBackupMetadata(@NonNull BackupMetadataV2 metadata) { Backup backup = new Backup(); backup.packageName = metadata.packageName; backup.backupName = metadata.backupName != null ? metadata.backupName : ""; backup.label = metadata.label; backup.versionName = metadata.versionName; backup.versionCode = metadata.versionCode; backup.isSystem = metadata.isSystem; backup.hasSplits = metadata.isSplitApk; backup.hasRules = metadata.hasRules; backup.backupTime = metadata.backupTime; backup.crypto = metadata.crypto; backup.version = metadata.version; backup.flags = metadata.flags.getFlags(); backup.userId = metadata.userId; backup.tarType = metadata.tarType; backup.hasKeyStore = metadata.keyStore; backup.installer = metadata.installer; backup.relativeDir = metadata.backupItem.getRelativeDir(); return backup; } @NonNull public static Backup fromBackupMetadataV5(@NonNull BackupMetadataV5 metadata) { return fromBackupInfoAndMeta(metadata.info, metadata.metadata); } @NonNull public static Backup fromBackupInfoAndMeta(@NonNull BackupMetadataV5.Info info, @NonNull BackupMetadataV5.Metadata metadata) { Backup backup = new Backup(); backup.packageName = metadata.packageName; backup.backupName = metadata.backupName != null ? metadata.backupName : ""; backup.label = metadata.label; backup.versionName = metadata.versionName; backup.versionCode = metadata.versionCode; backup.isSystem = metadata.isSystem; backup.hasSplits = metadata.isSplitApk; backup.hasRules = metadata.hasRules; backup.backupTime = info.backupTime; backup.crypto = info.crypto; backup.version = metadata.version; backup.flags = info.flags.getFlags(); backup.userId = info.userId; backup.tarType = info.tarType; backup.hasKeyStore = metadata.keyStore; backup.installer = metadata.installer; backup.relativeDir = info.getRelativeDir(); return backup; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/db/entity/FmFavorite.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.db.entity; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.PrimaryKey; @Entity(tableName = "fm_favorite") public class FmFavorite { @ColumnInfo(name = "id") @PrimaryKey(autoGenerate = true) public long id; @ColumnInfo(name = "name") @NonNull public String name; @ColumnInfo(name = "uri") @NonNull public String uri; @ColumnInfo(name = "init_uri") @Nullable public String initUri; @ColumnInfo(name = "options") public int options; @ColumnInfo(name = "order") public long order; @ColumnInfo(name = "type") public int type; } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/db/entity/FreezeType.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.db.entity; import androidx.annotation.NonNull; import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.PrimaryKey; import io.github.muntashirakon.AppManager.utils.FreezeUtils; @Entity(tableName = "freeze_type") public class FreezeType { @PrimaryKey @ColumnInfo(name = "package_name") @NonNull public String packageName; @ColumnInfo(name = "type") @FreezeUtils.FreezeMethod public int type; public FreezeType() {} public FreezeType(@NonNull String packageName, @FreezeUtils.FreezeMethod int type) { this.packageName = packageName; this.type = type; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/db/entity/LogFilter.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.db.entity; import androidx.annotation.NonNull; import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.Index; import androidx.room.PrimaryKey; import java.util.Comparator; import io.github.muntashirakon.AppManager.utils.AlphanumComparator; @Entity(tableName = "log_filter", indices = {@Index(name = "index_name", value = {"name"}, unique = true)}) public class LogFilter implements Comparable { public static final Comparator COMPARATOR = (o1, o2) -> AlphanumComparator.compareStringIgnoreCase(o1.name, o2.name); @ColumnInfo(name = "id") @PrimaryKey(autoGenerate = true) public long id; @ColumnInfo(name = "name") public String name; @Override public int compareTo(@NonNull LogFilter o) { return COMPARATOR.compare(this, o); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/db/entity/OpHistory.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.db.entity; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.PrimaryKey; @Entity(tableName = "op_history") public class OpHistory { @ColumnInfo(name = "id") @PrimaryKey(autoGenerate = true) public long id; @ColumnInfo(name = "type") @NonNull public String type; @ColumnInfo(name = "time") public long execTime; @ColumnInfo(name = "data") @NonNull public String serializedData; @ColumnInfo(name = "status") @NonNull public String status; @ColumnInfo(name = "extra") @Nullable public String serializedExtra; } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/db/utils/AppDb.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.db.utils; import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.GET_SIGNING_CERTIFICATES; import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.MATCH_DISABLED_COMPONENTS; import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES; import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.MATCH_UNINSTALLED_PACKAGES; import android.annotation.UserIdInt; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Build; import android.os.RemoteException; import android.os.UserHandleHidden; import android.text.TextUtils; import android.util.ArrayMap; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import java.io.IOException; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Set; import io.github.muntashirakon.AppManager.backup.BackupUtils; import io.github.muntashirakon.AppManager.compat.PackageManagerCompat; import io.github.muntashirakon.AppManager.db.AppsDb; import io.github.muntashirakon.AppManager.db.dao.AppDao; import io.github.muntashirakon.AppManager.db.dao.BackupDao; import io.github.muntashirakon.AppManager.db.entity.App; import io.github.muntashirakon.AppManager.db.entity.Backup; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.rules.compontents.ComponentsBlocker; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.settings.FeatureController; import io.github.muntashirakon.AppManager.ssaid.SsaidSettings; import io.github.muntashirakon.AppManager.types.PackageSizeInfo; import io.github.muntashirakon.AppManager.uri.UriManager; import io.github.muntashirakon.AppManager.usage.AppUsageStatsManager; import io.github.muntashirakon.AppManager.usage.PackageUsageInfo; import io.github.muntashirakon.AppManager.usage.TimeInterval; import io.github.muntashirakon.AppManager.usage.UsageUtils; import io.github.muntashirakon.AppManager.users.Users; import io.github.muntashirakon.AppManager.utils.BroadcastUtils; import io.github.muntashirakon.AppManager.utils.ExUtils; import io.github.muntashirakon.AppManager.utils.KeyStoreUtils; import io.github.muntashirakon.AppManager.utils.PackageUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; public class AppDb { public static final String TAG = AppDb.class.getSimpleName(); private static final Object sLock = new Object(); private final AppDao mAppDao; private final BackupDao mBackupDao; public AppDb() { mAppDao = AppsDb.getInstance().appDao(); mBackupDao = AppsDb.getInstance().backupDao(); } public List getAllApplications() { synchronized (sLock) { return mAppDao.getAll(); } } public List getAllInstalledApplications() { synchronized (sLock) { return mAppDao.getAllInstalled(); } } public List getAllApplications(String packageName) { synchronized (sLock) { return mAppDao.getAll(packageName); } } public List getAllApplications(String packageName, @UserIdInt int userId) { synchronized (sLock) { return mAppDao.getAll(packageName, userId); } } public List getAllBackups() { synchronized (sLock) { return mBackupDao.getAll(); } } public List getAllBackups(String packageName) { synchronized (sLock) { return mBackupDao.get(packageName); } } /** * Fetch backups without a lock file. Necessary checks must be done to ensure that the backups actually exist. */ public List getAllBackupsNoLock(String packageName) { return mBackupDao.get(packageName); } public void insert(App app) { synchronized (sLock) { mAppDao.insert(app); } } public void insert(Backup backup) { synchronized (sLock) { mBackupDao.insert(backup); } } public void insertBackups(List backups) { synchronized (sLock) { mBackupDao.insert(backups); } } public void deleteApplication(String packageName, int userId) { synchronized (sLock) { mAppDao.delete(packageName, userId); } } public void deleteAllApplications() { synchronized (sLock) { mAppDao.deleteAll(); } } public void deleteAllBackups() { synchronized (sLock) { mBackupDao.deleteAll(); } } public void deleteBackup(Backup backup) { synchronized (sLock) { mBackupDao.delete(backup); } } @WorkerThread public void loadInstalledOrBackedUpApplications(@NonNull Context context) { getBackups(true); updateApplications(context); } @WorkerThread public List updateApplications(@NonNull Context context, @NonNull String[] packageNames) { synchronized (sLock) { List appList = new ArrayList<>(); for (String packageName : packageNames) { appList.addAll(updateApplicationInternal(context, packageName)); } // Update usage and others updateVariableData(context, appList); mAppDao.insert(appList); return appList; } } @WorkerThread public List updateApplication(@NonNull Context context, @NonNull String packageName) { synchronized (sLock) { List appList = updateApplicationInternal(context, packageName); // Update usage and others updateVariableData(context, appList); mAppDao.insert(appList); return appList; } } @WorkerThread @NonNull private List updateApplicationInternal(@NonNull Context context, @NonNull String packageName) { int[] userIds = Users.getUsersIds(); List oldApps = new ArrayList<>(mAppDao.getAll(packageName)); List appList = new ArrayList<>(userIds.length); List backups = new ArrayList<>(mBackupDao.get(packageName)); for (int userId : userIds) { int oldAppIndex = findIndexOfApp(oldApps, packageName, userId); PackageInfo packageInfo = null; Backup backup = null; ListIterator backupListIterator = backups.listIterator(); while (backupListIterator.hasNext()) { Backup b = backupListIterator.next(); if (b.userId == userId) { backup = b; backupListIterator.remove(); break; } } try { packageInfo = PackageManagerCompat.getPackageInfo(packageName, PackageManager.GET_META_DATA | GET_SIGNING_CERTIFICATES | PackageManager.GET_ACTIVITIES | PackageManager.GET_RECEIVERS | PackageManager.GET_PROVIDERS | PackageManager.GET_SERVICES | MATCH_DISABLED_COMPONENTS | MATCH_UNINSTALLED_PACKAGES | MATCH_STATIC_SHARED_AND_SDK_LIBRARIES, userId); } catch (RemoteException | PackageManager.NameNotFoundException | SecurityException e) { // Package does not exist } if (backup == null && packageInfo == null) { // Neither backup nor package exist if (oldAppIndex >= 0) { // Delete existing backup mAppDao.delete(oldApps.get(oldAppIndex)); } continue; } if (oldAppIndex >= 0) { // There's already existing app App oldApp = oldApps.get(oldAppIndex); mAppDao.delete(oldApp); if ((packageInfo != null && isUpToDate(oldApp, packageInfo)) || (backup != null && isUpToDate(oldApp, backup))) { // Up-to-date app appList.add(oldApp); oldApp.lastActionTime = System.currentTimeMillis(); continue; } } // New app App app = packageInfo != null ? App.fromPackageInfo(context, packageInfo) : App.fromBackup(backup); appList.add(app); } // Add the rest of the backups if any for (Backup backup : backups) { appList.add(App.fromBackup(backup)); } // Return the list instead of triggering broadcast return appList; } @WorkerThread public void updateApplications(@NonNull Context context) { synchronized (sLock) { Map backups = getBackups(false); List oldApps = new ArrayList<>(mAppDao.getAll()); List modifiedApps = new ArrayList<>(); Set newApps = new HashSet<>(); Set updatedApps = new HashSet<>(); // Interrupt thread on request if (ThreadUtils.isInterrupted()) return; for (int userId : Users.getUsersIds()) { // Interrupt thread on request if (ThreadUtils.isInterrupted()) return; if (!SelfPermissions.checkCrossUserPermission(userId, false)) { // No support for cross user continue; } List packageInfoList = PackageManagerCompat.getInstalledPackages( GET_SIGNING_CERTIFICATES | PackageManager.GET_ACTIVITIES | PackageManager.GET_RECEIVERS | PackageManager.GET_PROVIDERS | PackageManager.GET_SERVICES | MATCH_DISABLED_COMPONENTS | MATCH_UNINSTALLED_PACKAGES | MATCH_STATIC_SHARED_AND_SDK_LIBRARIES, userId); for (PackageInfo packageInfo : packageInfoList) { // Interrupt thread on request if (ThreadUtils.isInterrupted()) return; int oldAppIndex = findIndexOfApp(oldApps, packageInfo.packageName, UserHandleHidden.getUserId(packageInfo.applicationInfo.uid)); if (oldAppIndex >= 0) { // There's already existing app App oldApp = oldApps.remove(oldAppIndex); if (isUpToDate(oldApp, packageInfo)) { // Up-to-date app updatedApps.add(oldApp.packageName); modifiedApps.add(oldApp); backups.remove(packageInfo.packageName); oldApp.lastActionTime = System.currentTimeMillis(); continue; } } // New app App app = App.fromPackageInfo(context, packageInfo); backups.remove(packageInfo.packageName); newApps.add(app.packageName); modifiedApps.add(app); } } // Update usage and others updateVariableData(context, modifiedApps); // Add rest of the backup items, i.e., items that aren't installed for (Backup backup : backups.values()) { if (backup == null) continue; // Interrupt thread on request if (ThreadUtils.isInterrupted()) return; int oldAppIndex = findIndexOfApp(oldApps, backup.packageName, backup.userId); if (oldAppIndex >= 0) { // There's already existing app App oldApp = oldApps.remove(oldAppIndex); if (isUpToDate(oldApp, backup)) { // Up-to-date app updatedApps.add(oldApp.packageName); modifiedApps.add(oldApp); continue; } } // New app App app = App.fromBackup(backup); newApps.add(app.packageName); modifiedApps.add(app); } // Add new data mAppDao.delete(oldApps); mAppDao.insert(modifiedApps); if (!oldApps.isEmpty()) { // Delete broadcast BroadcastUtils.sendDbPackageRemoved(context, getPackageNamesFromApps(oldApps)); } if (!newApps.isEmpty()) { // New apps BroadcastUtils.sendDbPackageAdded(context, newApps.toArray(new String[0])); } if (!updatedApps.isEmpty()) { // Altered apps BroadcastUtils.sendDbPackageAltered(context, updatedApps.toArray(new String[0])); } } } @WorkerThread @NonNull public Map getBackups(boolean loadBackups) { if (loadBackups) { // Very long operation return BackupUtils.storeAllAndGetLatestBackupMetadata(); } else { return BackupUtils.getAllLatestBackupMetadataFromDb(); } } private static void updateVariableData(@NonNull Context context, @NonNull List modifiedApps) { UriManager uriManager = new UriManager(); ArrayMap userIdSsaidSettingsMap = new ArrayMap<>(); List packageUsageInfoList = new ArrayList<>(); boolean hasUsageAccess = FeatureController.isUsageAccessEnabled() && SelfPermissions.checkUsageStatsPermission(); for (int userId : Users.getUsersIds()) { // Interrupt thread on request if (ThreadUtils.isInterrupted()) return; if (hasUsageAccess) { TimeInterval interval = UsageUtils.getLastWeek(); List usageInfoList = ExUtils.exceptionAsNull(() -> AppUsageStatsManager.getInstance().getUsageStats(interval, userId)); if (usageInfoList != null) { packageUsageInfoList.addAll(usageInfoList); } } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { try { userIdSsaidSettingsMap.put(userId, new SsaidSettings(userId)); } catch (IOException e) { Log.w(TAG, "Error: " + e.getMessage()); } } } for (App app : modifiedApps) { if (!app.isInstalled && !app.isSystemApp()) { continue; } int userId = app.userId; try (ComponentsBlocker cb = ComponentsBlocker.getInstance(app.packageName, userId, false)) { app.rulesCount = cb.entryCount(); } app.codeSize = app.dataSize = 0; if (hasUsageAccess) { PackageSizeInfo sizeInfo = PackageUtils.getPackageSizeInfo(context, app.packageName, userId, null); if (sizeInfo != null) { app.codeSize = sizeInfo.codeSize + sizeInfo.obbSize; app.dataSize = sizeInfo.dataSize + sizeInfo.mediaSize + sizeInfo.cacheSize; } } // Interrupt thread on request if (ThreadUtils.isInterrupted()) return; if (!app.isInstalled) { continue; } app.hasKeystore = KeyStoreUtils.hasKeyStore(app.uid); app.usesSaf = uriManager.getGrantedUris(app.packageName) != null; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { SsaidSettings ssaidSettings = userIdSsaidSettingsMap.get(userId); if (ssaidSettings != null) { String ssaid = ssaidSettings.getSsaid(app.packageName, app.uid); app.ssaid = TextUtils.isEmpty(ssaid) ? null : ssaid; } else { app.ssaid = null; } } PackageUsageInfo usageInfo = findUsage(packageUsageInfoList, app.packageName, userId); if (usageInfo != null) { app.mobileDataUsage = usageInfo.mobileData != null ? usageInfo.mobileData.getTotal() : 0; app.wifiDataUsage = usageInfo.wifiData != null ? usageInfo.wifiData.getTotal() : 0; app.openCount = usageInfo.timesOpened; app.screenTime = usageInfo.screenTime; app.lastUsageTime = usageInfo.lastUsageTime; } else { app.mobileDataUsage = app.wifiDataUsage = app.screenTime = app.lastUsageTime = 0; app.openCount = 0; } } } private static int findIndexOfApp(@NonNull List appList, @NonNull String packageName, @UserIdInt int userId) { for (int i = 0; i < appList.size(); ++i) { App app = appList.get(i); if (app.userId == userId && app.packageName.equals(packageName)) { return i; } } return -1; } @Nullable private static PackageUsageInfo findUsage(@NonNull List usageInfoList, @NonNull String packageName, @UserIdInt int userId) { for (PackageUsageInfo usageInfo : usageInfoList) { if (usageInfo.userId == userId && usageInfo.packageName.equals(packageName)) { return usageInfo; } } return null; } private static boolean isUpToDate(@NonNull App currentApp, @NonNull PackageInfo installedPackageInfo) { if (!currentApp.isInstalled) { // The app was not installed earlier return false; } // App was installed return currentApp.lastUpdateTime == installedPackageInfo.lastUpdateTime && currentApp.flags == installedPackageInfo.applicationInfo.flags; } private static boolean isUpToDate(@NonNull App currentApp, @NonNull Backup backup) { if (currentApp.isInstalled) { // The app was installed earlier return false; } // App was not installed if (currentApp.sdk != 0) { // The app is a system app return true; } // The app is a backed up app return currentApp.lastUpdateTime == backup.backupTime; } @NonNull private static String[] getPackageNamesFromApps(@NonNull List apps) { HashSet packages = new HashSet<>(apps.size()); for (App app : apps) { packages.add(app.packageName); } return packages.toArray(new String[0]); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/debloat/BloatwareDetailsDialog.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.debloat; import android.app.Application; import android.content.Intent; import android.content.res.ColorStateList; import android.graphics.Color; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.text.SpannableStringBuilder; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.appcompat.widget.LinearLayoutCompat; import androidx.core.graphics.ColorUtils; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; import com.google.android.material.button.MaterialButton; import com.google.android.material.chip.Chip; import com.google.android.material.textview.MaterialTextView; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.StaticDataset; import io.github.muntashirakon.AppManager.db.utils.AppDb; import io.github.muntashirakon.AppManager.details.AppDetailsActivity; import io.github.muntashirakon.util.AdapterUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.AppManager.utils.appearance.ColorCodes; import io.github.muntashirakon.dialog.CapsuleBottomSheetDialogFragment; import io.github.muntashirakon.util.UiUtils; import io.github.muntashirakon.widget.FlowLayout; import io.github.muntashirakon.widget.MaterialAlertView; import io.github.muntashirakon.widget.RecyclerView; public class BloatwareDetailsDialog extends CapsuleBottomSheetDialogFragment { public static final String TAG = BloatwareDetailsDialog.class.getSimpleName(); public static final String ARG_PACKAGE_NAME = "pkg"; @NonNull public static BloatwareDetailsDialog getInstance(@NonNull String packageName) { BloatwareDetailsDialog fragment = new BloatwareDetailsDialog(); Bundle args = new Bundle(); args.putString(ARG_PACKAGE_NAME, packageName); fragment.setArguments(args); return fragment; } private ImageView mAppIconView; private MaterialButton mOpenAppInfoButton; private TextView mAppLabelView; private TextView mPackageNameView; private FlowLayout mFlowLayout; private MaterialAlertView mWarningView; private MaterialTextView mDescriptionView; private LinearLayoutCompat mSuggestionContainer; private RecyclerView mSuggestionView; private SuggestionsAdapter mAdapter; @Override public boolean displayLoaderByDefault() { return true; } @NonNull @Override public View initRootView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.dialog_bloatware_details, container, false); mAppIconView = view.findViewById(R.id.icon); mOpenAppInfoButton = view.findViewById(R.id.info); mAppLabelView = view.findViewById(R.id.name); mPackageNameView = view.findViewById(R.id.package_name); mFlowLayout = view.findViewById(R.id.tag_cloud); mWarningView = view.findViewById(R.id.alert_text); mDescriptionView = view.findViewById(R.id.apk_description); mSuggestionContainer = view.findViewById(R.id.container); mSuggestionView = view.findViewById(R.id.recycler_view); mSuggestionView.setLayoutManager(new LinearLayoutManager(requireContext())); mAdapter = new SuggestionsAdapter(); mSuggestionView.setAdapter(mAdapter); return view; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); String packageName = requireArguments().getString(ARG_PACKAGE_NAME); if (packageName == null) { dismiss(); return; } BloatwareDetailsViewModel viewModel = new ViewModelProvider(requireActivity()).get(BloatwareDetailsViewModel.class); viewModel.debloatObjectLiveData.observe(getViewLifecycleOwner(), debloatObject -> { if (debloatObject == null) { dismiss(); return; } finishLoading(); updateDialog(debloatObject); updateDialog(debloatObject.getSuggestions()); }); viewModel.findDebloatObject(packageName); } private void updateDialog(@NonNull DebloatObject debloatObject) { Drawable icon = debloatObject.getIcon(); mAppIconView.setImageDrawable(icon != null ? icon : requireActivity().getPackageManager().getDefaultActivityIcon()); int[] users = debloatObject.getUsers(); if (users != null && users.length > 0) { mOpenAppInfoButton.setVisibility(View.VISIBLE); mOpenAppInfoButton.setOnClickListener(v -> { Intent appDetailsIntent = AppDetailsActivity.getIntent(requireContext(), debloatObject.packageName, users[0]); startActivity(appDetailsIntent); dismiss(); }); } else { mOpenAppInfoButton.setVisibility(View.GONE); } mAppLabelView.setText(debloatObject.getLabelOrPackageName()); mPackageNameView.setText(debloatObject.packageName); String warning = debloatObject.getWarning(); if (warning != null) { mWarningView.setVisibility(View.VISIBLE); mWarningView.setText(warning); if (debloatObject.getRemoval() >= DebloatObject.REMOVAL_CAUTION) { mWarningView.setAlertType(MaterialAlertView.ALERT_TYPE_INFO); } else mWarningView.setAlertType(MaterialAlertView.ALERT_TYPE_WARN); } else mWarningView.setVisibility(View.GONE); mDescriptionView.setText(getDescription(debloatObject)); // Add tags int removalColor; @StringRes int removalRes; switch (debloatObject.getRemoval()) { case DebloatObject.REMOVAL_SAFE: removalColor = ColorCodes.getRemovalSafeIndicatorColor(requireContext()); removalRes = R.string.debloat_removal_safe_short_description; break; default: case DebloatObject.REMOVAL_CAUTION: removalColor = ColorCodes.getRemovalCautionIndicatorColor(requireContext()); removalRes = R.string.debloat_removal_caution_short_description; break; case DebloatObject.REMOVAL_REPLACE: removalColor = ColorCodes.getRemovalReplaceIndicatorColor(requireContext()); removalRes = R.string.debloat_removal_replace_short_description; break; case DebloatObject.REMOVAL_UNSAFE: removalColor = ColorCodes.getRemovalUnsafeIndicatorColor(requireContext()); removalRes = R.string.debloat_removal_unsafe; break; } mFlowLayout.removeAllViews(); addTag(mFlowLayout, debloatObject.type); addTag(mFlowLayout, removalRes, removalColor); } private void updateDialog(@Nullable List suggestionObjects) { if (suggestionObjects == null || suggestionObjects.isEmpty()) { mSuggestionContainer.setVisibility(View.GONE); return; } mSuggestionContainer.setVisibility(View.VISIBLE); mAdapter.setList(suggestionObjects); } @NonNull private CharSequence getDescription(@NonNull DebloatObject debloatObject) { String description = debloatObject.getDescription(); String[] refSites = debloatObject.getWebRefs(); String[] dependencies = debloatObject.getDependencies(); String[] requiredBy = debloatObject.getRequiredBy(); SpannableStringBuilder sb = new SpannableStringBuilder(); sb.append(description.trim()); if (dependencies.length > 0) { // Add dependencies if (dependencies.length == 1) { sb.append(UIUtils.getBoldString("\n\nDependency: ")).append(dependencies[0]); } else { sb.append(UIUtils.getBoldString("\n\nDependencies\n")) .append(UiUtils.getOrderedList(Arrays.asList(dependencies))); } } if (requiredBy.length > 0) { // Add dependencies if (requiredBy.length == 1) { sb.append(UIUtils.getBoldString("\n\nRequired by: ")).append(requiredBy[0]); } else { sb.append(UIUtils.getBoldString("\n\nRequired by\n")) .append(UiUtils.getOrderedList(Arrays.asList(requiredBy))); } } if (refSites.length > 0) { // Add references sb.append(UIUtils.getBoldString("\n\nReferences\n")) .append(UiUtils.getOrderedList(Arrays.asList(refSites))); } return sb; } private void addTag(@NonNull ViewGroup parent, @StringRes int titleRes, @ColorInt int background) { Chip chip = (Chip) LayoutInflater.from(requireContext()).inflate(R.layout.item_chip, parent, false); chip.setText(titleRes); chip.setChipBackgroundColor(ColorStateList.valueOf(background)); double luminance = ColorUtils.calculateLuminance(background); chip.setTextColor(luminance < 0.5 ? Color.WHITE : Color.BLACK); parent.addView(chip); } private void addTag(@NonNull ViewGroup parent, @NonNull CharSequence title) { Chip chip = (Chip) LayoutInflater.from(requireContext()).inflate(R.layout.item_chip, parent, false); chip.setText(title); parent.addView(chip); } public static class BloatwareDetailsViewModel extends AndroidViewModel { public final MutableLiveData debloatObjectLiveData = new MutableLiveData<>(); public BloatwareDetailsViewModel(@NonNull Application application) { super(application); } public void findDebloatObject(@NonNull String packageName) { ThreadUtils.postOnBackgroundThread(() -> { List debloatObjects = StaticDataset.getDebloatObjects(); for (DebloatObject debloatObject : debloatObjects) { if (packageName.equals(debloatObject.packageName)) { debloatObject.fillInstallInfo(getApplication(), new AppDb()); debloatObjectLiveData.postValue(debloatObject); return; } } debloatObjectLiveData.postValue(null); }); } } private class SuggestionsAdapter extends RecyclerView.Adapter { private final List mSuggestions = Collections.synchronizedList(new ArrayList<>()); public SuggestionsAdapter() { } public void setList(@NonNull List suggestions) { AdapterUtils.notifyDataSetChanged(this, mSuggestions, suggestions); } @NonNull @Override public SuggestionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_bloatware_details, parent, false); return new SuggestionViewHolder(v); } @Override public void onBindViewHolder(@NonNull SuggestionViewHolder holder, int position) { SuggestionObject suggestion = mSuggestions.get(position); holder.labelView.setText(suggestion.getLabel()); holder.packageNameView.setText(suggestion.packageName); int[] users = suggestion.getUsers(); if (users != null && users.length > 0) { MaterialButton appInfoButton = holder.marketOrAppInfoButton; appInfoButton.setIconResource(io.github.muntashirakon.ui.R.drawable.ic_information); appInfoButton.setOnClickListener(v -> { Intent appDetailsIntent = AppDetailsActivity.getIntent(requireContext(), suggestion.packageName, users[0]); startActivity(appDetailsIntent); }); } else { MaterialButton marketButton = holder.marketOrAppInfoButton; marketButton.setIconResource(suggestion.isInFDroidMarket() ? R.drawable.ic_frost_fdroid : R.drawable.ic_frost_aurorastore); marketButton.setOnClickListener(v -> { Intent appDetailsIntent = suggestion.getMarketLink(); appDetailsIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); try { startActivity(appDetailsIntent); } catch (Throwable th) { UIUtils.displayLongToast("Error: " + th.getMessage()); } }); } String reason = suggestion.getReason(); StringBuilder sb = new StringBuilder(); if (reason != null) sb.append(reason).append("\n"); sb.append(suggestion.getRepo()); holder.repoView.setText(sb); } @Override public int getItemCount() { return mSuggestions.size(); } @Override public long getItemId(int position) { return mSuggestions.get(position).hashCode(); } private class SuggestionViewHolder extends RecyclerView.ViewHolder { final TextView labelView; final TextView packageNameView; final TextView repoView; final MaterialButton marketOrAppInfoButton; public SuggestionViewHolder(@NonNull View itemView) { super(itemView); labelView = itemView.findViewById(R.id.name); packageNameView = itemView.findViewById(R.id.package_name); repoView = itemView.findViewById(R.id.message); marketOrAppInfoButton = itemView.findViewById(R.id.info); } } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/debloat/DebloatObject.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.debloat; import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES; import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.MATCH_UNINSTALLED_PACKAGES; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; import android.os.RemoteException; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.gson.annotations.SerializedName; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; import io.github.muntashirakon.AppManager.compat.ApplicationInfoCompat; import io.github.muntashirakon.AppManager.compat.PackageManagerCompat; import io.github.muntashirakon.AppManager.db.entity.App; import io.github.muntashirakon.AppManager.db.utils.AppDb; import io.github.muntashirakon.AppManager.utils.ArrayUtils; import io.github.muntashirakon.AppManager.utils.FreezeUtils; public class DebloatObject { @IntDef({REMOVAL_SAFE, REMOVAL_REPLACE, REMOVAL_CAUTION, REMOVAL_UNSAFE}) @Retention(RetentionPolicy.SOURCE) public @interface Removal { } public static final int REMOVAL_SAFE = 1; public static final int REMOVAL_REPLACE = 1 << 1; public static final int REMOVAL_CAUTION = 1 << 2; public static final int REMOVAL_UNSAFE = 1 << 3; @SerializedName("id") public String packageName; @SerializedName("label") @Nullable private String mInternalLabel; @SerializedName("tags") @Nullable private String[] mTags; @SerializedName("dependencies") @Nullable private String[] mDependencies; @SerializedName("required_by") @Nullable private String[] mRequiredBy; // Possible values: aosp, carrier, google, misc, oem, pending @SerializedName("type") public String type; @SerializedName("description") private String mDescription; @SerializedName("web") @Nullable private String[] mWebRefs; @SerializedName("removal") private String mRemoval; @SerializedName("warning") @Nullable private String mWarning; @SerializedName("suggestions") @Nullable private String mSuggestionId; private int mId; @Nullable private Drawable mIcon; @Nullable private CharSequence mLabel; @Nullable private int[] mUsers; private boolean mInstalled; @Nullable private Boolean mSystemApp = null; @Nullable private Boolean mFrozen = null; @Nullable private List mSuggestions; public void setId(int id) { mId = id; } public int getId() { return mId; } @NonNull public String[] getDependencies() { return ArrayUtils.defeatNullable(mDependencies); } @NonNull public String[] getRequiredBy() { return ArrayUtils.defeatNullable(mRequiredBy); } @Removal public int getRemoval() { switch (mRemoval) { default: case "safe": return REMOVAL_SAFE; case "replace": return REMOVAL_REPLACE; case "caution": return REMOVAL_CAUTION; case "unsafe": return REMOVAL_UNSAFE; } } @Nullable public String getWarning() { return mWarning; } public String getDescription() { return mDescription; } @NonNull public String[] getWebRefs() { return ArrayUtils.defeatNullable(mWebRefs); } @Nullable public String getSuggestionId() { return mSuggestionId; } @Nullable public List getSuggestions() { return mSuggestions; } public void setSuggestions(@Nullable List suggestions) { mSuggestions = suggestions; } @Nullable public CharSequence getLabel() { return mLabel != null ? mLabel : mInternalLabel; } @NonNull public CharSequence getLabelOrPackageName() { CharSequence label = mLabel != null ? mLabel : mInternalLabel; return label != null ? label : packageName; } @Nullable public Drawable getIcon() { return mIcon; } @Nullable public int[] getUsers() { return mUsers; } private void addUser(int userId) { if (mUsers == null) { mUsers = new int[]{userId}; } else { mUsers = ArrayUtils.appendInt(mUsers, userId); } } public boolean isInstalled() { return mInstalled; } public boolean isSystemApp() { return Boolean.TRUE.equals(mSystemApp); } public boolean isUserApp() { return Boolean.FALSE.equals(mSystemApp); } public boolean isFrozen() { return Boolean.TRUE.equals(mFrozen); } public void fillInstallInfo(@NonNull Context context, @NonNull AppDb appDb) { PackageManager pm = context.getPackageManager(); List suggestionObjects = getSuggestions(); if (suggestionObjects != null) { for (SuggestionObject suggestionObject : suggestionObjects) { List apps = appDb.getAllApplications(suggestionObject.packageName); for (App app : apps) { if (app.isInstalled) { suggestionObject.addUser(app.userId); } } } } // Update application data mInstalled = false; mUsers = null; mSystemApp = null; mFrozen = null; List apps = appDb.getAllApplications(packageName); for (App app : apps) { if (!app.isInstalled) { continue; } mInstalled = true; addUser(app.userId); mSystemApp = app.isSystemApp(); mFrozen = !app.isEnabled; mLabel = app.packageLabel; if (getIcon() == null) { try { ApplicationInfo ai = PackageManagerCompat.getApplicationInfo(packageName, MATCH_UNINSTALLED_PACKAGES | MATCH_STATIC_SHARED_AND_SDK_LIBRARIES, app.userId); mInstalled = (ai.flags & ApplicationInfo.FLAG_INSTALLED) != 0; mSystemApp = ApplicationInfoCompat.isSystemApp(ai); mLabel = ai.loadLabel(pm); mIcon = ai.loadIcon(pm); mFrozen = FreezeUtils.isFrozen(ai); } catch (RemoteException | PackageManager.NameNotFoundException ignore) { } } } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/debloat/DebloaterActivity.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.debloat; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import android.view.View; import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.core.content.ContextCompat; import androidx.lifecycle.ViewModelProvider; import com.google.android.material.checkbox.MaterialCheckBox; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.progressindicator.LinearProgressIndicator; import io.github.muntashirakon.AppManager.BaseActivity; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.apk.behavior.FreezeUnfreeze; import io.github.muntashirakon.AppManager.batchops.BatchOpsManager; import io.github.muntashirakon.AppManager.batchops.BatchOpsService; import io.github.muntashirakon.AppManager.batchops.BatchQueueItem; import io.github.muntashirakon.AppManager.batchops.struct.BatchFreezeOptions; import io.github.muntashirakon.AppManager.batchops.struct.IBatchOpOptions; import io.github.muntashirakon.AppManager.misc.AdvancedSearchView; import io.github.muntashirakon.AppManager.profiles.AddToProfileDialogFragment; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.utils.StoragePermission; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.multiselection.MultiSelectionActionsView; import io.github.muntashirakon.widget.MultiSelectionView; import io.github.muntashirakon.widget.RecyclerView; public class DebloaterActivity extends BaseActivity implements MultiSelectionView.OnSelectionChangeListener, MultiSelectionActionsView.OnItemSelectedListener, AdvancedSearchView.OnQueryTextListener, MultiSelectionView.OnSelectionModeChangeListener { DebloaterViewModel viewModel; private LinearProgressIndicator mProgressIndicator; private MultiSelectionView mMultiSelectionView; private DebloaterRecyclerViewAdapter mAdapter; private final StoragePermission mStoragePermission = StoragePermission.init(this); private final BroadcastReceiver mBatchOpsBroadCastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (mProgressIndicator != null) { mProgressIndicator.hide(); } } }; private final OnBackPressedCallback mOnBackPressedCallback = new OnBackPressedCallback(false) { @Override public void handleOnBackPressed() { if (mAdapter != null && mMultiSelectionView != null && mAdapter.isInSelectionMode()) { mMultiSelectionView.cancel(); return; } setEnabled(false); getOnBackPressedDispatcher().onBackPressed(); } }; @Override protected void onAuthenticated(@Nullable Bundle savedInstanceState) { setContentView(R.layout.activity_debloater); setSupportActionBar(findViewById(R.id.toolbar)); getOnBackPressedDispatcher().addCallback(this, mOnBackPressedCallback); ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setDisplayShowCustomEnabled(true); UIUtils.setupAdvancedSearchView(actionBar, this); } viewModel = new ViewModelProvider(this).get(DebloaterViewModel.class); mProgressIndicator = findViewById(R.id.progress_linear); mProgressIndicator.show(); RecyclerView recyclerView = findViewById(R.id.recycler_view); recyclerView.setLayoutManager(UIUtils.getGridLayoutAt450Dp(this)); mAdapter = new DebloaterRecyclerViewAdapter(this); recyclerView.setAdapter(mAdapter); mMultiSelectionView = findViewById(R.id.selection_view); mMultiSelectionView.setAdapter(mAdapter); mMultiSelectionView.hide(); mMultiSelectionView.setOnItemSelectedListener(this); mMultiSelectionView.setOnSelectionModeChangeListener(this); mMultiSelectionView.setOnSelectionChangeListener(this); viewModel.getDebloatObjectListLiveData().observe(this, debloatObjects -> { mProgressIndicator.hide(); mAdapter.setAdapterList(debloatObjects); }); viewModel.loadPackages(); } @Override protected void onResume() { super.onResume(); ContextCompat.registerReceiver(this, mBatchOpsBroadCastReceiver, new IntentFilter(BatchOpsService.ACTION_BATCH_OPS_COMPLETED), ContextCompat.RECEIVER_NOT_EXPORTED); } @Override protected void onPause() { super.onPause(); unregisterReceiver(mBatchOpsBroadCastReceiver); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.activity_debloater_actions, menu); return super.onCreateOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int id = item.getItemId(); if (id == android.R.id.home) { finish(); return true; } else if (id == R.id.action_list_options) { DebloaterListOptions dialog = new DebloaterListOptions(); dialog.show(getSupportFragmentManager(), DebloaterListOptions.TAG); return true; } return super.onOptionsItemSelected(item); } @Override public void onSelectionModeEnabled() { mOnBackPressedCallback.setEnabled(true); } @Override public void onSelectionModeDisabled() { mOnBackPressedCallback.setEnabled(false); } @Override public boolean onSelectionChange(int selectionCount) { // TODO: 7/8/22 return false; } @Override public boolean onNavigationItemSelected(@NonNull MenuItem item) { int id = item.getItemId(); if (id == R.id.action_uninstall) { handleBatchOpWithWarning(BatchOpsManager.OP_UNINSTALL); } else if (id == R.id.action_put_back) { // TODO: 8/8/22 } else if (id == R.id.action_freeze_unfreeze) { showFreezeUnfreezeDialog(Prefs.Blocking.getDefaultFreezingMethod()); } else if (id == R.id.action_save_apk) { mStoragePermission.request(granted -> { if (granted) handleBatchOp(BatchOpsManager.OP_BACKUP_APK); }); } else if (id == R.id.action_block_unblock_trackers) { new MaterialAlertDialogBuilder(this) .setTitle(R.string.block_unblock_trackers) .setMessage(R.string.choose_what_to_do) .setPositiveButton(R.string.block, (dialog, which) -> handleBatchOp(BatchOpsManager.OP_BLOCK_TRACKERS)) .setNegativeButton(R.string.cancel, null) .setNeutralButton(R.string.unblock, (dialog, which) -> handleBatchOp(BatchOpsManager.OP_UNBLOCK_TRACKERS)) .show(); } else if (id == R.id.action_add_to_profile) { AddToProfileDialogFragment dialog = AddToProfileDialogFragment.getInstance(viewModel.getSelectedPackages() .keySet().toArray(new String[0])); dialog.show(getSupportFragmentManager(), AddToProfileDialogFragment.TAG); } else return false; return true; } @Override public boolean onQueryTextChange(String newText, int type) { viewModel.setQuery(newText, type); return true; } @Override public boolean onQueryTextSubmit(String query, int type) { return false; } private void showFreezeUnfreezeDialog(int freezeType) { View view = View.inflate(this, R.layout.item_checkbox, null); MaterialCheckBox checkBox = view.findViewById(R.id.checkbox); checkBox.setText(R.string.freeze_prefer_per_app_option); FreezeUnfreeze.getFreezeDialog(this, freezeType) .setIcon(R.drawable.ic_snowflake) .setTitle(R.string.freeze_unfreeze) .setView(view) .setPositiveButton(R.string.freeze, (dialog, which, selectedItem) -> { if (selectedItem == null) { return; } BatchFreezeOptions options = new BatchFreezeOptions(selectedItem, checkBox.isChecked()); handleBatchOp(BatchOpsManager.OP_ADVANCED_FREEZE, options); }) .setNegativeButton(R.string.cancel, null) .setNeutralButton(R.string.unfreeze, (dialog, which, selectedItem) -> handleBatchOp(BatchOpsManager.OP_UNFREEZE)) .show(); } private void handleBatchOpWithWarning(@BatchOpsManager.OpType int op) { new MaterialAlertDialogBuilder(this) .setTitle(R.string.are_you_sure) .setMessage(R.string.this_action_cannot_be_undone) .setPositiveButton(R.string.yes, (dialog, which) -> handleBatchOp(op)) .setNegativeButton(R.string.no, null) .show(); } private void handleBatchOp(@BatchOpsManager.OpType int op) { handleBatchOp(op, null); } private void handleBatchOp(@BatchOpsManager.OpType int op, @Nullable IBatchOpOptions options) { if (viewModel == null) return; if (mProgressIndicator != null) { mProgressIndicator.show(); } BatchOpsManager.Result input = new BatchOpsManager.Result(viewModel.getSelectedPackagesWithUsers()); BatchQueueItem item = BatchQueueItem.getBatchOpQueue(op, input.getFailedPackages(), input.getAssociatedUsers(), options); ContextCompat.startForegroundService(this, BatchOpsService.getServiceIntent(this, item)); mMultiSelectionView.cancel(); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/debloat/DebloaterListOptions.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.debloat; import android.content.Context; import android.os.Bundle; import android.util.SparseIntArray; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import com.google.android.material.chip.Chip; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.dialog.CapsuleBottomSheetDialogFragment; public class DebloaterListOptions extends CapsuleBottomSheetDialogFragment { public static final String TAG = DebloaterListOptions.class.getSimpleName(); @IntDef(flag = true, value = { FILTER_NO_FILTER, FILTER_LIST_AOSP, FILTER_LIST_OEM, FILTER_LIST_CARRIER, FILTER_LIST_GOOGLE, FILTER_LIST_MISC, FILTER_REMOVAL_SAFE, FILTER_REMOVAL_REPLACE, FILTER_REMOVAL_CAUTION, FILTER_REMOVAL_UNSAFE, FILTER_USER_APPS, FILTER_SYSTEM_APPS, FILTER_INSTALLED_APPS, FILTER_UNINSTALLED_APPS, FILTER_FROZEN_APPS, FILTER_UNFROZEN_APPS, }) @Retention(RetentionPolicy.SOURCE) public @interface Filter { } public static final int FILTER_NO_FILTER = 0; public static final int FILTER_LIST_AOSP = 1; public static final int FILTER_LIST_OEM = 1 << 1; public static final int FILTER_LIST_CARRIER = 1 << 2; public static final int FILTER_LIST_GOOGLE = 1 << 3; public static final int FILTER_LIST_MISC = 1 << 4; public static final int FILTER_REMOVAL_SAFE = 1 << 6; public static final int FILTER_REMOVAL_REPLACE = 1 << 7; public static final int FILTER_REMOVAL_CAUTION = 1 << 8; public static final int FILTER_REMOVAL_UNSAFE = 1 << 9; public static final int FILTER_USER_APPS = 1 << 10; public static final int FILTER_SYSTEM_APPS = 1 << 11; public static final int FILTER_INSTALLED_APPS = 1 << 12; public static final int FILTER_UNINSTALLED_APPS = 1 << 13; public static final int FILTER_FROZEN_APPS = 1 << 14; public static final int FILTER_UNFROZEN_APPS = 1 << 15; private static final SparseIntArray LIST_FILTER_MAP = new SparseIntArray() {{ put(FILTER_LIST_AOSP, R.string.debloat_list_aosp); put(FILTER_LIST_OEM, R.string.debloat_list_oem); put(FILTER_LIST_CARRIER, R.string.debloat_list_carrier); put(FILTER_LIST_GOOGLE, R.string.debloat_list_google); put(FILTER_LIST_MISC, R.string.debloat_list_misc); }}; private static final SparseIntArray REMOVAL_FILTER_MAP = new SparseIntArray() {{ put(FILTER_REMOVAL_SAFE, R.string.debloat_removal_safe); put(FILTER_REMOVAL_REPLACE, R.string.debloat_removal_replace); put(FILTER_REMOVAL_CAUTION, R.string.debloat_removal_caution); put(FILTER_REMOVAL_UNSAFE, R.string.debloat_removal_unsafe); }}; private static final SparseIntArray NORMAL_FILTER_MAP = new SparseIntArray() {{ put(FILTER_USER_APPS, R.string.filter_user_apps); put(FILTER_SYSTEM_APPS, R.string.filter_system_apps); put(FILTER_INSTALLED_APPS, R.string.installed_apps); put(FILTER_UNINSTALLED_APPS, R.string.uninstalled_apps); put(FILTER_FROZEN_APPS, R.string.filter_frozen_apps); put(FILTER_UNFROZEN_APPS, R.string.filter_unfrozen_apps); }}; @Filter public static int getDefaultFilterFlags() { return FILTER_LIST_AOSP | FILTER_LIST_OEM | FILTER_LIST_CARRIER | FILTER_LIST_GOOGLE | FILTER_LIST_MISC | FILTER_REMOVAL_SAFE | FILTER_REMOVAL_REPLACE | FILTER_REMOVAL_CAUTION | FILTER_INSTALLED_APPS | FILTER_SYSTEM_APPS; } private DebloaterViewModel mModel; @NonNull @Override public View initRootView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.dialog_debloater_list_options, container, false); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); DebloaterActivity activity = (DebloaterActivity) requireActivity(); mModel = activity.viewModel; ViewGroup listTypes = view.findViewById(R.id.list_types); for (int i = 0; i < LIST_FILTER_MAP.size(); ++i) { listTypes.addView(getFilterChip(listTypes.getContext(), LIST_FILTER_MAP.keyAt(i), LIST_FILTER_MAP.valueAt(i))); } ViewGroup removalTypes = view.findViewById(R.id.removal_types); for (int i = 0; i < REMOVAL_FILTER_MAP.size(); ++i) { removalTypes.addView(getFilterChip(removalTypes.getContext(), REMOVAL_FILTER_MAP.keyAt(i), REMOVAL_FILTER_MAP.valueAt(i))); } ViewGroup filterView = view.findViewById(R.id.filter_options); for (int i = 0; i < NORMAL_FILTER_MAP.size(); ++i) { filterView.addView(getFilterChip(filterView.getContext(), NORMAL_FILTER_MAP.keyAt(i), NORMAL_FILTER_MAP.valueAt(i))); } } public Chip getFilterChip(@NonNull Context context, @Filter int flag, @StringRes int strRes) { Chip chip = new Chip(context); chip.setFocusable(true); chip.setCloseIconVisible(false); chip.setText(strRes); chip.setChecked(mModel.hasFilterFlag(flag)); chip.setOnCheckedChangeListener((buttonView, isChecked) -> { if (isChecked) mModel.addFilterFlag(flag); else mModel.removeFilterFlag(flag); }); return chip; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/debloat/DebloaterRecyclerViewAdapter.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.debloat; import static io.github.muntashirakon.AppManager.utils.UIUtils.getColoredText; import android.content.Context; import android.graphics.drawable.Drawable; import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.StringRes; import androidx.appcompat.widget.AppCompatImageView; import androidx.fragment.app.FragmentActivity; import com.google.android.material.card.MaterialCardView; import com.google.android.material.color.MaterialColors; import com.google.android.material.textview.MaterialTextView; import java.util.ArrayList; import java.util.List; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.utils.appearance.ColorCodes; import io.github.muntashirakon.util.AccessibilityUtils; import io.github.muntashirakon.util.AdapterUtils; import io.github.muntashirakon.widget.MultiSelectionView; public class DebloaterRecyclerViewAdapter extends MultiSelectionView.Adapter { private final List mAdapterList = new ArrayList<>(); private final FragmentActivity mActivity; @ColorInt private final int mRemovalSafeColor; @ColorInt private final int mRemovalReplaceColor; @ColorInt private final int mRemovalUnsafeColor; @ColorInt private final int mRemovalCautionColor; @ColorInt private final int mColorSurface; private final Object mLock = new Object(); @NonNull private final DebloaterViewModel mViewModel; @NonNull private final Drawable mDefaultIcon; public DebloaterRecyclerViewAdapter(DebloaterActivity activity) { mActivity = activity; mRemovalSafeColor = ColorCodes.getRemovalSafeIndicatorColor(activity); mRemovalReplaceColor = ColorCodes.getRemovalReplaceIndicatorColor(activity); mRemovalCautionColor = ColorCodes.getRemovalCautionIndicatorColor(activity); mRemovalUnsafeColor = ColorCodes.getRemovalUnsafeIndicatorColor(activity); mColorSurface = MaterialColors.getColor(activity, com.google.android.material.R.attr.colorSurface, DebloaterRecyclerViewAdapter.class.getCanonicalName()); mViewModel = activity.viewModel; mDefaultIcon = activity.getPackageManager().getDefaultActivityIcon(); } public void setAdapterList(List adapterList) { synchronized (mLock) { AdapterUtils.notifyDataSetChanged(this, mAdapterList, adapterList); } } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_debloater, parent, false); return new ViewHolder(v); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { DebloatObject debloatObject; synchronized (mLock) { debloatObject = mAdapterList.get(position); } Context context = holder.itemView.getContext(); Drawable icon = debloatObject.getIcon() != null ? debloatObject.getIcon() : mDefaultIcon; String warning = debloatObject.getWarning(); SpannableStringBuilder sb = new SpannableStringBuilder(); int removalColor; @StringRes int removalRes; switch (debloatObject.getRemoval()) { case DebloatObject.REMOVAL_SAFE: removalColor = mRemovalSafeColor; removalRes = R.string.debloat_removal_safe_short_description; break; default: case DebloatObject.REMOVAL_CAUTION: removalColor = mRemovalCautionColor; removalRes = R.string.debloat_removal_caution_short_description; break; case DebloatObject.REMOVAL_REPLACE: removalColor = mRemovalReplaceColor; removalRes = R.string.debloat_removal_replace_short_description; break; case DebloatObject.REMOVAL_UNSAFE: removalColor = mRemovalUnsafeColor; removalRes = R.string.debloat_removal_unsafe; break; } sb.append(getColoredText(context.getString(removalRes), removalColor)); if (!TextUtils.isEmpty(warning)) { sb.append(" — ").append(warning); } CharSequence label = debloatObject.getLabelOrPackageName(); holder.iconView.setImageDrawable(icon); holder.listTypeView.setText(debloatObject.type); holder.packageNameView.setText(debloatObject.packageName); holder.descriptionView.setText(sb); holder.itemView.setStrokeColor(removalColor); holder.labelView.setText(label); holder.itemView.setOnLongClickListener(v -> { toggleSelection(position); AccessibilityUtils.requestAccessibilityFocus(holder.itemView); return true; }); holder.iconView.setOnClickListener(v -> { toggleSelection(position); AccessibilityUtils.requestAccessibilityFocus(holder.itemView); }); holder.itemView.setOnClickListener(v -> { if (isInSelectionMode()) { toggleSelection(position); AccessibilityUtils.requestAccessibilityFocus(holder.itemView); } else { BloatwareDetailsDialog dialog = BloatwareDetailsDialog.getInstance(debloatObject.packageName); dialog.show(mActivity.getSupportFragmentManager(), BloatwareDetailsDialog.TAG); } }); super.onBindViewHolder(holder, position); } @Override public long getItemId(int position) { synchronized (mLock) { return mAdapterList.get(position).getId(); } } @Override public int getItemCount() { synchronized (mLock) { return mAdapterList.size(); } } @Override protected boolean select(int position) { synchronized (mLock) { mViewModel.select(mAdapterList.get(position)); return true; } } @Override protected boolean deselect(int position) { synchronized (mLock) { mViewModel.deselect(mAdapterList.get(position)); return true; } } @Override protected void cancelSelection() { super.cancelSelection(); mViewModel.deselectAll(); } @Override protected boolean isSelected(int position) { synchronized (mLock) { return mViewModel.isSelected(mAdapterList.get(position)); } } @Override protected int getSelectedItemCount() { return mViewModel.getSelectedItemCount(); } @Override protected int getTotalItemCount() { return mViewModel.getTotalItemCount(); } public static class ViewHolder extends MultiSelectionView.ViewHolder { public final MaterialCardView itemView; public final AppCompatImageView iconView; public final MaterialTextView listTypeView; public final MaterialTextView labelView; public final MaterialTextView packageNameView; public final MaterialTextView descriptionView; public ViewHolder(@NonNull View itemView) { super(itemView); this.itemView = (MaterialCardView) itemView; iconView = itemView.findViewById(R.id.icon); listTypeView = itemView.findViewById(R.id.list_type); labelView = itemView.findViewById(R.id.label); packageNameView = itemView.findViewById(R.id.package_name); descriptionView = itemView.findViewById(R.id.apk_description); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/debloat/DebloaterViewModel.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.debloat; import android.app.Application; import android.os.UserHandleHidden; import android.text.TextUtils; import androidx.annotation.AnyThread; import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.concurrent.ExecutorService; import io.github.muntashirakon.AppManager.StaticDataset; import io.github.muntashirakon.AppManager.misc.AdvancedSearchView; import io.github.muntashirakon.AppManager.types.UserPackagePair; import io.github.muntashirakon.AppManager.users.Users; import io.github.muntashirakon.AppManager.utils.AppPref; import io.github.muntashirakon.AppManager.utils.ArrayUtils; import io.github.muntashirakon.AppManager.utils.MultithreadedExecutor; public class DebloaterViewModel extends AndroidViewModel { @DebloaterListOptions.Filter private int mFilterFlags; private String mQueryString = null; @AdvancedSearchView.SearchType private int mQueryType; @NonNull private final List mDebloatObjects = new ArrayList<>(); private final Map mSelectedPackages = new HashMap<>(); private final MutableLiveData> mDebloatObjectListLiveData = new MutableLiveData<>(); private final ExecutorService mExecutor = MultithreadedExecutor.getNewInstance(); public DebloaterViewModel(@NonNull Application application) { super(application); mFilterFlags = AppPref.getInt(AppPref.PrefKey.PREF_DEBLOATER_FILTER_FLAGS_INT); } public boolean hasFilterFlag(@DebloaterListOptions.Filter int flag) { return (mFilterFlags & flag) != 0; } public void addFilterFlag(@DebloaterListOptions.Filter int flag) { mFilterFlags |= flag; AppPref.set(AppPref.PrefKey.PREF_DEBLOATER_FILTER_FLAGS_INT, mFilterFlags); loadPackages(); } public void removeFilterFlag(@DebloaterListOptions.Filter int flag) { mFilterFlags &= ~flag; AppPref.set(AppPref.PrefKey.PREF_DEBLOATER_FILTER_FLAGS_INT, mFilterFlags); loadPackages(); } public void setQuery(String queryString, @AdvancedSearchView.SearchType int searchType) { mQueryString = queryString; mQueryType = searchType; loadPackages(); } public LiveData> getDebloatObjectListLiveData() { return mDebloatObjectListLiveData; } public int getTotalItemCount() { return mDebloatObjects.size(); } public int getSelectedItemCount() { return mSelectedPackages.size(); } public void select(@NonNull DebloatObject debloatObject) { mSelectedPackages.put(debloatObject.packageName, debloatObject.getUsers()); } public void deselect(@NonNull DebloatObject debloatObject) { mSelectedPackages.remove(debloatObject.packageName); } public void deselectAll() { mSelectedPackages.clear(); } public boolean isSelected(@NonNull DebloatObject debloatObject) { return mSelectedPackages.containsKey(debloatObject.packageName); } public Map getSelectedPackages() { return mSelectedPackages; } @NonNull public ArrayList getSelectedPackagesWithUsers() { ArrayList userPackagePairs = new ArrayList<>(); int myUserId = UserHandleHidden.myUserId(); int[] userIds = Users.getUsersIds(); for (String packageName : mSelectedPackages.keySet()) { int[] userHandles = mSelectedPackages.get(packageName); if (userHandles == null || userHandles.length == 0) { // Assign current user in it userPackagePairs.add(new UserPackagePair(packageName, myUserId)); } else { for (int userHandle : userHandles) { if (!ArrayUtils.contains(userIds, userHandle)) continue; userPackagePairs.add(new UserPackagePair(packageName, userHandle)); } } } return userPackagePairs; } @AnyThread public void loadPackages() { mExecutor.submit(() -> { loadDebloatObjects(); List debloatObjects = new ArrayList<>(); if (mFilterFlags != DebloaterListOptions.FILTER_NO_FILTER) { for (DebloatObject debloatObject : mDebloatObjects) { // List if ((mFilterFlags & DebloaterListOptions.FILTER_LIST_AOSP) == 0 && debloatObject.type.equals("aosp")) { continue; } if ((mFilterFlags & DebloaterListOptions.FILTER_LIST_CARRIER) == 0 && debloatObject.type.equals("carrier")) { continue; } if ((mFilterFlags & DebloaterListOptions.FILTER_LIST_GOOGLE) == 0 && debloatObject.type.equals("google")) { continue; } if ((mFilterFlags & DebloaterListOptions.FILTER_LIST_MISC) == 0 && debloatObject.type.equals("misc")) { continue; } if ((mFilterFlags & DebloaterListOptions.FILTER_LIST_OEM) == 0 && debloatObject.type.equals("oem")) { continue; } // Removal int removalType = debloatObject.getRemoval(); if ((mFilterFlags & DebloaterListOptions.FILTER_REMOVAL_SAFE) == 0 && removalType == DebloatObject.REMOVAL_SAFE) { continue; } if ((mFilterFlags & DebloaterListOptions.FILTER_REMOVAL_REPLACE) == 0 && removalType == DebloatObject.REMOVAL_REPLACE) { continue; } if ((mFilterFlags & DebloaterListOptions.FILTER_REMOVAL_CAUTION) == 0 && removalType == DebloatObject.REMOVAL_CAUTION) { continue; } if ((mFilterFlags & DebloaterListOptions.FILTER_REMOVAL_UNSAFE) == 0 && removalType == DebloatObject.REMOVAL_UNSAFE) { continue; } // Filter others if ((mFilterFlags & DebloaterListOptions.FILTER_INSTALLED_APPS) != 0 && !debloatObject.isInstalled()) { continue; } if ((mFilterFlags & DebloaterListOptions.FILTER_UNINSTALLED_APPS) != 0 && debloatObject.isInstalled()) { continue; } if ((mFilterFlags & DebloaterListOptions.FILTER_USER_APPS) != 0 && !debloatObject.isUserApp()) { continue; } if ((mFilterFlags & DebloaterListOptions.FILTER_SYSTEM_APPS) != 0 && !debloatObject.isSystemApp()) { continue; } if ((mFilterFlags & DebloaterListOptions.FILTER_FROZEN_APPS) != 0 && !debloatObject.isFrozen()) { continue; } if ((mFilterFlags & DebloaterListOptions.FILTER_UNFROZEN_APPS) != 0 && debloatObject.isFrozen()) { continue; } debloatObjects.add(debloatObject); } } if (TextUtils.isEmpty(mQueryString)) { mDebloatObjectListLiveData.postValue(debloatObjects); return; } // Apply searching List newList = AdvancedSearchView.matches(mQueryString, debloatObjects, (AdvancedSearchView.ChoicesGenerator) item -> { CharSequence label = item.getLabel(); if (label != null) { return Arrays.asList(item.packageName, label.toString().toLowerCase(Locale.getDefault())); } else { return Collections.singletonList(item.packageName); } }, mQueryType); mDebloatObjectListLiveData.postValue(newList); }); } @WorkerThread private void loadDebloatObjects() { if (!mDebloatObjects.isEmpty()) { return; } mDebloatObjects.addAll(StaticDataset.getDebloatObjectsWithInstalledInfo(getApplication())); Collections.sort(mDebloatObjects, (o1, o2) -> CharSequence.compare(o1.getLabelOrPackageName(), o2.getLabelOrPackageName())); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/debloat/SuggestionObject.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.debloat; import android.content.Intent; import android.net.Uri; import androidx.annotation.Nullable; import com.google.gson.annotations.SerializedName; import io.github.muntashirakon.AppManager.utils.ArrayUtils; public class SuggestionObject { @SerializedName("_id") public String suggestionId; @SerializedName("id") public String packageName; @SerializedName("label") private String mLabel; @SerializedName("reason") @Nullable private String mReason; @SerializedName("source") private String mSource; @SerializedName("repo") private String mRepo; private int[] mUsers; public String getLabel() { return mLabel; } public String getRepo() { return mRepo; } @Nullable public String getReason() { return mReason; } public boolean isInFDroidMarket() { return mSource != null && mSource.contains("f"); } public Intent getMarketLink() { // Not supported by most app stores // return new Intent(Intent.ACTION_VIEW, Uri.parse("market://search?q=pname:" + packageName + " " + mLabel)); Uri uri; if (isInFDroidMarket()) { uri = Uri.parse("https://f-droid.org/packages/" + packageName); } else uri = Uri.parse("https://play.google.com/store/apps/details?id=" + packageName); return new Intent(Intent.ACTION_VIEW, uri); } public int[] getUsers() { return mUsers; } public void addUser(int userId) { if (mUsers == null) { mUsers = new int[]{userId}; } else if (!ArrayUtils.contains(mUsers, userId)) { mUsers = ArrayUtils.appendInt(mUsers, userId); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/details/ActivityLauncherShortcutActivity.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.details; import android.Manifest; import android.annotation.UserIdInt; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.os.Bundle; import android.os.UserHandleHidden; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.util.Objects; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import io.github.muntashirakon.AppManager.BaseActivity; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.apk.behavior.FreezeUnfreeze; import io.github.muntashirakon.AppManager.apk.behavior.FreezeUnfreezeService; import io.github.muntashirakon.AppManager.compat.ActivityManagerCompat; import io.github.muntashirakon.AppManager.compat.ManifestCompat; import io.github.muntashirakon.AppManager.compat.PackageManagerCompat; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.ExUtils; import io.github.muntashirakon.AppManager.utils.FreezeUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; public class ActivityLauncherShortcutActivity extends BaseActivity { private static final String EXTRA_PKG = BuildConfig.APPLICATION_ID + ".intent.EXTRA.shortcut.pkg"; private static final String EXTRA_CLS = BuildConfig.APPLICATION_ID + ".intent.EXTRA.shortcut.cls"; private static final String EXTRA_AST = BuildConfig.APPLICATION_ID + ".intent.EXTRA.shortcut.ast"; private static final String EXTRA_USR = BuildConfig.APPLICATION_ID + ".intent.EXTRA.shortcut.usr"; @NonNull public static Intent getShortcutIntent(@NonNull Context context, @NonNull String pkg, @NonNull String clazz, @UserIdInt int userId, boolean launchViaAssist) { return new Intent() .setClass(context, ActivityLauncherShortcutActivity.class) .putExtra(EXTRA_PKG, pkg) .putExtra(EXTRA_CLS, clazz) .putExtra(EXTRA_USR, userId) .putExtra(EXTRA_AST, launchViaAssist) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); } private Intent mIntent; private String mPackageName; private ComponentName mComponentName; private int mUserId; private boolean mCanLaunchViaAssist; private boolean mIsLaunchViaAssist; @Override protected void onAuthenticated(@Nullable Bundle savedInstanceState) { Intent intent = getIntent(); if (!Intent.ACTION_CREATE_SHORTCUT.equals(intent.getAction()) || !intent.hasExtra(EXTRA_PKG) || !intent.hasExtra(EXTRA_CLS)) { // Invalid intent finishActivity(0); return; } unfreezeAndLaunchActivity(intent); } @Override protected void onNewIntent(@NonNull Intent intent) { super.onNewIntent(intent); unfreezeAndLaunchActivity(intent); } @Override public boolean getTransparentBackground() { return true; } private void unfreezeAndLaunchActivity(@NonNull Intent intent) { mIntent = new Intent(intent); mPackageName = Objects.requireNonNull(mIntent.getStringExtra(EXTRA_PKG)); String className = Objects.requireNonNull(mIntent.getStringExtra(EXTRA_CLS)); mComponentName = new ComponentName(mPackageName, className); mIntent.setAction(null); mIntent.setComponent(mComponentName); mUserId = mIntent.getIntExtra(EXTRA_USR, UserHandleHidden.myUserId()); mCanLaunchViaAssist = SelfPermissions.checkSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS); mIsLaunchViaAssist = mIntent.getBooleanExtra(EXTRA_AST, false) && mCanLaunchViaAssist; mIntent.removeExtra(EXTRA_PKG); mIntent.removeExtra(EXTRA_CLS); mIntent.removeExtra(EXTRA_AST); mIntent.removeExtra(EXTRA_USR); // Check for frozen ApplicationInfo info = ExUtils.exceptionAsNull(() -> PackageManagerCompat.getApplicationInfo(mPackageName, 0, mUserId)); if (info != null && FreezeUtils.isFrozen(info)) { // Ask to unfreeze new MaterialAlertDialogBuilder(this) .setTitle(R.string.title_shortcut_for_frozen_app) .setMessage(R.string.message_shortcut_for_frozen_app) .setCancelable(false) .setPositiveButton(R.string.yes, (dialog, which) -> ThreadUtils.postOnBackgroundThread(() -> { try { FreezeUtils.unfreeze(mPackageName, mUserId); ThreadUtils.postOnMainThread(() -> { Intent service = new Intent(FreezeUnfreeze.getShortcutIntent(this, mPackageName, mUserId, 0)) .setClassName(this, FreezeUnfreezeService.class.getName()); ContextCompat.startForegroundService(this, service); launchActivity(); }); } catch (Throwable e) { ThreadUtils.postOnMainThread(() -> { UIUtils.displayShortToast(R.string.failed); finishActivity(0); }); } })) .setNegativeButton(R.string.cancel, (dialog, which) -> finishActivity(0)) .show(); } else { // Try launching it anyway (we don't care about failure) launchActivity(); } } private void launchActivity() { if (mIsLaunchViaAssist && !SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.START_ANY_ACTIVITY)) { launchActivityViaAssist(); } else { try { finishActivity(0); ActivityManagerCompat.startActivity(mIntent, mUserId); } catch (Throwable e) { e.printStackTrace(); UIUtils.displayLongToast("Error: " + e.getMessage()); // Try assist instead if (mCanLaunchViaAssist) { launchActivityViaAssist(); } else finishActivity(0); } } } private void launchActivityViaAssist() { boolean launched = ActivityManagerCompat.startActivityViaAssist(ContextUtils.getContext(), mComponentName, () -> { CountDownLatch waitForInteraction = new CountDownLatch(1); ThreadUtils.postOnMainThread(() -> new MaterialAlertDialogBuilder(this) .setTitle(R.string.launch_activity_dialog_title) .setMessage(R.string.launch_activity_dialog_message) .setCancelable(false) .setOnDismissListener((dialog) -> { waitForInteraction.countDown(); finishActivity(0); }) .setNegativeButton(R.string.close, null) .show()); try { waitForInteraction.await(10, TimeUnit.MINUTES); } catch (InterruptedException ignore) { } }); if (launched) { finishActivity(0); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/details/AppDetailsActivity.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.details; import android.annotation.UserIdInt; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.res.TypedArray; import android.net.Uri; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.os.UserHandleHidden; import android.view.MenuItem; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; import androidx.core.os.BundleCompat; import androidx.core.os.ParcelCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.lifecycle.ViewModelProvider; import androidx.viewpager2.adapter.FragmentStateAdapter; import androidx.viewpager2.widget.ViewPager2; import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayoutMediator; import java.util.Objects; import io.github.muntashirakon.AppManager.BaseActivity; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.apk.ApkSource; import io.github.muntashirakon.AppManager.details.info.AppInfoFragment; import io.github.muntashirakon.AppManager.intercept.IntentCompat; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.main.MainActivity; import io.github.muntashirakon.AppManager.misc.AdvancedSearchView; import io.github.muntashirakon.AppManager.self.SelfUriManager; import io.github.muntashirakon.AppManager.types.UserPackagePair; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; import io.github.muntashirakon.util.UiUtils; public class AppDetailsActivity extends BaseActivity { public static final String ALIAS_APP_INFO = "io.github.muntashirakon.AppManager.details.AppInfoActivity"; private static final String EXTRA_PACKAGE_NAME = "android.intent.extra.PACKAGE_NAME"; // Intent.EXTRA_PACKAGE_NAME private static final String EXTRA_APK_SOURCE = "src"; private static final String EXTRA_USER_HANDLE = "user"; private static final String EXTRA_BACK_TO_MAIN = "main"; @NonNull public static Intent getIntent(@NonNull Context context, @NonNull String packageName, @UserIdInt int userId) { Intent intent = new Intent(context, AppDetailsActivity.class); intent.putExtra(EXTRA_PACKAGE_NAME, packageName); intent.putExtra(EXTRA_USER_HANDLE, userId); return intent; } @NonNull public static Intent getIntent(@NonNull Context context, @NonNull String packageName, @UserIdInt int userId, boolean backToMainPage) { Intent intent = new Intent(context, AppDetailsActivity.class); intent.putExtra(EXTRA_PACKAGE_NAME, packageName); intent.putExtra(EXTRA_USER_HANDLE, userId); intent.putExtra(EXTRA_BACK_TO_MAIN, backToMainPage); return intent; } @NonNull public static Intent getIntent(@NonNull Context context, @NonNull ApkSource apkSource, boolean backToMainPage) { Intent intent = new Intent(context, AppDetailsActivity.class); IntentCompat.putWrappedParcelableExtra(intent, EXTRA_APK_SOURCE, apkSource); intent.putExtra(EXTRA_BACK_TO_MAIN, backToMainPage); return intent; } @NonNull public static Intent getIntent(@NonNull Context context, @NonNull Path apkPath, boolean backToMainPage) { return getIntent(context, apkPath.getUri(), apkPath.getType(), backToMainPage); } @NonNull public static Intent getIntent(@NonNull Context context, @NonNull Uri apkPath, @Nullable String mimeType, boolean backToMainPage) { Intent intent = new Intent(context, AppDetailsActivity.class); if (mimeType != null) { intent.setDataAndType(apkPath, mimeType); } else { intent.setData(apkPath); } intent.putExtra(EXTRA_BACK_TO_MAIN, backToMainPage); return intent; } public AppDetailsViewModel model; public AdvancedSearchView searchView; private ViewPager2 mViewPager; private TypedArray mTabTitleIds; private Fragment[] mTabFragments; private boolean mBackToMainPage; @Nullable private String mPackageName; @Nullable private ApkSource mApkSource; @Nullable private String mApkType; @UserIdInt private int mUserId; @Override protected void onAuthenticated(@Nullable Bundle savedInstanceState) { setContentView(R.layout.activity_app_details); setSupportActionBar(findViewById(R.id.toolbar)); setTitle("…"); model = new ViewModelProvider(this).get(AppDetailsViewModel.class); // Restore instance state SavedState ss = savedInstanceState != null ? BundleCompat.getParcelable(savedInstanceState, "ss", SavedState.class) : null; if (ss != null) { mBackToMainPage = ss.mBackToMainPage; mPackageName = ss.mPackageName; mApkSource = ss.mApkSource; mApkType = ss.mApkType; mUserId = ss.mUserId; } else { Intent intent = getIntent(); mBackToMainPage = intent.getBooleanExtra(EXTRA_BACK_TO_MAIN, mBackToMainPage); UserPackagePair pair = SelfUriManager.getUserPackagePairFromUri(intent.getData()); if (pair != null) { mPackageName = pair.getPackageName(); mApkSource = null; mUserId = pair.getUserId(); } else { mPackageName = getPackageNameFromExtras(intent); mApkSource = getApkSource(intent); mUserId = intent.getIntExtra(EXTRA_USER_HANDLE, UserHandleHidden.myUserId()); } mApkType = intent.getType(); } model.setUserId(mUserId); // Initialize tabs mTabTitleIds = getResources().obtainTypedArray(R.array.TAB_TITLES); mTabFragments = new Fragment[mTabTitleIds.length()]; if (mPackageName == null && mApkSource == null) { UIUtils.displayLongToast(R.string.empty_package_name); finish(); return; } // Set search ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayShowCustomEnabled(true); searchView = UIUtils.setupAdvancedSearchView(actionBar, null); } mViewPager = findViewById(R.id.pager); TabLayout tabLayout = findViewById(R.id.tab_layout); UiUtils.applyWindowInsetsAsPadding(tabLayout, false, true); final AlertDialog progressDialog = UIUtils.getProgressDialog(this, getText(R.string.loading), true); if (mPackageName == null) { // Display progress dialog only for external apk files progressDialog.show(); } // Set tabs mViewPager.setOffscreenPageLimit(4); mViewPager.setAdapter(new AppDetailsFragmentPagerAdapter(this)); new TabLayoutMediator(tabLayout, mViewPager, (tab, position) -> tab.setText(mTabTitleIds.getString(position))) .attach(); // Load package info (mPackageName != null ? model.setPackage(mPackageName) : model.setPackage(Objects.requireNonNull(mApkSource)) ).observe(this, packageInfo -> { progressDialog.dismiss(); if (packageInfo == null) { UIUtils.displayShortToast(R.string.failed_to_fetch_package_info); if (!isDestroyed()) { finish(); } return; } // Set title ApplicationInfo applicationInfo = packageInfo.applicationInfo; // Set title as the package label setTitle(applicationInfo.loadLabel(getPackageManager())); }); // Check for the existence of package model.getIsPackageExistLiveData().observe(this, isPackageExist -> { if (!isPackageExist) { if (!model.isExternalApk()) { UIUtils.displayShortToast(R.string.app_not_installed); } finish(); } }); // Set subtitle as the username if more than one user exists model.getUserInfo().observe(this, userInfo -> getSupportActionBar() .setSubtitle(getString(R.string.user_profile_with_id, userInfo.name, userInfo.id))); // Check for package changes model.isPackageChanged().observe(this, isPackageChanged -> { if (isPackageChanged && model.isPackageExist()) { loadTabs(); } }); } @Nullable private String getPackageNameFromExtras(@NonNull Intent intent) { String pkg = intent.getStringExtra(EXTRA_PACKAGE_NAME); if (pkg == null) { // Legacy argument, kept for compatibility pkg = intent.getStringExtra("pkg"); } if (pkg != null) { // Package name needs to be sanitized since it's also a file return Paths.sanitizeFilename(pkg); } return null; } @Nullable private ApkSource getApkSource(@NonNull Intent intent) { Uri uri = intent.getData(); if (uri != null) { return ApkSource.getApkSource(uri, intent.getType()); } return IntentCompat.getUnwrappedParcelableExtra(intent, EXTRA_APK_SOURCE, ApkSource.class); } static class SavedState implements Parcelable { private boolean mBackToMainPage; @Nullable private String mPackageName; @Nullable private ApkSource mApkSource; @Nullable private String mApkType; private int mUserId; protected SavedState() { } public SavedState(Parcel source) { mBackToMainPage = ParcelCompat.readBoolean(source); mPackageName = source.readString(); mApkSource = ParcelCompat.readParcelable(source, ApkSource.class.getClassLoader(), ApkSource.class); mApkType = source.readString(); mUserId = source.readInt(); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { ParcelCompat.writeBoolean(dest, mBackToMainPage); dest.writeString(mPackageName); dest.writeParcelable(mApkSource, flags); dest.writeString(mApkType); dest.writeInt(mUserId); } public static final Creator CREATOR = new ClassLoaderCreator() { @Override public SavedState createFromParcel(Parcel in, ClassLoader loader) { return new SavedState(in); } @Override public SavedState createFromParcel(Parcel in) { return new SavedState(in); } @Override public SavedState[] newArray(int size) { return new SavedState[size]; } }; } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { if (mApkSource != null || mPackageName != null) { SavedState ss = new SavedState(); ss.mBackToMainPage = mBackToMainPage; ss.mPackageName = mPackageName; ss.mApkSource = mApkSource; ss.mApkType = mApkType; ss.mUserId = mUserId; outState.putParcelable("ss", ss); } super.onSaveInstanceState(outState); } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { final int id = item.getItemId(); if (id == android.R.id.home) { if (mBackToMainPage) { Intent intent = new Intent(this, MainActivity.class); startActivity(intent); } finish(); return true; } return super.onOptionsItemSelected(item); } private void loadTabs() { @AppDetailsFragment.Property int id = mViewPager.getCurrentItem(); Log.d("ADA - " + mTabTitleIds.getText(id), "isPackageChanged called"); for (int i = 0; i < mTabTitleIds.length(); ++i) model.load(i); } // For tab layout private class AppDetailsFragmentPagerAdapter extends FragmentStateAdapter { AppDetailsFragmentPagerAdapter(@NonNull FragmentActivity fragmentActivity) { super(fragmentActivity); } @NonNull @Override public Fragment createFragment(@AppDetailsFragment.Property int position) { if (mTabFragments[position] != null) { return mTabFragments[position]; } switch (position) { case AppDetailsFragment.APP_INFO: return mTabFragments[position] = new AppInfoFragment(); case AppDetailsFragment.ACTIVITIES: case AppDetailsFragment.SERVICES: case AppDetailsFragment.RECEIVERS: case AppDetailsFragment.PROVIDERS: { AppDetailsComponentsFragment fragment = new AppDetailsComponentsFragment(); Bundle args = new Bundle(); args.putInt(AppDetailsFragment.ARG_TYPE, position); fragment.setArguments(args); return mTabFragments[position] = fragment; } case AppDetailsFragment.APP_OPS: case AppDetailsFragment.PERMISSIONS: case AppDetailsFragment.USES_PERMISSIONS: { AppDetailsPermissionsFragment fragment = new AppDetailsPermissionsFragment(); Bundle args = new Bundle(); args.putInt(AppDetailsFragment.ARG_TYPE, position); fragment.setArguments(args); return mTabFragments[position] = fragment; } case AppDetailsFragment.CONFIGURATIONS: case AppDetailsFragment.FEATURES: case AppDetailsFragment.SHARED_LIBRARIES: case AppDetailsFragment.SIGNATURES: { AppDetailsOtherFragment fragment = new AppDetailsOtherFragment(); Bundle args = new Bundle(); args.putInt(AppDetailsFragment.ARG_TYPE, position); fragment.setArguments(args); return mTabFragments[position] = fragment; } case AppDetailsFragment.OVERLAYS: AppDetailsOverlaysFragment fragment = new AppDetailsOverlaysFragment(); Bundle args = new Bundle(); args.putInt(AppDetailsFragment.ARG_TYPE, position); fragment.setArguments(args); return mTabFragments[position] = fragment; } return mTabFragments[position]; } @Override public int getItemCount() { return mTabTitleIds.length(); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/details/AppDetailsComponentsFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.details; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.PathPermission; import android.content.pm.ProviderInfo; import android.content.pm.ServiceInfo; import android.graphics.Color; import android.os.Bundle; import android.os.PatternMatcher; import android.os.UserHandleHidden; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.IntDef; import androidx.annotation.MainThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.appcompat.widget.PopupMenu; import com.google.android.material.button.MaterialButton; import com.google.android.material.card.MaterialCardView; import com.google.android.material.chip.Chip; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.materialswitch.MaterialSwitch; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.compat.ActivityManagerCompat; import io.github.muntashirakon.AppManager.compat.ManifestCompat; import io.github.muntashirakon.AppManager.details.struct.AppDetailsActivityItem; import io.github.muntashirakon.AppManager.details.struct.AppDetailsComponentItem; import io.github.muntashirakon.AppManager.details.struct.AppDetailsItem; import io.github.muntashirakon.AppManager.details.struct.AppDetailsServiceItem; import io.github.muntashirakon.AppManager.intercept.ActivityInterceptor; import io.github.muntashirakon.AppManager.rules.RuleType; import io.github.muntashirakon.AppManager.rules.compontents.ComponentUtils; import io.github.muntashirakon.AppManager.rules.struct.ComponentRule; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.self.imagecache.ImageLoader; import io.github.muntashirakon.AppManager.settings.FeatureController; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.shortcut.CreateShortcutDialogFragment; import io.github.muntashirakon.AppManager.types.UserPackagePair; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.AppManager.utils.Utils; import io.github.muntashirakon.AppManager.utils.appearance.ColorCodes; import io.github.muntashirakon.util.AdapterUtils; import io.github.muntashirakon.view.ProgressIndicatorCompat; import io.github.muntashirakon.widget.MaterialAlertView; import io.github.muntashirakon.widget.RecyclerView; public class AppDetailsComponentsFragment extends AppDetailsFragment { @IntDef(value = { ACTIVITIES, SERVICES, RECEIVERS, PROVIDERS, }) @Retention(RetentionPolicy.SOURCE) public @interface ComponentProperty { } private String mPackageName; private AppDetailsRecyclerAdapter mAdapter; private MenuItem mBlockingToggler; private boolean mIsExternalApk; @ComponentProperty private int mNeededProperty; private int mSortOrder; private String mSearchQuery; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); mNeededProperty = requireArguments().getInt(ARG_TYPE); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); emptyView.setText(getNotFoundString(mNeededProperty)); mAdapter = new AppDetailsRecyclerAdapter(); recyclerView.setAdapter(mAdapter); alertView.setEndIconOnClickListener(v -> alertView.hide()); int helpStringRes = R.string.rules_not_applied; alertView.setText(helpStringRes); alertView.setVisibility(View.GONE); if (viewModel == null) return; mSortOrder = viewModel.getSortOrder(mNeededProperty); mSearchQuery = viewModel.getSearchQuery(); mPackageName = viewModel.getPackageName(); viewModel.get(mNeededProperty).observe(getViewLifecycleOwner(), appDetailsItems -> { if (appDetailsItems != null && mAdapter != null && viewModel.isPackageExist()) { mPackageName = viewModel.getPackageName(); mIsExternalApk = viewModel.isExternalApk(); mAdapter.setDefaultList(appDetailsItems); } else ProgressIndicatorCompat.setVisibility(progressIndicator, false); }); viewModel.getRuleApplicationStatus().observe(getViewLifecycleOwner(), status -> { alertView.setAlertType(MaterialAlertView.ALERT_TYPE_WARN); if (status == AppDetailsViewModel.RULE_NOT_APPLIED) { alertView.show(); } else alertView.hide(); updateBlockMenuItem(status); }); } @Override public void onRefresh() { refreshDetails(); swipeRefresh.setRefreshing(false); } @Override public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) { if (viewModel != null && !viewModel.isExternalApk() && SelfPermissions.canModifyAppComponentStates( viewModel.getUserId(), viewModel.getPackageName(), viewModel.isTestOnlyApp())) { menuInflater.inflate(R.menu.fragment_app_details_components_actions, menu); mBlockingToggler = menu.findItem(R.id.action_toggle_blocking); } else menuInflater.inflate(R.menu.fragment_app_details_refresh_actions, menu); } @Override public void onPrepareMenu(@NonNull Menu menu) { if (viewModel == null || viewModel.isExternalApk()) { return; } MenuItem sortItem = menu.findItem(AppDetailsFragment.sSortMenuItemIdsMap[viewModel.getSortOrder(mNeededProperty)]); if (sortItem != null) { sortItem.setChecked(true); } Integer status = viewModel.getRuleApplicationStatus().getValue(); if (status != null) { updateBlockMenuItem(status); } } @Override public boolean onMenuItemSelected(@NonNull MenuItem item) { int id = item.getItemId(); if (id == R.id.action_refresh_details) { refreshDetails(); } else if (id == R.id.action_toggle_blocking) { // Components if (viewModel != null) { viewModel.applyRules(); } } else if (id == R.id.action_block_unblock_trackers) { // Components new MaterialAlertDialogBuilder(activity) .setTitle(R.string.block_unblock_trackers) .setMessage(R.string.choose_what_to_do) .setPositiveButton(R.string.block, (dialog, which) -> blockUnblockTrackers(true)) .setNegativeButton(R.string.cancel, null) .setNeutralButton(R.string.unblock, (dialog, which) -> blockUnblockTrackers(false)) .show(); } else if (id == R.id.action_sort_by_name) { // All setSortBy(AppDetailsFragment.SORT_BY_NAME); item.setChecked(true); } else if (id == R.id.action_sort_by_blocked_components) { // Components setSortBy(AppDetailsFragment.SORT_BY_BLOCKED); item.setChecked(true); } else if (id == R.id.action_sort_by_tracker_components) { // Components setSortBy(AppDetailsFragment.SORT_BY_TRACKERS); item.setChecked(true); } else return false; return true; } @Override public void onPause() { super.onPause(); if (viewModel != null) { mSortOrder = viewModel.getSortOrder(mNeededProperty); mSearchQuery = viewModel.getSearchQuery(); } } @Override public void onResume() { super.onResume(); if (activity.searchView != null) { if (!activity.searchView.isShown()) { activity.searchView.setVisibility(View.VISIBLE); } activity.searchView.setOnQueryTextListener(this); if (viewModel != null) { int sortOrder = viewModel.getSortOrder(mNeededProperty); String searchQuery = viewModel.getSearchQuery(); if (sortOrder != mSortOrder || !Objects.equals(searchQuery, mSearchQuery)) { viewModel.filterAndSortItems(mNeededProperty); } } } } @Override public boolean onQueryTextChange(String searchQuery, int type) { if (viewModel != null) { viewModel.setSearchQuery(searchQuery, type, mNeededProperty); } return true; } private void updateBlockMenuItem(int status) { if (mBlockingToggler != null) { switch (status) { case AppDetailsViewModel.RULE_APPLIED: mBlockingToggler.setVisible(!Prefs.Blocking.globalBlockingEnabled()); mBlockingToggler.setTitle(R.string.menu_remove_rules); break; case AppDetailsViewModel.RULE_NOT_APPLIED: mBlockingToggler.setVisible(!Prefs.Blocking.globalBlockingEnabled()); mBlockingToggler.setTitle(R.string.menu_apply_rules); break; case AppDetailsViewModel.RULE_NO_RULE: mBlockingToggler.setVisible(false); } } } private void blockUnblockTrackers(boolean block) { if (viewModel == null) return; // TODO: 19/3/23 Do it via ViewModel List userPackagePairs = Collections.singletonList(new UserPackagePair(mPackageName, UserHandleHidden.myUserId())); ThreadUtils.postOnBackgroundThread(() -> { List failedPkgList = block ? ComponentUtils.blockTrackingComponents(userPackagePairs) : ComponentUtils.unblockTrackingComponents(userPackagePairs); if (!failedPkgList.isEmpty()) { ThreadUtils.postOnMainThread(() -> UIUtils.displayShortToast(block ? R.string.failed_to_block_trackers : R.string.failed_to_unblock_trackers)); } else { ThreadUtils.postOnMainThread(() -> { UIUtils.displayShortToast(block ? R.string.trackers_blocked_successfully : R.string.trackers_unblocked_successfully); if (!isDetached()) { refreshDetails(); } }); } viewModel.setRuleApplicationStatus(); }); } private int getNotFoundString(@ComponentProperty int index) { switch (index) { case SERVICES: return R.string.no_service; case RECEIVERS: return R.string.no_receivers; case PROVIDERS: return R.string.no_providers; case ACTIVITIES: default: return R.string.no_activities; } } private void setSortBy(@SortOrder int sortBy) { ProgressIndicatorCompat.setVisibility(progressIndicator, true); if (viewModel == null) return; viewModel.setSortOrder(sortBy, mNeededProperty); } @MainThread private void refreshDetails() { if (viewModel == null || mIsExternalApk) return; ProgressIndicatorCompat.setVisibility(progressIndicator, true); viewModel.triggerPackageChange(); } private void applyRules(@NonNull AppDetailsComponentItem componentItem, @NonNull RuleType type, @NonNull @ComponentRule.ComponentStatus String componentStatus) { if (viewModel != null) { viewModel.updateRulesForComponent(componentItem, type, componentStatus); } } @UiThread private class AppDetailsRecyclerAdapter extends RecyclerView.Adapter { @NonNull private final List> mAdapterList; @ComponentProperty private int mRequestedProperty; @Nullable private String mConstraint; private int mUserId; private boolean mCanModifyComponentStates; private boolean mCanStartAnyActivity; private final int mBlockedIndicatorColor; private final int mBlockedExternallyIndicatorColor; private final int mTrackerIndicatorColor; private final int mRunningIndicatorColor; AppDetailsRecyclerAdapter() { mAdapterList = new ArrayList<>(); mBlockedIndicatorColor = ColorCodes.getComponentBlockedIndicatorColor(activity); mBlockedExternallyIndicatorColor = ColorCodes.getComponentExternallyBlockedIndicatorColor(activity); mTrackerIndicatorColor = ColorCodes.getComponentTrackerIndicatorColor(activity); mRunningIndicatorColor = ColorCodes.getComponentRunningIndicatorColor(activity); } @UiThread void setDefaultList(@NonNull List> list) { ThreadUtils.postOnBackgroundThread(() -> { mRequestedProperty = mNeededProperty; mCanStartAnyActivity = SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.START_ANY_ACTIVITY); if (viewModel != null) { mCanModifyComponentStates = !mIsExternalApk && SelfPermissions.canModifyAppComponentStates(mUserId, viewModel.getPackageName(), viewModel.isTestOnlyApp()); mConstraint = viewModel.getSearchQuery(); mUserId = viewModel.getUserId(); } else { mCanModifyComponentStates = false; mConstraint = null; mUserId = UserHandleHidden.myUserId(); } ThreadUtils.postOnMainThread(() -> { if (isDetached()) return; ProgressIndicatorCompat.setVisibility(progressIndicator, false); synchronized (mAdapterList) { AdapterUtils.notifyDataSetChanged(this, mAdapterList, list); } }); }); } /** * ViewHolder to use recycled views efficiently. Fields names are not expressive because we use * the same holder for any kind of view, and view are not all sames. */ class ViewHolder extends RecyclerView.ViewHolder { MaterialCardView itemView; TextView labelView; TextView nameView; TextView textView1; TextView textView2; TextView textView3; TextView textView4; TextView processNameView; ImageView imageView; Button shortcutBtn; MaterialButton launchBtn; MaterialSwitch toggleSwitch; TextView blockingMethod; Chip chipType; public ViewHolder(@NonNull View itemView) { super(itemView); this.itemView = (MaterialCardView) itemView; imageView = itemView.findViewById(R.id.icon); imageView.setContentDescription(itemView.getContext().getString(R.string.icon)); labelView = itemView.findViewById(R.id.label); nameView = itemView.findViewById(R.id.name); processNameView = itemView.findViewById(R.id.process_name); shortcutBtn = itemView.findViewById(R.id.edit_shortcut_btn); toggleSwitch = itemView.findViewById(R.id.toggle_button); blockingMethod = itemView.findViewById(R.id.method); chipType = itemView.findViewById(R.id.type); launchBtn = itemView.findViewById(R.id.launch); if (mRequestedProperty == ACTIVITIES) { textView1 = itemView.findViewById(R.id.taskAffinity); textView2 = itemView.findViewById(R.id.launchMode); textView3 = itemView.findViewById(R.id.orientation); textView4 = itemView.findViewById(R.id.softInput); } else if (mRequestedProperty == SERVICES) { textView1 = itemView.findViewById(R.id.orientation); itemView.findViewById(R.id.taskAffinity).setVisibility(View.GONE); itemView.findViewById(R.id.launchMode).setVisibility(View.GONE); itemView.findViewById(R.id.softInput).setVisibility(View.GONE); shortcutBtn.setVisibility(View.GONE); } else if (mRequestedProperty == RECEIVERS) { textView1 = itemView.findViewById(R.id.taskAffinity); textView2 = itemView.findViewById(R.id.launchMode); textView3 = itemView.findViewById(R.id.orientation); textView4 = itemView.findViewById(R.id.softInput); launchBtn.setVisibility(View.GONE); shortcutBtn.setVisibility(View.GONE); } else if (mRequestedProperty == PROVIDERS) { textView1 = itemView.findViewById(R.id.launchMode); textView2 = itemView.findViewById(R.id.orientation); textView3 = itemView.findViewById(R.id.softInput); textView4 = itemView.findViewById(R.id.taskAffinity); launchBtn.setVisibility(View.GONE); shortcutBtn.setVisibility(View.GONE); } } } @NonNull @Override public AppDetailsRecyclerAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_app_details_primary, parent, false); return new AppDetailsRecyclerAdapter.ViewHolder(view); } @Override public void onBindViewHolder(@NonNull AppDetailsRecyclerAdapter.ViewHolder holder, int position) { Context context = holder.itemView.getContext(); if (mRequestedProperty == SERVICES) { getServicesView(context, holder, position); } else if (mRequestedProperty == RECEIVERS) { getReceiverView(holder, position); } else if (mRequestedProperty == PROVIDERS) { getProviderView(holder, position); } else if (mRequestedProperty == ACTIVITIES) { getActivityView(holder, position); } } @Override public long getItemId(int position) { return position; } @Override public int getItemCount() { synchronized (mAdapterList) { return mAdapterList.size(); } } private void handleBlock(@NonNull ViewHolder holder, @NonNull AppDetailsComponentItem item, RuleType ruleType) { ComponentRule rule = item.getRule(); boolean isBlocked = item.isBlocked(); if (isBlocked) { Objects.requireNonNull(rule); holder.blockingMethod.setVisibility(View.VISIBLE); String method; if (rule.isIfw()) { if (item.isDisabled()) { method = "IFW+Dis"; } else method = "IFW"; } else { method = "Dis"; } holder.blockingMethod.setText(method); } else { holder.blockingMethod.setVisibility(View.GONE); } holder.toggleSwitch.setChecked(!isBlocked); holder.toggleSwitch.setVisibility(View.VISIBLE); holder.toggleSwitch.setOnClickListener(buttonView -> { String componentStatus = item.isBlocked() ? ComponentRule.COMPONENT_TO_BE_DEFAULTED : Prefs.Blocking.getDefaultBlockingMethod(); applyRules(item, ruleType, componentStatus); }); holder.toggleSwitch.setOnLongClickListener(v -> { PopupMenu popupMenu = new PopupMenu(activity, holder.toggleSwitch); Menu menu = popupMenu.getMenu(); boolean canBlockByIfw = !(item.item instanceof ProviderInfo) && SelfPermissions.canBlockByIFW(); popupMenu.inflate(R.menu.fragment_app_details_components_selection_actions); menu.findItem(R.id.action_ifw_and_disable).setEnabled(canBlockByIfw); menu.findItem(R.id.action_ifw).setEnabled(canBlockByIfw); popupMenu.setOnMenuItemClickListener(item1 -> { int id = item1.getItemId(); String componentStatus; if (id == R.id.action_ifw_and_disable) { componentStatus = ComponentRule.COMPONENT_TO_BE_BLOCKED_IFW_DISABLE; } else if (id == R.id.action_ifw) { componentStatus = ComponentRule.COMPONENT_TO_BE_BLOCKED_IFW; } else if (id == R.id.action_disable) { componentStatus = ComponentRule.COMPONENT_TO_BE_DISABLED; } else if (id == R.id.action_enable) { componentStatus = ComponentRule.COMPONENT_TO_BE_ENABLED; } else if (id == R.id.action_default) { componentStatus = ComponentRule.COMPONENT_TO_BE_DEFAULTED; } else { componentStatus = ComponentRule.COMPONENT_TO_BE_BLOCKED_IFW_DISABLE; } applyRules(item, ruleType, componentStatus); return true; }); popupMenu.show(); return true; }); } private void getActivityView(@NonNull ViewHolder holder, int index) { final AppDetailsActivityItem componentItem; synchronized (mAdapterList) { componentItem = (AppDetailsActivityItem) mAdapterList.get(index); } final ActivityInfo activityInfo = (ActivityInfo) componentItem.item; final String activityName = componentItem.name; final boolean isDisabled = !mIsExternalApk && componentItem.isDisabled(); // Background color: regular < tracker < disabled < blocked if (!mIsExternalApk && componentItem.isBlocked()) { holder.itemView.setStrokeColor(mBlockedIndicatorColor); } else if (isDisabled) { holder.itemView.setStrokeColor(mBlockedExternallyIndicatorColor); } else if (componentItem.isTracker()) { holder.itemView.setStrokeColor(mTrackerIndicatorColor); } else { holder.itemView.setStrokeColor(Color.TRANSPARENT); } if (componentItem.isTracker()) { holder.chipType.setText(R.string.tracker); holder.chipType.setVisibility(View.VISIBLE); } else holder.chipType.setVisibility(View.GONE); // Name if (mConstraint != null && activityName.toLowerCase(Locale.ROOT).contains(mConstraint)) { // Highlight searched query holder.nameView.setText(UIUtils.getHighlightedText(activityName, mConstraint, colorQueryStringHighlight)); } else { holder.nameView.setText(activityName.startsWith(mPackageName) ? activityName.replaceFirst(mPackageName, "") : activityName); } // Icon String tag = mPackageName + "_" + activityName; holder.imageView.setTag(tag); ImageLoader.getInstance().displayImage(tag, activityInfo, holder.imageView); // TaskAffinity holder.textView1.setText(String.format(Locale.ROOT, "%s: %s", getString(R.string.task_affinity), activityInfo.taskAffinity)); // LaunchMode holder.textView2.setText(String.format(Locale.ROOT, "%s: %s | %s: %s", getString(R.string.launch_mode), getString(Utils.getLaunchMode(activityInfo.launchMode)), getString(R.string.orientation), getString(Utils.getOrientationString(activityInfo.screenOrientation)))); // Orientation holder.textView3.setText(Utils.getActivitiesFlagsString(activityInfo.flags)); // SoftInput holder.textView4.setText(String.format(Locale.ROOT, "%s: %s | %s", getString(R.string.soft_input), Utils.getSoftInputString(activityInfo.softInputMode), (activityInfo.permission == null ? getString(R.string.require_no_permission) : activityInfo.permission))); // Label holder.labelView.setText(componentItem.label); // Process name String processName = activityInfo.processName; if (processName != null && !processName.equals(mPackageName)) { holder.processNameView.setVisibility(View.VISIBLE); holder.processNameView.setText(String.format(Locale.ROOT, "%s: %s", getString(R.string.process_name), processName)); } else holder.processNameView.setVisibility(View.GONE); boolean isExported = activityInfo.exported; if (componentItem.canLaunch || componentItem.canLaunchAssist) { holder.launchBtn.setOnClickListener(v -> { ComponentName cn = new ComponentName(mPackageName, activityName); if (componentItem.canLaunch) { Intent intent = new Intent(); intent.setComponent(cn); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); try { ActivityManagerCompat.startActivity(intent, mUserId); } catch (Throwable e) { UIUtils.displayLongToast(e.getLocalizedMessage()); } } else if (componentItem.canLaunchAssist) { ActivityManagerCompat.startActivityViaAssist(ContextUtils.getContext(), cn, () -> { CountDownLatch waitForInteraction = new CountDownLatch(1); ThreadUtils.postOnMainThread(() -> new MaterialAlertDialogBuilder(holder.itemView.getContext()) .setTitle(R.string.launch_activity_dialog_title) .setMessage(R.string.launch_activity_dialog_message) .setCancelable(false) .setOnDismissListener((dialog) -> waitForInteraction.countDown()) .setNegativeButton(R.string.close, null) .show()); try { waitForInteraction.await(10, TimeUnit.MINUTES); } catch (InterruptedException ignore) { } }); } }); if (FeatureController.isInterceptorEnabled()) { holder.launchBtn.setOnLongClickListener(v -> { boolean needRoot = mCanStartAnyActivity && (!isExported || !SelfPermissions.checkSelfOrRemotePermission(activityInfo.permission)); Intent intent = new Intent(activity, ActivityInterceptor.class); intent.putExtra(ActivityInterceptor.EXTRA_PACKAGE_NAME, mPackageName); intent.putExtra(ActivityInterceptor.EXTRA_CLASS_NAME, activityName); intent.putExtra(ActivityInterceptor.EXTRA_USER_HANDLE, mUserId); intent.putExtra(ActivityInterceptor.EXTRA_ROOT, needRoot); startActivity(intent); return true; }); } holder.shortcutBtn.setOnClickListener(v -> { PackageItemShortcutInfo shortcutInfo = new PackageItemShortcutInfo<>(activityInfo, ActivityInfo.class, mUserId, componentItem.canLaunchAssist); shortcutInfo.setName(componentItem.label); shortcutInfo.setIcon(UIUtils.getBitmapFromDrawable(activityInfo.loadIcon(packageManager))); CreateShortcutDialogFragment dialog = CreateShortcutDialogFragment.getInstance(shortcutInfo); dialog.show(getParentFragmentManager(), CreateShortcutDialogFragment.TAG); }); holder.shortcutBtn.setVisibility(View.VISIBLE); holder.launchBtn.setVisibility(View.VISIBLE); } else { holder.shortcutBtn.setVisibility(View.GONE); holder.launchBtn.setVisibility(View.GONE); } // Blocking if (mCanModifyComponentStates) { handleBlock(holder, componentItem, RuleType.ACTIVITY); } else { holder.toggleSwitch.setVisibility(View.GONE); holder.blockingMethod.setVisibility(View.GONE); } } private void getServicesView(@NonNull Context context, @NonNull ViewHolder holder, int index) { final AppDetailsServiceItem serviceItem; synchronized (mAdapterList) { serviceItem = (AppDetailsServiceItem) mAdapterList.get(index); } final ServiceInfo serviceInfo = (ServiceInfo) serviceItem.item; final boolean isDisabled = !mIsExternalApk && serviceItem.isDisabled(); // Background color: regular < tracker < disabled < blocked < running if (serviceItem.isRunning()) { holder.itemView.setStrokeColor(mRunningIndicatorColor); } else if (!mIsExternalApk && serviceItem.isBlocked()) { holder.itemView.setStrokeColor(mBlockedIndicatorColor); } else if (isDisabled) { holder.itemView.setStrokeColor(mBlockedExternallyIndicatorColor); } else if (serviceItem.isTracker()) { holder.itemView.setStrokeColor(mTrackerIndicatorColor); } else { holder.itemView.setStrokeColor(Color.TRANSPARENT); } if (serviceItem.isTracker()) { holder.chipType.setText(R.string.tracker); holder.chipType.setVisibility(View.VISIBLE); } else holder.chipType.setVisibility(View.GONE); // Label holder.labelView.setText(serviceItem.label); // Name if (mConstraint != null && serviceInfo.name.toLowerCase(Locale.ROOT).contains(mConstraint)) { // Highlight searched query holder.nameView.setText(UIUtils.getHighlightedText(serviceInfo.name, mConstraint, colorQueryStringHighlight)); } else { holder.nameView.setText(serviceInfo.name.startsWith(mPackageName) ? serviceInfo.name.replaceFirst(mPackageName, "") : serviceInfo.name); } // Icon String tag = mPackageName + "_" + serviceInfo.name; holder.imageView.setTag(tag); ImageLoader.getInstance().displayImage(tag, serviceInfo, holder.imageView); // Flags and Permission StringBuilder flagsAndPermission = new StringBuilder(Utils.getServiceFlagsString(serviceInfo.flags)); if (flagsAndPermission.length() != 0) { flagsAndPermission.append("\n"); } flagsAndPermission.append(serviceInfo.permission != null ? serviceInfo.permission : getString(R.string.require_no_permission)); holder.textView1.setText(flagsAndPermission); // Process name String processName = serviceInfo.processName; if (processName != null && !processName.equals(mPackageName)) { holder.processNameView.setVisibility(View.VISIBLE); holder.processNameView.setText(String.format(Locale.ROOT, "%s: %s", getString(R.string.process_name), processName)); } else holder.processNameView.setVisibility(View.GONE); if (serviceItem.canLaunch) { holder.launchBtn.setOnClickListener(v -> { Intent intent = new Intent(); intent.setClassName(mPackageName, serviceInfo.name); try { ActivityManagerCompat.startService(intent, mUserId, true); } catch (Throwable th) { th.printStackTrace(); UIUtils.displayShortToast(th.toString()); } }); holder.launchBtn.setVisibility(View.VISIBLE); } else { holder.launchBtn.setVisibility(View.GONE); } // Blocking if (mCanModifyComponentStates) { handleBlock(holder, serviceItem, RuleType.SERVICE); } else { holder.toggleSwitch.setVisibility(View.GONE); holder.blockingMethod.setVisibility(View.GONE); } } private void getReceiverView(@NonNull ViewHolder holder, int index) { final AppDetailsComponentItem componentItem; synchronized (mAdapterList) { componentItem = (AppDetailsComponentItem) mAdapterList.get(index); } final ActivityInfo activityInfo = (ActivityInfo) componentItem.item; // Background color: regular < tracker < disabled < blocked if (!mIsExternalApk && componentItem.isBlocked()) { holder.itemView.setStrokeColor(mBlockedIndicatorColor); } else if (!mIsExternalApk && componentItem.isDisabled()) { holder.itemView.setStrokeColor(mBlockedExternallyIndicatorColor); } else if (componentItem.isTracker()) { holder.itemView.setStrokeColor(mTrackerIndicatorColor); } else { holder.itemView.setStrokeColor(Color.TRANSPARENT); } if (componentItem.isTracker()) { holder.chipType.setText(R.string.tracker); holder.chipType.setVisibility(View.VISIBLE); } else holder.chipType.setVisibility(View.GONE); // Label holder.labelView.setText(componentItem.label); // Name if (mConstraint != null && activityInfo.name.toLowerCase(Locale.ROOT).contains(mConstraint)) { // Highlight searched query holder.nameView.setText(UIUtils.getHighlightedText(activityInfo.name, mConstraint, colorQueryStringHighlight)); } else { holder.nameView.setText(activityInfo.name.startsWith(mPackageName) ? activityInfo.name.replaceFirst(mPackageName, "") : activityInfo.name); } // Icon String tag = mPackageName + "_" + activityInfo.name; holder.imageView.setTag(tag); ImageLoader.getInstance().displayImage(tag, activityInfo, holder.imageView); // TaskAffinity holder.textView1.setText(String.format(Locale.ROOT, "%s: %s", getString(R.string.task_affinity), activityInfo.taskAffinity)); // LaunchMode holder.textView2.setText(String.format(Locale.ROOT, "%s: %s | %s: %s", getString(R.string.launch_mode), getString(Utils.getLaunchMode(activityInfo.launchMode)), getString(R.string.orientation), getString(Utils.getOrientationString(activityInfo.screenOrientation)))); // Orientation holder.textView3.setText(activityInfo.permission == null ? getString(R.string.require_no_permission) : activityInfo.permission); // SoftInput holder.textView4.setText(String.format(Locale.ROOT, "%s: %s", getString(R.string.soft_input), Utils.getSoftInputString(activityInfo.softInputMode))); // Process name String processName = activityInfo.processName; if (processName != null && !processName.equals(mPackageName)) { holder.processNameView.setVisibility(View.VISIBLE); holder.processNameView.setText(String.format(Locale.ROOT, "%s: %s", getString(R.string.process_name), processName)); } else holder.processNameView.setVisibility(View.GONE); // Blocking if (mCanModifyComponentStates) { handleBlock(holder, componentItem, RuleType.RECEIVER); } else { holder.toggleSwitch.setVisibility(View.GONE); holder.blockingMethod.setVisibility(View.GONE); } } private void getProviderView(@NonNull ViewHolder holder, int index) { final AppDetailsComponentItem componentItem; synchronized (mAdapterList) { componentItem = (AppDetailsComponentItem) mAdapterList.get(index); } final ProviderInfo providerInfo = (ProviderInfo) componentItem.item; final String providerName = providerInfo.name; // Background color: regular < tracker < disabled < blocked if (!mIsExternalApk && componentItem.isBlocked()) { holder.itemView.setStrokeColor(mBlockedIndicatorColor); } else if (!mIsExternalApk && componentItem.isDisabled()) { holder.itemView.setStrokeColor(mBlockedExternallyIndicatorColor); } else if (componentItem.isTracker()) { holder.itemView.setStrokeColor(mTrackerIndicatorColor); } else { holder.itemView.setStrokeColor(Color.TRANSPARENT); } if (componentItem.isTracker()) { holder.chipType.setText(R.string.tracker); holder.chipType.setVisibility(View.VISIBLE); } else holder.chipType.setVisibility(View.GONE); // Label holder.labelView.setText(componentItem.label); // Icon String tag = mPackageName + "_" + providerName; holder.imageView.setTag(tag); ImageLoader.getInstance().displayImage(tag, providerInfo, holder.imageView); // Uri permission holder.textView1.setText(String.format(Locale.ROOT, "%s: %s", getString(R.string.grant_uri_permission), providerInfo.grantUriPermissions)); // Path permissions PathPermission[] pathPermissions = providerInfo.pathPermissions; String finalString; if (pathPermissions != null) { StringBuilder builder = new StringBuilder(); String read = getString(R.string.read); String write = getString(R.string.write); for (PathPermission permission : pathPermissions) { builder.append(read).append(": ").append(permission.getReadPermission()); builder.append("/"); builder.append(write).append(": ").append(permission.getWritePermission()); builder.append(", "); } Utils.checkStringBuilderEnd(builder); finalString = builder.toString(); } else finalString = "null"; holder.textView2.setText(String.format(Locale.ROOT, "%s: %s", getString(R.string.path_permissions), finalString)); // +"\n"+providerInfo.readPermission +"\n"+providerInfo.writePermission); // Pattern matchers PatternMatcher[] patternMatchers = providerInfo.uriPermissionPatterns; String finalString1; if (patternMatchers != null) { StringBuilder builder = new StringBuilder(); for (PatternMatcher patternMatcher : patternMatchers) { builder.append(patternMatcher.toString()); builder.append(", "); } Utils.checkStringBuilderEnd(builder); finalString1 = builder.toString(); } else finalString1 = "null"; holder.textView3.setText(String.format(Locale.ROOT, "%s: %s", getString(R.string.patterns_allowed), finalString1)); // Authority holder.textView4.setText(String.format(Locale.ROOT, "%s: %s", getString(R.string.authority), providerInfo.authority)); // Name if (mConstraint != null && providerName.toLowerCase(Locale.ROOT).contains(mConstraint)) { // Highlight searched query holder.nameView.setText(UIUtils.getHighlightedText(providerName, mConstraint, colorQueryStringHighlight)); } else { holder.nameView.setText(providerName.startsWith(mPackageName) ? providerName.replaceFirst(mPackageName, "") : providerName); } // Process name String processName = providerInfo.processName; if (processName != null && !processName.equals(mPackageName)) { holder.processNameView.setVisibility(View.VISIBLE); holder.processNameView.setText(String.format(Locale.ROOT, "%s: %s", getString(R.string.process_name), processName)); } else holder.processNameView.setVisibility(View.GONE); // Blocking if (mCanModifyComponentStates) { handleBlock(holder, componentItem, RuleType.PROVIDER); } else { holder.toggleSwitch.setVisibility(View.GONE); holder.blockingMethod.setVisibility(View.GONE); } } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/details/AppDetailsFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.details; import android.content.pm.PackageManager; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.CallSuper; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.view.MenuProvider; import androidx.fragment.app.Fragment; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.ViewModelProvider; import com.google.android.material.progressindicator.LinearProgressIndicator; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.misc.AdvancedSearchView; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.AppManager.utils.appearance.ColorCodes; import io.github.muntashirakon.view.ProgressIndicatorCompat; import io.github.muntashirakon.widget.MaterialAlertView; import io.github.muntashirakon.widget.RecyclerView; import io.github.muntashirakon.widget.SwipeRefreshLayout; public abstract class AppDetailsFragment extends Fragment implements AdvancedSearchView.OnQueryTextListener, SwipeRefreshLayout.OnRefreshListener, MenuProvider { @IntDef(value = { APP_INFO, ACTIVITIES, SERVICES, RECEIVERS, PROVIDERS, APP_OPS, USES_PERMISSIONS, PERMISSIONS, FEATURES, CONFIGURATIONS, SIGNATURES, SHARED_LIBRARIES, OVERLAYS, }) @Retention(RetentionPolicy.SOURCE) public @interface Property { } public static final int APP_INFO = 0; public static final int ACTIVITIES = 1; public static final int SERVICES = 2; public static final int RECEIVERS = 3; public static final int PROVIDERS = 4; public static final int APP_OPS = 5; public static final int USES_PERMISSIONS = 6; public static final int PERMISSIONS = 7; public static final int FEATURES = 8; public static final int CONFIGURATIONS = 9; public static final int SIGNATURES = 10; public static final int SHARED_LIBRARIES = 11; public static final int OVERLAYS = 12; @IntDef(value = { SORT_BY_NAME, SORT_BY_BLOCKED, SORT_BY_TRACKERS, SORT_BY_APP_OP_VALUES, SORT_BY_DENIED_APP_OPS, SORT_BY_DANGEROUS_PERMS, SORT_BY_DENIED_PERMS, SORT_BY_PRIORITY, }) @Retention(RetentionPolicy.SOURCE) public @interface SortOrder { } public static final int SORT_BY_NAME = 0; public static final int SORT_BY_BLOCKED = 1; public static final int SORT_BY_TRACKERS = 2; public static final int SORT_BY_APP_OP_VALUES = 3; public static final int SORT_BY_DENIED_APP_OPS = 4; public static final int SORT_BY_DANGEROUS_PERMS = 5; public static final int SORT_BY_DENIED_PERMS = 6; public static final int SORT_BY_PRIORITY = 7; public static final int[] sSortMenuItemIdsMap = { R.id.action_sort_by_name, R.id.action_sort_by_blocked_components, R.id.action_sort_by_tracker_components, R.id.action_sort_by_app_ops_values, R.id.action_sort_by_denied_app_ops, R.id.action_sort_by_dangerous_permissions, R.id.action_sort_by_denied_permissions, R.id.action_sort_by_priority}; public static final String ARG_TYPE = "type"; protected PackageManager packageManager; protected AppDetailsActivity activity; protected MaterialAlertView alertView; protected SwipeRefreshLayout swipeRefresh; protected LinearProgressIndicator progressIndicator; protected RecyclerView recyclerView; protected TextView emptyView; @Nullable protected AppDetailsViewModel viewModel; protected int colorQueryStringHighlight; @CallSuper @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); activity = (AppDetailsActivity) requireActivity(); viewModel = new ViewModelProvider(activity).get(AppDetailsViewModel.class); packageManager = activity.getPackageManager(); colorQueryStringHighlight = ColorCodes.getQueryStringHighlightColor(activity); } @Nullable @Override public final View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.pager_app_details, container, false); } @CallSuper @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { // Swipe refresh swipeRefresh = view.findViewById(R.id.swipe_refresh); swipeRefresh.setOnRefreshListener(this); recyclerView = view.findViewById(R.id.scrollView); recyclerView.setLayoutManager(UIUtils.getGridLayoutAt450Dp(activity)); emptyView = view.findViewById(android.R.id.empty); recyclerView.setEmptyView(emptyView); progressIndicator = view.findViewById(R.id.progress_linear); progressIndicator.setVisibilityAfterHide(View.GONE); ProgressIndicatorCompat.setVisibility(progressIndicator, true); alertView = view.findViewById(R.id.alert_text); alertView.setEndIconMode(MaterialAlertView.END_ICON_CUSTOM); alertView.setEndIconDrawable(com.google.android.material.R.drawable.mtrl_ic_cancel); alertView.setEndIconContentDescription(R.string.close); swipeRefresh.setOnChildScrollUpCallback((parent, child) -> recyclerView.canScrollVertically(-1)); requireActivity().addMenuProvider(this, getViewLifecycleOwner(), Lifecycle.State.RESUMED); } @CallSuper @Override public void onResume() { super.onResume(); swipeRefresh.setEnabled(true); } @CallSuper @Override public void onPause() { super.onPause(); swipeRefresh.setEnabled(false); } @CallSuper @Override public void onDestroyView() { swipeRefresh.setRefreshing(false); swipeRefresh.clearAnimation(); super.onDestroyView(); } @Override public boolean onQueryTextSubmit(String query, int type) { return false; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/details/AppDetailsOtherFragment.java ================================================ // SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.details; import static io.github.muntashirakon.AppManager.utils.Utils.openAsFolderInFM; import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; import android.content.pm.ConfigurationInfo; import android.content.pm.FeatureInfo; import android.content.pm.PackageInfo; import android.graphics.Color; import android.os.Build; import android.os.Bundle; import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.text.format.Formatter; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.core.content.ContextCompat; import androidx.core.content.pm.PackageInfoCompat; import com.google.android.material.button.MaterialButton; import com.google.android.material.card.MaterialCardView; import com.google.android.material.chip.Chip; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Objects; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.details.struct.AppDetailsFeatureItem; import io.github.muntashirakon.AppManager.details.struct.AppDetailsItem; import io.github.muntashirakon.AppManager.details.struct.AppDetailsLibraryItem; import io.github.muntashirakon.AppManager.utils.PackageUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.AppManager.utils.Utils; import io.github.muntashirakon.io.Paths; import io.github.muntashirakon.util.AdapterUtils; import io.github.muntashirakon.util.LocalizedString; import io.github.muntashirakon.view.ProgressIndicatorCompat; import io.github.muntashirakon.widget.RecyclerView; public class AppDetailsOtherFragment extends AppDetailsFragment { @IntDef(value = { FEATURES, CONFIGURATIONS, SIGNATURES, SHARED_LIBRARIES }) @Retention(RetentionPolicy.SOURCE) public @interface OtherProperty { } private AppDetailsRecyclerAdapter mAdapter; private boolean mIsExternalApk; @OtherProperty private int mNeededProperty; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); mNeededProperty = requireArguments().getInt(ARG_TYPE); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); emptyView.setText(getNotFoundString(mNeededProperty)); mAdapter = new AppDetailsRecyclerAdapter(); recyclerView.setAdapter(mAdapter); alertView.setVisibility(View.GONE); if (viewModel == null) return; viewModel.get(mNeededProperty).observe(getViewLifecycleOwner(), appDetailsItems -> { if (appDetailsItems != null && mAdapter != null && viewModel.isPackageExist()) { mIsExternalApk = viewModel.isExternalApk(); mAdapter.setDefaultList(appDetailsItems); } else ProgressIndicatorCompat.setVisibility(progressIndicator, false); }); } @Override public void onRefresh() { refreshDetails(); swipeRefresh.setRefreshing(false); } @Override public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { inflater.inflate(R.menu.fragment_app_details_refresh_actions, menu); } @Override public boolean onMenuItemSelected(@NonNull MenuItem item) { int id = item.getItemId(); if (id == R.id.action_refresh_details) { refreshDetails(); return true; } return false; } @Override public void onResume() { super.onResume(); if (activity.searchView != null) { activity.searchView.setVisibility(View.GONE); } } @Override public boolean onQueryTextChange(String searchQuery, int type) { if (viewModel != null) { viewModel.setSearchQuery(searchQuery, type, mNeededProperty); } return true; } private void refreshDetails() { if (viewModel == null || mIsExternalApk) return; ProgressIndicatorCompat.setVisibility(progressIndicator, true); viewModel.triggerPackageChange(); } /** * Return corresponding section's array */ private int getNotFoundString(@OtherProperty int index) { switch (index) { case FEATURES: return R.string.no_feature; case CONFIGURATIONS: return R.string.no_configurations; case SIGNATURES: return R.string.app_signing_no_signatures; case SHARED_LIBRARIES: return R.string.no_shared_libs; default: return 0; } } @UiThread private class AppDetailsRecyclerAdapter extends RecyclerView.Adapter { @NonNull private final List> mAdapterList; @OtherProperty private int mRequestedProperty; AppDetailsRecyclerAdapter() { mAdapterList = new ArrayList<>(); } @UiThread void setDefaultList(@NonNull List> list) { ThreadUtils.postOnBackgroundThread(() -> { mRequestedProperty = mNeededProperty; ThreadUtils.postOnMainThread(() -> { if (isDetached()) return; ProgressIndicatorCompat.setVisibility(progressIndicator, false); synchronized (mAdapterList) { AdapterUtils.notifyDataSetChanged(this, mAdapterList, list); } }); }); } /** * ViewHolder to use recycled views efficiently. Fields names are not expressive because we use * the same holder for any kind of view, and view are not all sames. */ class ViewHolder extends RecyclerView.ViewHolder { MaterialCardView itemView; TextView textView1; TextView textView2; TextView textView3; TextView textView4; TextView textView5; MaterialButton launchBtn; Chip chipType; public ViewHolder(@NonNull View itemView) { super(itemView); this.itemView = (MaterialCardView) itemView; switch (mRequestedProperty) { case FEATURES: textView1 = itemView.findViewById(R.id.name); textView3 = itemView.findViewById(R.id.gles_ver); break; case CONFIGURATIONS: textView1 = itemView.findViewById(R.id.reqgles); textView2 = itemView.findViewById(R.id.reqfea); textView3 = itemView.findViewById(R.id.reqkey); textView4 = itemView.findViewById(R.id.reqnav); textView5 = itemView.findViewById(R.id.reqtouch); break; case SHARED_LIBRARIES: textView1 = itemView.findViewById(R.id.item_title); textView2 = itemView.findViewById(R.id.item_subtitle); launchBtn = itemView.findViewById(R.id.item_open); chipType = itemView.findViewById(R.id.lib_type); textView1.setTextIsSelectable(true); textView2.setTextIsSelectable(true); break; case SIGNATURES: textView1 = itemView.findViewById(R.id.checksum_description); default: break; } } } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { @SuppressLint("InflateParams") final View view; switch (mRequestedProperty) { default: view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_app_details_primary, parent, false); break; case FEATURES: view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_app_details_secondary, parent, false); break; case CONFIGURATIONS: view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_app_details_tertiary, parent, false); break; case SIGNATURES: view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_app_details_signature, parent, false); break; case SHARED_LIBRARIES: view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_shared_lib, parent, false); break; } return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { Context context = holder.itemView.getContext(); switch (mRequestedProperty) { case FEATURES: getFeaturesView(context, holder, position); break; case CONFIGURATIONS: getConfigurationView(holder, position); break; case SIGNATURES: getSignatureView(context, holder, position); break; case SHARED_LIBRARIES: getSharedLibsView(context, holder, position); break; default: break; } } @Override public long getItemId(int position) { return position; } @Override public int getItemCount() { synchronized (mAdapterList) { return mAdapterList.size(); } } private void getSharedLibsView(@NonNull Context context, @NonNull ViewHolder holder, int index) { AppDetailsLibraryItem item; synchronized (mAdapterList) { item = (AppDetailsLibraryItem) mAdapterList.get(index); } holder.textView1.setText(item.name); holder.chipType.setText(item.type); switch (item.type) { case "APK": { PackageInfo packageInfo = (PackageInfo) item.item; StringBuilder sb = new StringBuilder() .append(packageInfo.packageName) .append("\n"); if (item.path != null) { sb.append(Formatter.formatFileSize(context, item.size)).append(", "); } sb.append(getString(R.string.version_name_with_code, packageInfo.versionName, PackageInfoCompat.getLongVersionCode(packageInfo))); if (item.path != null) { sb.append("\n").append(item.path); holder.launchBtn.setVisibility(View.VISIBLE); holder.launchBtn.setIconResource(io.github.muntashirakon.ui.R.drawable.ic_information); holder.launchBtn.setContentDescription(holder.itemView.getContext().getString(R.string.app_info)); holder.launchBtn.setOnClickListener(v -> { Intent intent = AppDetailsActivity.getIntent(context, Paths.get(item.path), false); startActivity(intent); }); } else holder.launchBtn.setVisibility(View.GONE); holder.textView2.setText(sb); break; } case "⚠️": case "SHARED": case "EXEC": case "SO": { if (item.path == null) { // Native lib holder.textView2.setText(((LocalizedString) item.item).toLocalizedString(context)); holder.launchBtn.setVisibility(View.GONE); break; } // else shared lib, fallthrough } case "JAR": { StringBuilder sb = new StringBuilder(Formatter.formatFileSize(context, item.size)) .append("\n").append(item.path); holder.textView2.setText(sb); holder.launchBtn.setVisibility(View.VISIBLE); holder.launchBtn.setIconResource(R.drawable.ic_open_in_new); holder.launchBtn.setContentDescription(holder.itemView.getContext().getString(R.string.open)); holder.launchBtn.setOnClickListener(openAsFolderInFM(context, item.path.getParent())); break; } } holder.itemView.setStrokeColor(Color.TRANSPARENT); } private void getFeaturesView(@NonNull Context context, @NonNull ViewHolder holder, int index) { MaterialCardView view = holder.itemView; final AppDetailsFeatureItem item; synchronized (mAdapterList) { item = (AppDetailsFeatureItem) mAdapterList.get(index); } FeatureInfo featureInfo = item.item; // Set background if (item.required && !item.available) { view.setStrokeColor(ContextCompat.getColor(context, io.github.muntashirakon.ui.R.color.red)); } else if (!item.available) { view.setStrokeColor(ContextCompat.getColor(context, io.github.muntashirakon.ui.R.color.disabled_user)); } else { view.setStrokeColor(Color.TRANSPARENT); } // Set feature name if (featureInfo.name == null) { // OpenGL ES if (featureInfo.reqGlEsVersion == FeatureInfo.GL_ES_VERSION_UNDEFINED) { holder.textView1.setText(item.name); } else { // GL ES version holder.textView1.setText(String.format(Locale.ROOT, "%s %s", getString(R.string.gles_version), Utils.getGlEsVersion(featureInfo.reqGlEsVersion))); } holder.textView3.setVisibility(View.GONE); return; } else holder.textView1.setText(item.name); // Feature version: 0 means any version if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && featureInfo.version != 0) { holder.textView3.setVisibility(View.VISIBLE); holder.textView3.setText(getString(R.string.minimum_version, featureInfo.version)); } else holder.textView3.setVisibility(View.GONE); } private void getConfigurationView(@NonNull ViewHolder holder, int index) { MaterialCardView view = holder.itemView; final ConfigurationInfo configurationInfo; synchronized (mAdapterList) { configurationInfo = (ConfigurationInfo) mAdapterList.get(index).item; } view.setStrokeColor(Color.TRANSPARENT); // GL ES version holder.textView1.setText(String.format(Locale.ROOT, "%s %s", getString(R.string.gles_version), Utils.getGlEsVersion(configurationInfo.reqGlEsVersion))); // Flag & others holder.textView2.setText(String.format(Locale.ROOT, "%s: %s", getString(R.string.input_features), Utils.getInputFeaturesString(configurationInfo.reqInputFeatures))); holder.textView3.setText(String.format(Locale.ROOT, "%s: %s", getString(R.string.keyboard_type), getString(Utils.getKeyboardType(configurationInfo.reqKeyboardType)))); holder.textView4.setText(String.format(Locale.ROOT, "%s: %s", getString(R.string.navigation), getString(Utils.getNavigation(configurationInfo.reqNavigation)))); holder.textView5.setText(String.format(Locale.ROOT, "%s: %s", getString(R.string.touchscreen), getString(Utils.getTouchScreen(configurationInfo.reqTouchScreen)))); } private void getSignatureView(@NonNull Context context, @NonNull ViewHolder holder, int index) { TextView textView = holder.textView1; AppDetailsItem item; synchronized (mAdapterList) { item = mAdapterList.get(index); } final X509Certificate signature = (X509Certificate) item.item; final SpannableStringBuilder builder = new SpannableStringBuilder(); if (index == 0) { // Display verifier info builder.append(PackageUtils.getApkVerifierInfo(Objects.requireNonNull(viewModel).getApkVerifierResult(), context)); } if (!TextUtils.isEmpty(item.name)) { builder.append(UIUtils.getTitleText(context, item.name)).append("\n"); } try { builder.append(PackageUtils.getSigningCertificateInfo(context, signature)); } catch (CertificateEncodingException ignore) { } textView.setText(builder); textView.setTextIsSelectable(true); holder.itemView.setStrokeColor(Color.TRANSPARENT); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/details/AppDetailsOverlaysFragment.java ================================================ package io.github.muntashirakon.AppManager.details; import android.content.om.IOverlayManager; import android.graphics.Color; import android.os.Build; import android.os.Bundle; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.UiThread; import com.google.android.material.card.MaterialCardView; import com.google.android.material.materialswitch.MaterialSwitch; import java.util.ArrayList; import java.util.List; import java.util.Locale; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.compat.ManifestCompat; import io.github.muntashirakon.AppManager.compat.OverlayManagerCompact; import io.github.muntashirakon.AppManager.details.struct.AppDetailsItem; import io.github.muntashirakon.AppManager.details.struct.AppDetailsOverlayItem; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.self.pref.TipsPrefs; import io.github.muntashirakon.AppManager.utils.LangUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.AppManager.utils.appearance.ColorCodes; import io.github.muntashirakon.util.AdapterUtils; import io.github.muntashirakon.view.ProgressIndicatorCompat; import io.github.muntashirakon.widget.MaterialAlertView; import io.github.muntashirakon.widget.RecyclerView; public class AppDetailsOverlaysFragment extends AppDetailsFragment { private static final String TAG = AppDetailsOverlaysFragment.class.getSimpleName(); private AppDetailsRecyclerAdapter mAdapter; private IOverlayManager overlayManager; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.CHANGE_OVERLAY_PACKAGES)) { overlayManager = OverlayManagerCompact.getOverlayManager(); } } @Override public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) { menuInflater.inflate(R.menu.fragment_app_details_overlay_actions, menu); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); String emptyStringText; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { emptyStringText = getString(R.string.overlay_sdk_version_too_low); } else if (!SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.CHANGE_OVERLAY_PACKAGES)) { emptyStringText = getString(R.string.no_overlay_permission); } else { emptyStringText = getString(R.string.no_overlays); } emptyView.setText(emptyStringText); mAdapter = new AppDetailsRecyclerAdapter(); recyclerView.setAdapter(mAdapter); alertView.setEndIconOnClickListener(v -> { alertView.hide(); TipsPrefs.getInstance().setDisplayInOverlaysTab(false); }); if (TipsPrefs.getInstance().displayInOverlaysTab()) { alertView.postDelayed(() -> alertView.hide(), 15_000); } else { alertView.setVisibility(View.GONE); } if (viewModel == null) return; viewModel.get(AppDetailsFragment.OVERLAYS).observe(getViewLifecycleOwner(), appDetailsItems -> { if (appDetailsItems != null && mAdapter != null && viewModel.isPackageExist()) { mAdapter.setDefaultList(appDetailsItems); } else ProgressIndicatorCompat.setVisibility(progressIndicator, false); }); viewModel.getRuleApplicationStatus().observe(getViewLifecycleOwner(), status -> { alertView.setAlertType(MaterialAlertView.ALERT_TYPE_WARN); if (status == AppDetailsViewModel.RULE_NOT_APPLIED) { alertView.show(); } else alertView.hide(); }); } @Override public boolean onMenuItemSelected(@NonNull MenuItem menuItem) { int id = menuItem.getItemId(); if (id == R.id.action_refresh_details) { refreshDetails(); } else if (id == R.id.action_sort_by_name) { setSortBy(SORT_BY_NAME); menuItem.setChecked(true); } else if (id == R.id.action_sort_by_priority) { setSortBy(SORT_BY_PRIORITY); menuItem.setChecked(true); } else return false; return true; } private void setSortBy(@SortOrder int sortBy) { ProgressIndicatorCompat.setVisibility(progressIndicator, true); if (viewModel == null) return; viewModel.setSortOrder(sortBy, OVERLAYS); } private void refreshDetails() { if (viewModel == null || overlayManager == null) return; ProgressIndicatorCompat.setVisibility(progressIndicator, true); viewModel.triggerPackageChange(); } @Override public void onRefresh() { swipeRefresh.setRefreshing(false); } @Override public boolean onQueryTextChange(String newText, int type) { if (viewModel != null) { viewModel.setSearchQuery(newText, type, OVERLAYS); } return true; } private class AppDetailsRecyclerAdapter extends RecyclerView.Adapter { @NonNull private final List> mAdapterList; @Nullable private String mConstraint; @UiThread void setDefaultList(List> list) { ThreadUtils.postOnBackgroundThread(() -> { mConstraint = viewModel == null ? null : viewModel.getSearchQuery(); ThreadUtils.postOnMainThread(() -> { if (isDetached()) return; ProgressIndicatorCompat.setVisibility(progressIndicator, false); synchronized (mAdapterList) { AdapterUtils.notifyDataSetChanged(this, mAdapterList, list); } }); }); } AppDetailsRecyclerAdapter() { mAdapterList = new ArrayList<>(); } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) { return new ViewHolder( LayoutInflater.from(viewGroup.getContext()) .inflate(R.layout.item_app_details_overlay, viewGroup, false) ); } @Override @RequiresApi(Build.VERSION_CODES.O) public void onBindViewHolder(@NonNull ViewHolder holder, int index) { AppDetailsOverlayItem overlayItem; synchronized (mAdapterList) { overlayItem = (AppDetailsOverlayItem) mAdapterList.get(index); } String overlayName = overlayItem.name; if (mConstraint != null && overlayName.toLowerCase(Locale.ROOT).contains(mConstraint)) { // Highlight searched query holder.overlayName.setText(UIUtils.getHighlightedText(overlayName, mConstraint, colorQueryStringHighlight)); } else holder.overlayName.setText(overlayName); holder.packageName.setText(overlayItem.getPackageName()); if (overlayItem.getCategory() != null) { holder.overlayCategory.setVisibility(View.VISIBLE); String category = getString(R.string.overlay_category) + LangUtils.getSeparatorString() + overlayItem.getCategory(); holder.overlayCategory.setText(category); } else { holder.overlayCategory.setVisibility(View.GONE); } holder.toggleSwitch.setEnabled(overlayItem.isMutable()); holder.toggleSwitch.setClickable(true); holder.toggleSwitch.setChecked(overlayItem.isEnabled()); StringBuilder sb = new StringBuilder(getString(R.string.state)) .append(LangUtils.getSeparatorString()) .append(overlayItem.getReadableState()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { sb.append(" | ") .append(getString(R.string.priority)) .append(LangUtils.getSeparatorString()) .append(overlayItem.getPriority()); } holder.overlayState.setText(sb); holder.itemView.setClickable(false); if (overlayItem.isMutable()) { holder.toggleSwitch.setClickable(true); holder.toggleSwitch.setOnClickListener((v) -> ThreadUtils.postOnBackgroundThread(() -> { try { // TODO: 2/18/25 Move to ViewModel if (overlayItem.setEnabled(overlayManager, !overlayItem.isEnabled())) { ThreadUtils.postOnMainThread(() -> notifyItemChanged(index, AdapterUtils.STUB)); } else throw new Exception("Error Changing Overlay State " + overlayItem); } catch (Exception e) { Log.e(TAG, "Couldn't Change Overlay State", e); ThreadUtils.postOnMainThread(() -> UIUtils.displayShortToast(R.string.failed)); } })); holder.toggleSwitch.setVisibility(View.VISIBLE); } else { holder.toggleSwitch.setOnClickListener(null); holder.toggleSwitch.setClickable(false); holder.toggleSwitch.setVisibility(View.GONE); } if (overlayItem.isFabricated()) { holder.itemView.setStrokeColor(ColorCodes.getPermissionDangerousIndicatorColor(requireContext())); } holder.itemView.setStrokeColor(Color.TRANSPARENT); } @Override public long getItemId(int position) { return position; } @Override public int getItemCount() { synchronized (mAdapterList) { return mAdapterList.size(); } } class ViewHolder extends RecyclerView.ViewHolder { MaterialCardView itemView; TextView overlayName; TextView packageName; TextView overlayCategory; TextView overlayState; MaterialSwitch toggleSwitch; public ViewHolder(@NonNull View itemView) { super(itemView); this.itemView = (MaterialCardView) itemView; overlayName = itemView.findViewById(R.id.overlay_name); packageName = itemView.findViewById(R.id.overlay_package_name); overlayCategory = itemView.findViewById(R.id.overlay_category); overlayState = itemView.findViewById(R.id.overlay_state); toggleSwitch = itemView.findViewById(R.id.overlay_toggle_btn); } } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/details/AppDetailsPermissionsFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.details; import static io.github.muntashirakon.AppManager.utils.PackageUtils.getAppOpModeNames; import static io.github.muntashirakon.AppManager.utils.PackageUtils.getAppOpNames; import android.annotation.SuppressLint; import android.content.Context; import android.content.pm.PermissionInfo; import android.graphics.Color; import android.os.Bundle; import android.text.SpannableStringBuilder; import android.util.SparseArray; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import com.google.android.material.button.MaterialButton; import com.google.android.material.card.MaterialCardView; import com.google.android.material.chip.Chip; import com.google.android.material.materialswitch.MaterialSwitch; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.Objects; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.compat.AppOpsManagerCompat; import io.github.muntashirakon.AppManager.compat.ManifestCompat; import io.github.muntashirakon.AppManager.compat.PermissionCompat; import io.github.muntashirakon.AppManager.details.struct.AppDetailsAppOpItem; import io.github.muntashirakon.AppManager.details.struct.AppDetailsDefinedPermissionItem; import io.github.muntashirakon.AppManager.details.struct.AppDetailsItem; import io.github.muntashirakon.AppManager.details.struct.AppDetailsPermissionItem; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.self.imagecache.ImageLoader; import io.github.muntashirakon.AppManager.self.pref.TipsPrefs; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.utils.DateUtils; import io.github.muntashirakon.AppManager.utils.ExUtils; import io.github.muntashirakon.AppManager.utils.LangUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.AppManager.utils.Utils; import io.github.muntashirakon.AppManager.utils.appearance.ColorCodes; import io.github.muntashirakon.dialog.SearchableItemsDialogBuilder; import io.github.muntashirakon.dialog.SearchableSingleChoiceDialogBuilder; import io.github.muntashirakon.dialog.TextInputDropdownDialogBuilder; import io.github.muntashirakon.util.AdapterUtils; import io.github.muntashirakon.view.ProgressIndicatorCompat; import io.github.muntashirakon.widget.MaterialAlertView; import io.github.muntashirakon.widget.RecyclerView; public class AppDetailsPermissionsFragment extends AppDetailsFragment { @IntDef(value = { APP_OPS, USES_PERMISSIONS, PERMISSIONS, }) @Retention(RetentionPolicy.SOURCE) public @interface PermissionProperty { } private String mPackageName; private AppDetailsRecyclerAdapter mAdapter; private boolean mIsExternalApk; @PermissionProperty private int mNeededProperty; private int mSortOrder; private String mSearchQuery; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); mNeededProperty = requireArguments().getInt(ARG_TYPE); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); emptyView.setText(getNotFoundString(mNeededProperty)); mAdapter = new AppDetailsRecyclerAdapter(); recyclerView.setAdapter(mAdapter); alertView.setEndIconOnClickListener(v -> { alertView.hide(); // Check tips if (mNeededProperty == APP_OPS) { TipsPrefs.getInstance().setDisplayInAppOpsTab(false); } if (mNeededProperty == USES_PERMISSIONS) { TipsPrefs.getInstance().setDisplayInUsesPermissionsTab(false); } if (mNeededProperty == PERMISSIONS) { TipsPrefs.getInstance().setDisplayInPermissionsTab(false); } }); int helpStringRes = getHelpString(mNeededProperty); if (helpStringRes != 0) alertView.setText(helpStringRes); if (helpStringRes == 0) { alertView.setVisibility(View.GONE); } else { alertView.postDelayed(() -> alertView.hide(), 15_000); } if (viewModel == null) return; mSortOrder = viewModel.getSortOrder(mNeededProperty); mSearchQuery = viewModel.getSearchQuery(); mPackageName = viewModel.getPackageName(); viewModel.get(mNeededProperty).observe(getViewLifecycleOwner(), appDetailsItems -> { if (appDetailsItems != null && mAdapter != null && viewModel.isPackageExist()) { mPackageName = viewModel.getPackageName(); mIsExternalApk = viewModel.isExternalApk(); mAdapter.setDefaultList(appDetailsItems); } else ProgressIndicatorCompat.setVisibility(progressIndicator, false); }); viewModel.getRuleApplicationStatus().observe(getViewLifecycleOwner(), status -> { alertView.setAlertType(MaterialAlertView.ALERT_TYPE_WARN); if (status == AppDetailsViewModel.RULE_NOT_APPLIED) { alertView.show(); } else alertView.hide(); }); } @Override public void onRefresh() { refreshDetails(); swipeRefresh.setRefreshing(false); } @Override public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { switch (mNeededProperty) { case APP_OPS: inflater.inflate(R.menu.fragment_app_details_app_ops_actions, menu); break; case USES_PERMISSIONS: if (viewModel != null && !viewModel.isExternalApk()) { inflater.inflate(R.menu.fragment_app_details_permissions_actions, menu); break; } // else fallthrough case PERMISSIONS: inflater.inflate(R.menu.fragment_app_details_refresh_actions, menu); break; } } @Override public void onPrepareMenu(@NonNull Menu menu) { if (viewModel == null || viewModel.isExternalApk()) { return; } MenuItem sortItem = menu.findItem(sSortMenuItemIdsMap[viewModel.getSortOrder(mNeededProperty)]); if (sortItem != null) { sortItem.setChecked(true); } } @Override public boolean onMenuItemSelected(@NonNull MenuItem item) { int id = item.getItemId(); if (id == R.id.action_refresh_details) { refreshDetails(); } else if (id == R.id.action_reset_to_default) { // App ops ProgressIndicatorCompat.setVisibility(progressIndicator, true); // TODO: 19/3/23 Perform using a ViewModel ThreadUtils.postOnBackgroundThread(() -> { if (viewModel == null || !viewModel.resetAppOps()) { ThreadUtils.postOnMainThread(() -> UIUtils.displayShortToast(R.string.failed_to_reset_app_ops)); } else { ThreadUtils.postOnMainThread(() -> { if (!isDetached()) { refreshDetails(); } }); } }); } else if (id == R.id.action_deny_dangerous_app_ops) { // App ops ProgressIndicatorCompat.setVisibility(progressIndicator, true); // TODO: 19/3/23 Perform using a ViewModel ThreadUtils.postOnBackgroundThread(() -> { boolean isSuccessful = ExUtils.requireNonNullElse(() -> viewModel != null && viewModel.ignoreDangerousAppOps(), false); if (isSuccessful) { ThreadUtils.postOnMainThread(() -> { if (!isDetached()) { refreshDetails(); } }); } else { ThreadUtils.postOnMainThread(() -> UIUtils.displayShortToast( R.string.failed_to_deny_dangerous_app_ops)); } }); } else if (id == R.id.action_toggle_default_app_ops) { // App ops ProgressIndicatorCompat.setVisibility(progressIndicator, true); // Turn filter on/off boolean curr = Prefs.AppDetailsPage.displayDefaultAppOps(); Prefs.AppDetailsPage.setDisplayDefaultAppOps(!curr); refreshDetails(); } else if (id == R.id.action_custom_app_op) { List modes = AppOpsManagerCompat.getModeConstants(); List appOps = AppOpsManagerCompat.getAllOps(); List modeNames = Arrays.asList(getAppOpModeNames(modes)); List appOpNames = Arrays.asList(getAppOpNames(appOps)); TextInputDropdownDialogBuilder builder = new TextInputDropdownDialogBuilder(activity, R.string.set_custom_app_op); builder.setTitle(R.string.set_custom_app_op) .setDropdownItems(appOpNames, -1, true) .setAuxiliaryInput(R.string.mode, null, null, modeNames, true) .setPositiveButton(R.string.apply, (dialog, which, inputText, isChecked) -> { // Get mode int mode; try { mode = Utils.getIntegerFromString(builder.getAuxiliaryInput(), modeNames, modes); } catch (IllegalArgumentException e) { return; } // Get op int op; try { op = Utils.getIntegerFromString(inputText, appOpNames, appOps); } catch (IllegalArgumentException e) { return; } // TODO: 22/5/23 Perform using a ViewModel ThreadUtils.postOnBackgroundThread(() -> { if (viewModel != null && viewModel.setAppOp(op, mode)) { ThreadUtils.postOnMainThread(() -> { if (!isDetached()) { refreshDetails(); } }); } else { ThreadUtils.postOnMainThread(() -> UIUtils.displayShortToast( R.string.failed_to_enable_op)); } }); }) .setNegativeButton(R.string.cancel, null) .show(); } else if (id == R.id.action_deny_dangerous_permissions) { // permissions ProgressIndicatorCompat.setVisibility(progressIndicator, true); // TODO: 22/5/23 Perform using a ViewModel ThreadUtils.postOnBackgroundThread(() -> { if (viewModel == null || !viewModel.revokeDangerousPermissions()) { ThreadUtils.postOnMainThread(() -> UIUtils.displayShortToast( R.string.failed_to_deny_dangerous_perms)); } ThreadUtils.postOnMainThread(() -> { if (!isDetached()) { refreshDetails(); } }); }); // Sorting } else if (id == R.id.action_sort_by_name) { // All setSortBy(SORT_BY_NAME); item.setChecked(true); } else if (id == R.id.action_sort_by_app_ops_values) { // App ops setSortBy(SORT_BY_APP_OP_VALUES); item.setChecked(true); } else if (id == R.id.action_sort_by_denied_app_ops) { // App ops setSortBy(SORT_BY_DENIED_APP_OPS); item.setChecked(true); } else if (id == R.id.action_sort_by_dangerous_permissions) { // App ops setSortBy(SORT_BY_DANGEROUS_PERMS); item.setChecked(true); } else if (id == R.id.action_sort_by_denied_permissions) { setSortBy(SORT_BY_DENIED_PERMS); item.setChecked(true); } else return false; return true; } @Override public void onPause() { super.onPause(); if (viewModel != null) { mSortOrder = viewModel.getSortOrder(mNeededProperty); mSearchQuery = viewModel.getSearchQuery(); } } @Override public void onResume() { super.onResume(); if (activity.searchView != null) { if (!activity.searchView.isShown()) { activity.searchView.setVisibility(View.VISIBLE); } activity.searchView.setOnQueryTextListener(this); if (viewModel != null) { int sortOrder = viewModel.getSortOrder(mNeededProperty); String searchQuery = viewModel.getSearchQuery(); if (sortOrder != mSortOrder || !Objects.equals(searchQuery, mSearchQuery)) { viewModel.filterAndSortItems(mNeededProperty); } } } } @Override public boolean onQueryTextChange(String searchQuery, int type) { if (viewModel != null) { viewModel.setSearchQuery(searchQuery, type, mNeededProperty); } return true; } private int getNotFoundString(@PermissionProperty int index) { switch (index) { case APP_OPS: if (mIsExternalApk) { return R.string.external_apk_no_app_op; } else if (SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.GET_APP_OPS_STATS)) { return R.string.no_app_ops; } else return R.string.no_app_ops_permission; case USES_PERMISSIONS: case PERMISSIONS: default: return R.string.require_no_permission; } } private int getHelpString(@PermissionProperty int index) { switch (index) { default: return 0; case APP_OPS: if (!TipsPrefs.getInstance().displayInAppOpsTab()) { return 0; } if (SelfPermissions.canModifyAppOpMode()) { return R.string.help_app_ops_tab; } else return 0; case USES_PERMISSIONS: if (!TipsPrefs.getInstance().displayInUsesPermissionsTab()) { return 0; } if (SelfPermissions.canModifyPermissions()) { return R.string.help_uses_permissions_tab; } else return 0; case PERMISSIONS: if (!TipsPrefs.getInstance().displayInPermissionsTab()) { return 0; } return R.string.help_permissions_tab; } } private void setSortBy(@SortOrder int sortBy) { ProgressIndicatorCompat.setVisibility(progressIndicator, true); if (viewModel == null) return; viewModel.setSortOrder(sortBy, mNeededProperty); } private void refreshDetails() { if (viewModel == null || mIsExternalApk) return; ProgressIndicatorCompat.setVisibility(progressIndicator, true); viewModel.triggerPackageChange(); } @UiThread private class AppDetailsRecyclerAdapter extends RecyclerView.Adapter { @NonNull private final List> mAdapterList; @PermissionProperty private int mRequestedProperty; @Nullable private String mConstraint; private boolean mCanModifyAppOpMode; AppDetailsRecyclerAdapter() { mAdapterList = new ArrayList<>(); } @UiThread void setDefaultList(@NonNull List> list) { ThreadUtils.postOnBackgroundThread(() -> { mRequestedProperty = mNeededProperty; mConstraint = viewModel == null ? null : viewModel.getSearchQuery(); mCanModifyAppOpMode = SelfPermissions.canModifyAppOpMode(); ThreadUtils.postOnMainThread(() -> { if (isDetached()) return; ProgressIndicatorCompat.setVisibility(progressIndicator, false); synchronized (mAdapterList) { AdapterUtils.notifyDataSetChanged(this, mAdapterList, list); } }); }); } /** * ViewHolder to use recycled views efficiently. Fields names are not expressive because we use * the same holder for any kind of view, and view are not all sames. */ class ViewHolder extends RecyclerView.ViewHolder { MaterialCardView itemView; TextView textView1; TextView textView2; TextView textView3; TextView textView4; TextView textView5; TextView textView6; TextView textView7; TextView textView8; ImageView imageView; MaterialSwitch toggleSwitch; MaterialButton settingButton; Chip chipType; public ViewHolder(@NonNull View itemView) { super(itemView); this.itemView = (MaterialCardView) itemView; switch (mRequestedProperty) { case PERMISSIONS: imageView = itemView.findViewById(R.id.icon); imageView.setContentDescription(itemView.getContext().getString(R.string.icon)); textView1 = itemView.findViewById(R.id.label); textView2 = itemView.findViewById(R.id.name); textView3 = itemView.findViewById(R.id.taskAffinity); textView4 = itemView.findViewById(R.id.orientation); textView5 = itemView.findViewById(R.id.launchMode); chipType = itemView.findViewById(R.id.type); itemView.findViewById(R.id.softInput).setVisibility(View.GONE); itemView.findViewById(R.id.launch).setVisibility(View.GONE); itemView.findViewById(R.id.edit_shortcut_btn).setVisibility(View.GONE); itemView.findViewById(R.id.toggle_button).setVisibility(View.GONE); break; case APP_OPS: textView1 = itemView.findViewById(R.id.op_name); textView2 = itemView.findViewById(R.id.perm_description); textView3 = itemView.findViewById(R.id.perm_protection_level); textView4 = itemView.findViewById(R.id.perm_package_name); textView5 = itemView.findViewById(R.id.perm_group); textView6 = itemView.findViewById(R.id.perm_name); textView7 = itemView.findViewById(R.id.op_mode_running_duration); textView8 = itemView.findViewById(R.id.op_accept_reject_time); toggleSwitch = itemView.findViewById(R.id.perm_toggle_btn); break; case USES_PERMISSIONS: textView1 = itemView.findViewById(R.id.perm_name); textView2 = itemView.findViewById(R.id.perm_description); textView3 = itemView.findViewById(R.id.perm_protection_level); textView4 = itemView.findViewById(R.id.perm_package_name); textView5 = itemView.findViewById(R.id.perm_group); toggleSwitch = itemView.findViewById(R.id.perm_toggle_btn); settingButton = itemView.findViewById(R.id.action_settings); break; default: break; } } } @NonNull @Override public AppDetailsRecyclerAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { @SuppressLint("InflateParams") final View view; switch (mRequestedProperty) { case PERMISSIONS: default: view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_app_details_primary, parent, false); break; case APP_OPS: view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_app_details_appop, parent, false); break; case USES_PERMISSIONS: view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_app_details_perm, parent, false); break; } return new AppDetailsRecyclerAdapter.ViewHolder(view); } @Override public void onBindViewHolder(@NonNull AppDetailsRecyclerAdapter.ViewHolder holder, int position) { Context context = holder.itemView.getContext(); switch (mRequestedProperty) { case APP_OPS: getAppOpsView(context, holder, position); break; case USES_PERMISSIONS: getUsesPermissionsView(context, holder, position); break; case PERMISSIONS: getPermissionsView(context, holder, position); break; default: break; } } @Override public long getItemId(int position) { return position; } @Override public int getItemCount() { synchronized (mAdapterList) { return mAdapterList.size(); } } private void getAppOpsView(@NonNull Context context, @NonNull ViewHolder holder, int index) { AppDetailsAppOpItem item; synchronized (mAdapterList) { item = (AppDetailsAppOpItem) mAdapterList.get(index); } final String opStr = item.name; PermissionInfo permissionInfo = item.permissionInfo; // Set op name SpannableStringBuilder opName = new SpannableStringBuilder(item.getOp() + " - "); if (item.name.equals(String.valueOf(item.getOp()))) { opName.append(getString(R.string.unknown_op)); } else if (mConstraint != null && opStr.toLowerCase(Locale.ROOT).contains(mConstraint)) { // Highlight searched query opName.append(UIUtils.getHighlightedText(opStr, mConstraint, colorQueryStringHighlight)); } else opName.append(opStr); holder.textView1.setText(opName); // Set op mode, running and duration StringBuilder opRunningInfo = new StringBuilder() .append(context.getString(R.string.mode)) .append(LangUtils.getSeparatorString()) .append(AppOpsManagerCompat.modeToName(item.getMode())); if (item.isRunning()) { opRunningInfo.append(", ").append(context.getString(R.string.running)); } if (item.getDuration() != 0) { opRunningInfo.append(", ").append(context.getString(R.string.duration)) .append(LangUtils.getSeparatorString()) .append(DateUtils.getFormattedDuration(context, item.getDuration(), true)); } holder.textView7.setText(opRunningInfo); // Set accept-time and/or reject-time long currentTime = System.currentTimeMillis(); boolean hasAcceptTime = item.getTime() != 0 && item.getTime() != -1; boolean hasRejectTime = item.getRejectTime() != 0 && item.getRejectTime() != -1; if (hasAcceptTime || hasRejectTime) { StringBuilder opTime = new StringBuilder(); if (hasAcceptTime) { opTime.append(context.getString(R.string.accept_time)) .append(LangUtils.getSeparatorString()) .append(DateUtils.getFormattedDuration(context, currentTime - item.getTime())) .append(" ").append(context.getString(R.string.ago)); } if (hasRejectTime) { opTime.append(opTime.length() == 0 ? "" : "\n") .append(context.getString(R.string.reject_time)) .append(LangUtils.getSeparatorString()) .append(DateUtils.getFormattedDuration(context, currentTime - item.getRejectTime())) .append(" ").append(context.getString(R.string.ago)); } holder.textView8.setVisibility(View.VISIBLE); holder.textView8.setText(opTime); } else holder.textView8.setVisibility(View.GONE); // Set others if (permissionInfo != null) { // Set permission name holder.textView6.setVisibility(View.VISIBLE); holder.textView6.setText(String.format(Locale.ROOT, "%s%s%s", context.getString(R.string.permission_name), LangUtils.getSeparatorString(), permissionInfo.name)); // Description CharSequence description = permissionInfo.loadDescription(packageManager); if (description != null) { holder.textView2.setVisibility(View.VISIBLE); holder.textView2.setText(description); } else holder.textView2.setVisibility(View.GONE); // Protection level String protectionLevel = Utils.getProtectionLevelString(permissionInfo); protectionLevel += '|' + (Objects.requireNonNull(item.permission).isGranted() ? "granted" : "revoked"); holder.textView3.setVisibility(View.VISIBLE); holder.textView3.setText(String.format(Locale.ROOT, "⚑ %s", protectionLevel)); // Set package name if (permissionInfo.packageName != null) { holder.textView4.setVisibility(View.VISIBLE); holder.textView4.setText(String.format(Locale.ROOT, "%s%s%s", context.getString(R.string.package_name), LangUtils.getSeparatorString(), permissionInfo.packageName)); } else holder.textView4.setVisibility(View.GONE); // Set group name if (permissionInfo.group != null) { holder.textView5.setVisibility(View.VISIBLE); holder.textView5.setText(String.format(Locale.ROOT, "%s%s%s", context.getString(R.string.group), LangUtils.getSeparatorString(), permissionInfo.group)); } else { holder.textView5.setVisibility(View.GONE); } } else { holder.textView2.setVisibility(View.GONE); holder.textView3.setVisibility(View.GONE); holder.textView4.setVisibility(View.GONE); holder.textView5.setVisibility(View.GONE); holder.textView6.setVisibility(View.GONE); } // Set background if (item.isDangerous) { holder.itemView.setStrokeColor(ColorCodes.getPermissionDangerousIndicatorColor(context)); } else { holder.itemView.setStrokeColor(Color.TRANSPARENT); } // Op Switch holder.toggleSwitch.setVisibility(mCanModifyAppOpMode ? View.VISIBLE : View.GONE); // op granted holder.toggleSwitch.setChecked(item.isAllowed()); holder.itemView.setOnClickListener(v -> { boolean isAllowed = !item.isAllowed(); // TODO: 22/5/23 Perform using a ViewModel ThreadUtils.postOnBackgroundThread(() -> { if (viewModel != null && viewModel.setAppOpMode(item)) { ThreadUtils.postOnMainThread(() -> notifyItemChanged(index, AdapterUtils.STUB)); } else { ThreadUtils.postOnMainThread(() -> UIUtils.displayLongToast(isAllowed ? R.string.failed_to_enable_op : R.string.failed_to_disable_op)); } }); }); holder.itemView.setOnLongClickListener(v -> { List modes = AppOpsManagerCompat.getModeConstants(); new SearchableSingleChoiceDialogBuilder<>(activity, modes, getAppOpModeNames(modes)) .setTitle(R.string.set_app_op_mode) .setSelection(item.getMode()) .setOnSingleChoiceClickListener((dialog, which, item1, isChecked) -> { int opMode = modes.get(which); // TODO: 22/5/23 Perform using a ViewModel ThreadUtils.postOnBackgroundThread(() -> { if (viewModel != null && viewModel.setAppOpMode(item, opMode)) { ThreadUtils.postOnMainThread(() -> notifyItemChanged(index, AdapterUtils.STUB)); } else { ThreadUtils.postOnMainThread(() -> UIUtils.displayLongToast( R.string.failed_to_change_app_op_mode)); } }); dialog.dismiss(); }) .show(); return true; }); } private void getUsesPermissionsView(@NonNull Context context, @NonNull ViewHolder holder, int index) { AppDetailsPermissionItem permissionItem; synchronized (mAdapterList) { permissionItem = (AppDetailsPermissionItem) mAdapterList.get(index); } @NonNull PermissionInfo permissionInfo = permissionItem.item; final String permName = permissionInfo.name; // Set permission name if (mConstraint != null && permName.toLowerCase(Locale.ROOT).contains(mConstraint)) { // Highlight searched query holder.textView1.setText(UIUtils.getHighlightedText(permName, mConstraint, colorQueryStringHighlight)); } else holder.textView1.setText(permName); // Set others // Description CharSequence description = permissionInfo.loadDescription(packageManager); if (description != null) { holder.textView2.setVisibility(View.VISIBLE); holder.textView2.setText(description); } else holder.textView2.setVisibility(View.GONE); // Protection level String protectionLevel = Utils.getProtectionLevelString(permissionInfo); protectionLevel += '|' + (permissionItem.permission.isGranted() ? "granted" : "revoked"); holder.textView3.setText(String.format(Locale.ROOT, "⚑ %s", protectionLevel)); // Set background color if (permissionItem.isDangerous) { holder.itemView.setStrokeColor(ColorCodes.getPermissionDangerousIndicatorColor(context)); } else { holder.itemView.setStrokeColor(Color.TRANSPARENT); } // Set package name if (permissionInfo.packageName != null) { holder.textView4.setVisibility(View.VISIBLE); holder.textView4.setText(String.format("%s%s%s", context.getString(R.string.package_name), LangUtils.getSeparatorString(), permissionInfo.packageName)); } else holder.textView4.setVisibility(View.GONE); // Set group name if (permissionInfo.group != null) { holder.textView5.setVisibility(View.VISIBLE); holder.textView5.setText(String.format("%s%s%s", context.getString(R.string.group), LangUtils.getSeparatorString(), permissionInfo.group)); } else holder.textView5.setVisibility(View.GONE); // Permission Switch boolean canGrantOrRevokePermission = permissionItem.modifiable && !mIsExternalApk; if (canGrantOrRevokePermission) { holder.toggleSwitch.setVisibility(View.VISIBLE); holder.toggleSwitch.setChecked(permissionItem.isGranted()); // TODO: 22/5/23 Perform using a ViewModel holder.itemView.setOnClickListener(v -> ThreadUtils.postOnBackgroundThread(() -> { try { if (Objects.requireNonNull(viewModel).togglePermission(permissionItem)) { ThreadUtils.postOnMainThread(() -> notifyItemChanged(index, AdapterUtils.STUB)); } else throw new Exception("Couldn't grant permission: " + permName); } catch (Exception e) { e.printStackTrace(); ThreadUtils.postOnMainThread(() -> UIUtils.displayShortToast(permissionItem.isGranted() ? R.string.failed_to_grant_permission : R.string.failed_to_revoke_permission)); } })); } else { holder.toggleSwitch.setVisibility(View.GONE); holder.itemView.setOnClickListener(null); holder.itemView.setClickable(false); if (permissionItem.settingItem != null) { holder.settingButton.setVisibility(View.VISIBLE); holder.settingButton.setOnClickListener(v -> { try { String packageName = Objects.requireNonNull(viewModel).getPackageName(); startActivity(permissionItem.settingItem.toIntent(Objects.requireNonNull(packageName))); } catch (Throwable th) { th.printStackTrace(); if (th.getLocalizedMessage() != null) { UIUtils.displayLongToast(th.getLocalizedMessage()); } } }); } else { holder.settingButton.setVisibility(View.GONE); } } int flags = permissionItem.permission.getFlags(); holder.itemView.setOnLongClickListener(flags == 0 ? null : v -> { // TODO: 12/1/22 Use ViewModel SparseArray permissionFlags = PermissionCompat.getPermissionFlagsWithString(flags); String[] flagStrings = new String[permissionFlags.size()]; for (int i = 0; i < flagStrings.length; ++i) { flagStrings[i] = permissionFlags.valueAt(i); } new SearchableItemsDialogBuilder<>(activity, flagStrings) .setTitle(R.string.permission_flags) .setNegativeButton(R.string.close, null) .show(); return true; }); holder.itemView.setLongClickable(flags != 0); } private void getPermissionsView(@NonNull Context context, @NonNull ViewHolder holder, int index) { AppDetailsDefinedPermissionItem permissionItem; synchronized (mAdapterList) { permissionItem = (AppDetailsDefinedPermissionItem) mAdapterList.get(index); } PermissionInfo permissionInfo = permissionItem.item; // Internal or external holder.chipType.setText(permissionItem.isExternal ? R.string.external : R.string.internal); // Label holder.textView1.setText(permissionInfo.loadLabel(packageManager)); // Name if (mConstraint != null && permissionInfo.name.toLowerCase(Locale.ROOT).contains(mConstraint)) { // Highlight searched query holder.textView2.setText(UIUtils.getHighlightedText(permissionInfo.name, mConstraint, colorQueryStringHighlight)); } else { holder.textView2.setText(permissionInfo.name.startsWith(mPackageName) ? permissionInfo.name.replaceFirst(mPackageName, "") : permissionInfo.name); } // Icon String tag = mPackageName + "_" + permissionInfo.name; holder.imageView.setTag(tag); ImageLoader.getInstance().displayImage(tag, permissionInfo, holder.imageView); // Description CharSequence description = permissionInfo.loadDescription(packageManager); if (description != null) { holder.textView3.setVisibility(View.VISIBLE); holder.textView3.setText(description); } else { holder.textView3.setVisibility(View.GONE); } // LaunchMode holder.textView4.setText(String.format(Locale.ROOT, "%s: %s", getString(R.string.group), permissionInfo.group + permAppOp(permissionInfo.name))); // Protection level String protectionLevel = Utils.getProtectionLevelString(permissionInfo); holder.textView5.setText(String.format(Locale.ROOT, "⚑ %s", protectionLevel)); // Set border color if (protectionLevel.contains("dangerous")) { holder.itemView.setStrokeColor(ColorCodes.getPermissionDangerousIndicatorColor(context)); } else { holder.itemView.setStrokeColor(Color.TRANSPARENT); } } @NonNull private String permAppOp(String s) { String opStr = AppOpsManagerCompat.permissionToOp(s); return opStr != null ? "\nAppOp: " + opStr : ""; } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/details/AppDetailsViewModel.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.details; import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.GET_SIGNING_CERTIFICATES; import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.MATCH_DISABLED_COMPONENTS; import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.MATCH_UNINSTALLED_PACKAGES; import android.Manifest; import android.annotation.SuppressLint; import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.AppOpsManager; import android.app.Application; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.om.OverlayInfo; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.ComponentInfo; import android.content.pm.ConfigurationInfo; import android.content.pm.FeatureInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PermissionInfo; import android.content.pm.ProviderInfo; import android.content.pm.ServiceInfo; import android.os.Build; import android.os.RemoteException; import android.os.UserHandleHidden; import android.text.TextUtils; import androidx.annotation.AnyThread; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.annotation.WorkerThread; import androidx.core.content.pm.PermissionInfoCompat; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import com.android.apksig.ApkVerifier; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Optional; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; import io.github.muntashirakon.AppManager.apk.ApkFile; import io.github.muntashirakon.AppManager.apk.ApkSource; import io.github.muntashirakon.AppManager.apk.CachedApkSource; import io.github.muntashirakon.AppManager.apk.signing.SignerInfo; import io.github.muntashirakon.AppManager.compat.ActivityManagerCompat; import io.github.muntashirakon.AppManager.compat.AppOpsManagerCompat; import io.github.muntashirakon.AppManager.compat.ApplicationInfoCompat; import io.github.muntashirakon.AppManager.compat.ManifestCompat; import io.github.muntashirakon.AppManager.compat.OverlayManagerCompact; import io.github.muntashirakon.AppManager.compat.PackageManagerCompat; import io.github.muntashirakon.AppManager.compat.PermissionCompat; import io.github.muntashirakon.AppManager.details.struct.AppDetailsActivityItem; import io.github.muntashirakon.AppManager.details.struct.AppDetailsAppOpItem; import io.github.muntashirakon.AppManager.details.struct.AppDetailsComponentItem; import io.github.muntashirakon.AppManager.details.struct.AppDetailsDefinedPermissionItem; import io.github.muntashirakon.AppManager.details.struct.AppDetailsFeatureItem; import io.github.muntashirakon.AppManager.details.struct.AppDetailsItem; import io.github.muntashirakon.AppManager.details.struct.AppDetailsLibraryItem; import io.github.muntashirakon.AppManager.details.struct.AppDetailsOverlayItem; import io.github.muntashirakon.AppManager.details.struct.AppDetailsPermissionItem; import io.github.muntashirakon.AppManager.details.struct.AppDetailsServiceItem; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.misc.AdvancedSearchView; import io.github.muntashirakon.AppManager.misc.AdvancedSearchView.ChoiceGenerator; import io.github.muntashirakon.AppManager.permission.DevelopmentPermission; import io.github.muntashirakon.AppManager.permission.PermUtils; import io.github.muntashirakon.AppManager.permission.Permission; import io.github.muntashirakon.AppManager.permission.PermissionException; import io.github.muntashirakon.AppManager.permission.ReadOnlyPermission; import io.github.muntashirakon.AppManager.permission.RuntimePermission; import io.github.muntashirakon.AppManager.rules.RuleType; import io.github.muntashirakon.AppManager.rules.compontents.ComponentUtils; import io.github.muntashirakon.AppManager.rules.compontents.ComponentsBlocker; import io.github.muntashirakon.AppManager.rules.struct.AppOpRule; import io.github.muntashirakon.AppManager.rules.struct.ComponentRule; import io.github.muntashirakon.AppManager.rules.struct.RuleEntry; import io.github.muntashirakon.AppManager.scanner.NativeLibraries; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.settings.Ops; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.types.PackageChangeReceiver; import io.github.muntashirakon.AppManager.users.UserInfo; import io.github.muntashirakon.AppManager.users.Users; import io.github.muntashirakon.AppManager.utils.ExUtils; import io.github.muntashirakon.AppManager.utils.FreezeUtils; import io.github.muntashirakon.AppManager.utils.Utils; import io.github.muntashirakon.io.IoUtils; public class AppDetailsViewModel extends AndroidViewModel { public static final String TAG = AppDetailsViewModel.class.getSimpleName(); private final PackageManager mPackageManager; private final Object mBlockerLocker = new Object(); private final ExecutorService mExecutor = Executors.newFixedThreadPool(4); private final CountDownLatch mPackageInfoWatcher = new CountDownLatch(1); private final MutableLiveData mPackageInfoLiveData = new MutableLiveData<>(); private final MutableLiveData mTagsAlteredLiveData = new MutableLiveData<>(); private final MutableLiveData mFreezeTypeLiveData = new MutableLiveData<>(); private final MutableLiveData mComponentChangedLiveData = new MutableLiveData<>(); @Nullable private PackageInfo mPackageInfo; @Nullable private PackageInfo mInstalledPackageInfo; @Nullable private String mPackageName; @GuardedBy("blockerLocker") private ComponentsBlocker mBlocker; @Nullable private PackageIntentReceiver mReceiver; @Nullable private String mApkPath; @Nullable private ApkSource mApkSource; @Nullable private ApkFile mApkFile; private int mUserId; @AppDetailsFragment.SortOrder private int mSortOrderComponents = Prefs.AppDetailsPage.getComponentsSortOrder(); @AppDetailsFragment.SortOrder private int mSortOrderAppOps = Prefs.AppDetailsPage.getAppOpsSortOrder(); @AppDetailsFragment.SortOrder private int mSortOrderPermissions = Prefs.AppDetailsPage.getPermissionsSortOrder(); @AppDetailsFragment.SortOrder private int mSortOrderOverlays = Prefs.AppDetailsPage.getOverlaysSortOrder(); private String mSearchQuery; @AdvancedSearchView.SearchType private int mSearchType; private boolean mWaitForBlocker; private boolean mExternalApk = false; public AppDetailsViewModel(@NonNull Application application) { super(application); mPackageManager = application.getPackageManager(); mReceiver = new PackageIntentReceiver(this); mWaitForBlocker = true; } @GuardedBy("blockerLocker") @Override public void onCleared() { Log.d(TAG, "On Clear called for %s", mPackageName); super.onCleared(); mExecutor.submit(() -> { synchronized (mBlockerLocker) { if (mBlocker != null) { // To prevent commit if a mutable instance was created in the middle, // set the instance read only again mBlocker.setReadOnly(); mBlocker.close(); } } }); if (mReceiver != null) { getApplication().unregisterReceiver(mReceiver); } mReceiver = null; IoUtils.closeQuietly(mApkFile); if (mApkSource instanceof CachedApkSource) { ((CachedApkSource) mApkSource).cleanup(); } mExecutor.shutdownNow(); } public LiveData getFreezeTypeLiveData() { return mFreezeTypeLiveData; } public void loadFreezeType() { mExecutor.submit(() -> { Integer freezeType = FreezeUtils.loadFreezeMethod(mPackageName); mFreezeTypeLiveData.postValue(freezeType); }); } public MutableLiveData getTagsAlteredLiveData() { return mTagsAlteredLiveData; } @UiThread @NonNull public LiveData setPackage(@NonNull ApkSource apkSource) { mApkSource = apkSource; mExternalApk = true; mExecutor.submit(() -> { try { Log.d(TAG, "Package Uri is being set"); mApkFile = mApkSource.resolve(); setPackageName(mApkFile.getPackageName()); File cachedApkFile = mApkFile.getBaseEntry().getFile(false); if (!cachedApkFile.canRead()) throw new Exception("Cannot read " + cachedApkFile); mApkPath = cachedApkFile.getAbsolutePath(); setPackageInfo(false); mPackageInfoLiveData.postValue(getPackageInfo()); } catch (Throwable th) { Log.e(TAG, "Could not fetch package info.", th); mPackageInfoLiveData.postValue(null); } finally { mPackageInfoWatcher.countDown(); } }); return mPackageInfoLiveData; } @UiThread @NonNull public LiveData setPackage(@NonNull String packageName) { mExternalApk = false; mExecutor.submit(() -> { try { Log.d(TAG, "Package name is being set"); setPackageName(packageName); // TODO: 23/5/21 The app could be “data only” setPackageInfo(false); PackageInfo pi = getPackageInfo(); if (pi == null) throw new ApkFile.ApkFileException("Package not installed."); mApkSource = ApkSource.getApkSource(pi.applicationInfo); mApkFile = mApkSource.resolve(); mPackageInfoLiveData.postValue(pi); } catch (Throwable th) { Log.e(TAG, "Could not fetch package info.", th); mPackageInfoLiveData.postValue(null); } finally { mPackageInfoWatcher.countDown(); } }); return mPackageInfoLiveData; } @AnyThread public void setUserId(@UserIdInt int userId) { mUserId = userId; } @AnyThread public int getUserId() { return mUserId; } @AnyThread @GuardedBy("blockerLocker") private void setPackageName(String packageName) { if (mPackageName != null) return; Log.d(TAG, "Package name is being set for %s", packageName); mPackageName = packageName; if (mExternalApk) return; mExecutor.submit(() -> { synchronized (mBlockerLocker) { try { mWaitForBlocker = true; if (mBlocker != null) { // To prevent commit if a mutable instance was created in the middle, // set the instance read only again mBlocker.setReadOnly(); mBlocker.close(); } mBlocker = ComponentsBlocker.getInstance(packageName, mUserId); } finally { mWaitForBlocker = false; mBlockerLocker.notifyAll(); } } }); } @AnyThread public String getPackageName() { return mPackageName; } @Nullable public ApkFile getApkFile() { return mApkFile; } @AnyThread @Nullable public ApkSource getApkSource() { return mApkSource; } public boolean isTestOnlyApp() { return mPackageInfo != null && ApplicationInfoCompat.isTestOnly(mPackageInfo.applicationInfo); } @AnyThread @SuppressLint("SwitchIntDef") public void setSortOrder(@AppDetailsFragment.SortOrder int sortOrder, @AppDetailsFragment.Property int property) { switch (property) { case AppDetailsFragment.ACTIVITIES: case AppDetailsFragment.SERVICES: case AppDetailsFragment.RECEIVERS: case AppDetailsFragment.PROVIDERS: mSortOrderComponents = sortOrder; Prefs.AppDetailsPage.setComponentsSortOrder(sortOrder); break; case AppDetailsFragment.APP_OPS: mSortOrderAppOps = sortOrder; Prefs.AppDetailsPage.setAppOpsSortOrder(sortOrder); break; case AppDetailsFragment.USES_PERMISSIONS: mSortOrderPermissions = sortOrder; Prefs.AppDetailsPage.setPermissionsSortOrder(sortOrder); break; case AppDetailsFragment.OVERLAYS: mSortOrderOverlays = sortOrder; Prefs.AppDetailsPage.setOverlaysSortOrder(sortOrder); } mExecutor.submit(() -> filterAndSortItemsInternal(property)); } @AnyThread @SuppressLint("SwitchIntDef") @AppDetailsFragment.SortOrder public int getSortOrder(@AppDetailsFragment.Property int property) { switch (property) { case AppDetailsFragment.ACTIVITIES: case AppDetailsFragment.SERVICES: case AppDetailsFragment.RECEIVERS: case AppDetailsFragment.PROVIDERS: return mSortOrderComponents; case AppDetailsFragment.APP_OPS: return mSortOrderAppOps; case AppDetailsFragment.USES_PERMISSIONS: return mSortOrderPermissions; case AppDetailsFragment.OVERLAYS: return mSortOrderOverlays; } return AppDetailsFragment.SORT_BY_NAME; } @AnyThread public void setSearchQuery(String searchQuery, int searchType, @AppDetailsFragment.Property int property) { mSearchQuery = searchType == AdvancedSearchView.SEARCH_TYPE_REGEX ? searchQuery : searchQuery.toLowerCase(Locale.ROOT); mSearchType = searchType; mExecutor.submit(() -> filterAndSortItemsInternal(property)); } @AnyThread public String getSearchQuery() { return mSearchQuery; } public void filterAndSortItems(@AppDetailsFragment.Property int property) { mExecutor.submit(() -> filterAndSortItemsInternal(property)); } @SuppressLint({"SwitchIntDef", "NewApi"}) @WorkerThread private void filterAndSortItemsInternal(@AppDetailsFragment.Property int property) { switch (property) { case AppDetailsFragment.ACTIVITIES: synchronized (mActivityItems) { mActivities.postValue(filterAndSortComponents(mActivityItems)); } break; case AppDetailsFragment.PROVIDERS: synchronized (mProviderItems) { mProviders.postValue(filterAndSortComponents(mProviderItems)); } break; case AppDetailsFragment.RECEIVERS: synchronized (mReceiverItems) { mReceivers.postValue(filterAndSortComponents(mReceiverItems)); } break; case AppDetailsFragment.SERVICES: synchronized (mServiceItems) { mServices.postValue(filterAndSortComponents(mServiceItems)); } break; case AppDetailsFragment.APP_OPS: { List appDetailsItems; synchronized (mAppOpItems) { if (!TextUtils.isEmpty(mSearchQuery)) { appDetailsItems = AdvancedSearchView.matches(mSearchQuery, mAppOpItems, (ChoiceGenerator) item -> lowercaseIfNotRegex(item.name, mSearchType), mSearchType); } else appDetailsItems = mAppOpItems; } Collections.sort(appDetailsItems, (o1, o2) -> { switch (mSortOrderAppOps) { case AppDetailsFragment.SORT_BY_NAME: return o1.name.compareToIgnoreCase(o2.name); case AppDetailsFragment.SORT_BY_APP_OP_VALUES: Integer o1Op = o1.getOp(); Integer o2Op = o2.getOp(); return o1Op.compareTo(o2Op); case AppDetailsFragment.SORT_BY_DENIED_APP_OPS: // A slight hack to sort it this way: ignore > foreground > deny > default[ > ask] > allow Integer o1Mode = o1.getMode(); Integer o2Mode = o2.getMode(); return -o1Mode.compareTo(o2Mode); } return 0; }); mAppOps.postValue(appDetailsItems); break; } case AppDetailsFragment.USES_PERMISSIONS: { List appDetailsItems; synchronized (mUsesPermissionItems) { if (!TextUtils.isEmpty(mSearchQuery)) { appDetailsItems = AdvancedSearchView.matches(mSearchQuery, mUsesPermissionItems, (ChoiceGenerator) item -> lowercaseIfNotRegex(item.name, mSearchType), mSearchType); } else appDetailsItems = mUsesPermissionItems; } Collections.sort(appDetailsItems, (o1, o2) -> { switch (mSortOrderPermissions) { case AppDetailsFragment.SORT_BY_NAME: return o1.name.compareToIgnoreCase(o2.name); case AppDetailsFragment.SORT_BY_DANGEROUS_PERMS: return -Boolean.compare(o1.isDangerous, o2.isDangerous); case AppDetailsFragment.SORT_BY_DENIED_PERMS: return Boolean.compare(o1.permission.isGranted(), o2.permission.isGranted()); } return 0; }); mUsesPermissions.postValue(new ArrayList<>(appDetailsItems)); break; } case AppDetailsFragment.PERMISSIONS: synchronized (mPermissionItems) { mPermissions.postValue(filterAndSortPermissions(mPermissionItems)); } break; case AppDetailsFragment.OVERLAYS: { List appDetailsItems; synchronized (mOverlays) { if (!TextUtils.isEmpty(mSearchQuery)) { appDetailsItems = AdvancedSearchView.matches(mSearchQuery, mOverlays.getValue(), (ChoiceGenerator) item -> lowercaseIfNotRegex(item.name, mSearchType), mSearchType); } else appDetailsItems = mOverlays.getValue(); } Collections.sort(appDetailsItems, (o1, o2) -> { switch (mSortOrderOverlays) { case AppDetailsFragment.SORT_BY_NAME: return o1.name.compareToIgnoreCase(o2.name); case AppDetailsFragment.SORT_BY_PRIORITY: return Integer.compare(o1.getPriority(), o2.getPriority()); } return 0; }); mOverlays.postValue(new ArrayList<>(appDetailsItems)); break; } case AppDetailsFragment.APP_INFO: case AppDetailsFragment.CONFIGURATIONS: case AppDetailsFragment.FEATURES: case AppDetailsFragment.SHARED_LIBRARIES: case AppDetailsFragment.SIGNATURES: // do nothing break; } } @WorkerThread @Nullable private List> filterAndSortComponents( @Nullable List> appDetailsItems) { if (appDetailsItems == null) return null; if (TextUtils.isEmpty(mSearchQuery)) { sortComponents(appDetailsItems); return appDetailsItems; } List> appDetailsItemsInt = AdvancedSearchView.matches(mSearchQuery, appDetailsItems, (ChoiceGenerator>) item -> lowercaseIfNotRegex(item.name, mSearchType), mSearchType); sortComponents(appDetailsItemsInt); return appDetailsItemsInt; } @WorkerThread @Nullable private List> filterAndSortPermissions( @Nullable List> appDetailsItems) { if (appDetailsItems == null) return null; if (TextUtils.isEmpty(mSearchQuery)) { Collections.sort(appDetailsItems, (o1, o2) -> o1.name.compareToIgnoreCase(o2.name)); return appDetailsItems; } List> appDetailsItemsInt = AdvancedSearchView.matches(mSearchQuery, appDetailsItems, (ChoiceGenerator>) item -> lowercaseIfNotRegex(item.name, mSearchType), mSearchType); Collections.sort(appDetailsItemsInt, (o1, o2) -> o1.name.compareToIgnoreCase(o2.name)); return appDetailsItemsInt; } /** * Return lowercase string if regex isn't enabled (among the search types, only regex is case-sensitive). */ private String lowercaseIfNotRegex(String s, @AdvancedSearchView.SearchType int filterType) { return filterType == AdvancedSearchView.SEARCH_TYPE_REGEX ? s : s.toLowerCase(Locale.ROOT); } public static final int RULE_APPLIED = 0; public static final int RULE_NOT_APPLIED = 1; public static final int RULE_NO_RULE = 2; @NonNull private final MutableLiveData mRuleApplicationStatus = new MutableLiveData<>(); @UiThread public LiveData getRuleApplicationStatus() { if (mRuleApplicationStatus.getValue() == null) { mExecutor.submit(this::setRuleApplicationStatus); } return mRuleApplicationStatus; } @WorkerThread @GuardedBy("blockerLocker") public void setRuleApplicationStatus() { if (mPackageName == null || mExternalApk) { mRuleApplicationStatus.postValue(RULE_NO_RULE); return; } synchronized (mBlockerLocker) { waitForBlockerOrExit(); final AtomicInteger newRuleApplicationStatus = new AtomicInteger(); newRuleApplicationStatus.set(mBlocker.isRulesApplied() ? RULE_APPLIED : RULE_NOT_APPLIED); if (mBlocker.componentCount() == 0) newRuleApplicationStatus.set(RULE_NO_RULE); mRuleApplicationStatus.postValue(newRuleApplicationStatus.get()); } } @AnyThread @GuardedBy("blockerLocker") public void updateRulesForComponent(@NonNull AppDetailsComponentItem componentItem, @NonNull RuleType type, @ComponentRule.ComponentStatus String componentStatus) { if (mExternalApk) return; mExecutor.submit(() -> { Optional.ofNullable(mReceiver).ifPresent(PackageIntentReceiver::pauseWatcher); String componentName = componentItem.name; synchronized (mBlockerLocker) { waitForBlockerOrExit(); mBlocker.setMutable(); if (mBlocker.hasComponentName(componentName)) { // Simply delete it mBlocker.deleteComponent(componentName); } // Add to the list mBlocker.addComponent(componentName, type, componentStatus); // Apply rules if global blocking enable or already applied if (Prefs.Blocking.globalBlockingEnabled() || (mRuleApplicationStatus.getValue() != null && RULE_APPLIED == mRuleApplicationStatus.getValue())) { mBlocker.applyRules(true); } // Set new status setRuleApplicationStatus(); // Commit changes mBlocker.commit(); mBlocker.setReadOnly(); Optional.ofNullable(mReceiver).ifPresent(PackageIntentReceiver::resumeWatcher); } }); } @Nullable public ComponentRule getComponentRule(String componentName) { synchronized (mBlockerLocker) { if (mBlocker != null) { return mBlocker.getComponent(componentName); } return null; } } @WorkerThread @GuardedBy("blockerLocker") public void addRules(List entries, boolean forceApply) { if (mExternalApk) return; synchronized (mBlockerLocker) { waitForBlockerOrExit(); mBlocker.setMutable(); for (RuleEntry entry : entries) { String componentName = entry.name; if (mBlocker.hasComponentName(componentName)) { // Remove from the list mBlocker.removeComponent(componentName); } // Add to the list (again) mBlocker.addComponent(componentName, entry.type); } // Apply rules if global blocking enable or already applied if (forceApply || Prefs.Blocking.globalBlockingEnabled() || (mRuleApplicationStatus.getValue() != null && RULE_APPLIED == mRuleApplicationStatus.getValue())) { mBlocker.applyRules(true); } // Set new status setRuleApplicationStatus(); // Commit changes mBlocker.commit(); mBlocker.setReadOnly(); // Update UI reloadComponents(); } } @WorkerThread @GuardedBy("blockerLocker") public void removeRules(List entries, boolean forceApply) { if (mExternalApk) return; synchronized (mBlockerLocker) { waitForBlockerOrExit(); mBlocker.setMutable(); for (RuleEntry entry : entries) { String componentName = entry.name; if (mBlocker.hasComponentName(componentName)) { // Remove from the list mBlocker.removeComponent(componentName); } } // Apply rules if global blocking enable or already applied if (forceApply || Prefs.Blocking.globalBlockingEnabled() || (mRuleApplicationStatus.getValue() != null && RULE_APPLIED == mRuleApplicationStatus.getValue())) { mBlocker.applyRules(true); } // Set new status setRuleApplicationStatus(); // Commit changes mBlocker.commit(); mBlocker.setReadOnly(); // Update UI reloadComponents(); } } @WorkerThread @GuardedBy("blockerLocker") public boolean togglePermission(final AppDetailsPermissionItem permissionItem) { if (mExternalApk) return false; PackageInfo packageInfo = getPackageInfoInternal(); if (packageInfo == null) return false; try { if (!permissionItem.isGranted()) { Log.d(TAG, "Granting permission: %s", permissionItem.name); permissionItem.grantPermission(packageInfo, mAppOpsManager); } else { Log.d(TAG, "Revoking permission: %s", permissionItem.name); permissionItem.revokePermission(packageInfo, mAppOpsManager); } } catch (RemoteException | PermissionException e) { e.printStackTrace(); return false; } mExecutor.submit(() -> { synchronized (mBlockerLocker) { waitForBlockerOrExit(); mBlocker.setMutable(); mBlocker.setPermission(permissionItem.name, permissionItem.permission.isGranted(), permissionItem.permission.getFlags()); mBlocker.commit(); mBlocker.setReadOnly(); mBlockerLocker.notifyAll(); } }); setUsesPermission(permissionItem); return true; } @WorkerThread @GuardedBy("blockerLocker") public boolean revokeDangerousPermissions() { if (mExternalApk) return false; PackageInfo packageInfo = getPackageInfoInternal(); if (packageInfo == null) return false; List revokedPermissions = new ArrayList<>(); boolean isSuccessful = true; synchronized (mUsesPermissionItems) { for (AppDetailsPermissionItem permissionItem : mUsesPermissionItems) { if (!permissionItem.isDangerous || !permissionItem.permission.isGranted()) continue; try { permissionItem.revokePermission(packageInfo, mAppOpsManager); revokedPermissions.add(permissionItem); } catch (RemoteException | PermissionException e) { e.printStackTrace(); isSuccessful = false; } } } // Save values to the blocking rules mExecutor.submit(() -> { synchronized (mBlockerLocker) { waitForBlockerOrExit(); mBlocker.setMutable(); for (AppDetailsPermissionItem permItem : revokedPermissions) { mBlocker.setPermission(permItem.name, permItem.permission.isGranted(), permItem.permission.getFlags()); } mBlocker.commit(); mBlocker.setReadOnly(); mBlockerLocker.notifyAll(); } }); return isSuccessful; } @NonNull private final AppOpsManagerCompat mAppOpsManager = new AppOpsManagerCompat(); @WorkerThread @GuardedBy("blockerLocker") public boolean setAppOp(int op, int mode) { if (mExternalApk) return false; PackageInfo packageInfo = getPackageInfoInternal(); if (packageInfo == null) return false; try { // Set mode PermUtils.setAppOpMode(mAppOpsManager, op, mPackageName, packageInfo.applicationInfo.uid, mode); mExecutor.submit(() -> { synchronized (mBlockerLocker) { waitForBlockerOrExit(); mBlocker.setMutable(); mBlocker.setAppOp(op, mode); mBlocker.commit(); mBlocker.setReadOnly(); mBlockerLocker.notifyAll(); } }); } catch (PermissionException e) { e.printStackTrace(); return false; } return true; } @WorkerThread @GuardedBy("blockerLocker") public boolean setAppOpMode(AppDetailsAppOpItem appOpItem) { if (mExternalApk) return false; PackageInfo packageInfo = getPackageInfoInternal(); if (packageInfo == null) return false; try { if (appOpItem.isAllowed()) { appOpItem.disallowAppOp(packageInfo, mAppOpsManager); } else { appOpItem.allowAppOp(packageInfo, mAppOpsManager); } setAppOp(appOpItem); mExecutor.submit(() -> { synchronized (mBlockerLocker) { waitForBlockerOrExit(); mBlocker.setMutable(); mBlocker.setAppOp(appOpItem.getOp(), appOpItem.getMode()); mBlocker.commit(); mBlocker.setReadOnly(); mBlockerLocker.notifyAll(); } }); return true; } catch (PermissionException e) { e.printStackTrace(); return false; } } @WorkerThread @GuardedBy("blockerLocker") public boolean setAppOpMode(AppDetailsAppOpItem appOpItem, @AppOpsManagerCompat.Mode int mode) { if (mExternalApk) return false; PackageInfo packageInfo = getPackageInfoInternal(); if (packageInfo == null) return false; try { appOpItem.setAppOp(packageInfo, mAppOpsManager, mode); setAppOp(appOpItem); mExecutor.submit(() -> { synchronized (mBlockerLocker) { waitForBlockerOrExit(); mBlocker.setMutable(); mBlocker.setAppOp(appOpItem.getOp(), appOpItem.getMode()); mBlocker.commit(); mBlocker.setReadOnly(); mBlockerLocker.notifyAll(); } }); return true; } catch (PermissionException e) { e.printStackTrace(); return false; } } @WorkerThread @GuardedBy("blockerLocker") public boolean resetAppOps() { if (mExternalApk) return false; if (getPackageInfoInternal() == null || mPackageName == null) return false; try { mAppOpsManager.resetAllModes(mUserId, mPackageName); mExecutor.submit(this::loadAppOps); // Save values to the blocking rules mExecutor.submit(() -> { synchronized (mBlockerLocker) { waitForBlockerOrExit(); List appOpEntries = mBlocker.getAll(AppOpRule.class); mBlocker.setMutable(); for (AppOpRule entry : appOpEntries) mBlocker.removeEntry(entry); mBlocker.commit(); mBlocker.setReadOnly(); mBlockerLocker.notifyAll(); } }); return true; } catch (Exception e) { e.printStackTrace(); } return false; } @WorkerThread @GuardedBy("blockerLocker") public boolean ignoreDangerousAppOps() { if (mExternalApk) return false; PackageInfo packageInfo = getPackageInfoInternal(); if (packageInfo == null) return false; String permName; final List opItems = new ArrayList<>(); boolean isSuccessful = true; synchronized (mAppOpItems) { for (AppDetailsAppOpItem mAppOpItem : mAppOpItems) { try { permName = AppOpsManagerCompat.opToPermission(mAppOpItem.getOp()); if (permName != null) { PermissionInfo permissionInfo = mPackageManager.getPermissionInfo(permName, PackageManager.GET_META_DATA); int basePermissionType = PermissionInfoCompat.getProtection(permissionInfo); if (basePermissionType == PermissionInfo.PROTECTION_DANGEROUS) { // Set mode try { PermUtils.setAppOpMode(mAppOpsManager, mAppOpItem.getOp(), mPackageName, packageInfo.applicationInfo.uid, AppOpsManager.MODE_IGNORED); opItems.add(mAppOpItem.getOp()); mAppOpItem.invalidate(mAppOpsManager, packageInfo); } catch (PermissionException e) { e.printStackTrace(); isSuccessful = false; break; } } } } catch (PackageManager.NameNotFoundException | IllegalArgumentException | IndexOutOfBoundsException ignore) { } } } // Save values to the blocking rules mExecutor.submit(() -> { synchronized (mBlockerLocker) { waitForBlockerOrExit(); mBlocker.setMutable(); for (int op : opItems) mBlocker.setAppOp(op, AppOpsManager.MODE_IGNORED); mBlocker.commit(); mBlocker.setReadOnly(); mBlockerLocker.notifyAll(); } }); return isSuccessful; } @AnyThread @GuardedBy("blockerLocker") public void applyRules() { if (mExternalApk) return; mExecutor.submit(() -> { synchronized (mBlockerLocker) { waitForBlockerOrExit(); boolean oldIsRulesApplied = mBlocker.isRulesApplied(); mBlocker.setMutable(); mBlocker.applyRules(!oldIsRulesApplied); mBlocker.commit(); mBlocker.setReadOnly(); reloadComponents(); setRuleApplicationStatus(); mBlockerLocker.notifyAll(); } }); } @UiThread public LiveData>> get(@AppDetailsFragment.Property int property) { switch (property) { case AppDetailsFragment.ACTIVITIES: return observeInternal(mActivities); case AppDetailsFragment.SERVICES: return observeInternal(mServices); case AppDetailsFragment.RECEIVERS: return observeInternal(mReceivers); case AppDetailsFragment.PROVIDERS: return observeInternal(mProviders); case AppDetailsFragment.APP_OPS: return observeInternal(mAppOps); case AppDetailsFragment.USES_PERMISSIONS: return observeInternal(mUsesPermissions); case AppDetailsFragment.PERMISSIONS: return observeInternal(mPermissions); case AppDetailsFragment.FEATURES: return observeInternal(mFeatures); case AppDetailsFragment.CONFIGURATIONS: return observeInternal(mConfigurations); case AppDetailsFragment.SIGNATURES: return observeInternal(mSignatures); case AppDetailsFragment.SHARED_LIBRARIES: return observeInternal(mSharedLibraries); case AppDetailsFragment.OVERLAYS: return observeInternal(mOverlays); case AppDetailsFragment.APP_INFO: return observeInternal(mAppInfo); default: throw new IllegalArgumentException("Invalid property: " + property); } } @SuppressWarnings("unchecked") @AnyThread @NonNull private MutableLiveData>> observeInternal(@NonNull MutableLiveData liveData) { return (MutableLiveData>>) liveData; } @AnyThread public void load(@AppDetailsFragment.Property int property) { mExecutor.submit(() -> { Optional.ofNullable(mReceiver).ifPresent(PackageIntentReceiver::pauseWatcher); switch (property) { case AppDetailsFragment.ACTIVITIES: loadActivities(); break; case AppDetailsFragment.SERVICES: loadServices(); break; case AppDetailsFragment.RECEIVERS: loadReceivers(); break; case AppDetailsFragment.PROVIDERS: loadProviders(); break; case AppDetailsFragment.APP_OPS: loadAppOps(); break; case AppDetailsFragment.USES_PERMISSIONS: loadUsesPermissions(); break; case AppDetailsFragment.PERMISSIONS: loadPermissions(); break; case AppDetailsFragment.FEATURES: loadFeatures(); break; case AppDetailsFragment.CONFIGURATIONS: loadConfigurations(); break; case AppDetailsFragment.SIGNATURES: loadSignatures(); break; case AppDetailsFragment.SHARED_LIBRARIES: loadSharedLibraries(); break; case AppDetailsFragment.OVERLAYS: loadOverlays(); break; case AppDetailsFragment.APP_INFO: loadAppInfo(); break; } Optional.ofNullable(mReceiver).ifPresent(PackageIntentReceiver::resumeWatcher); }); } private final MutableLiveData mIsPackageExistLiveData = new MutableLiveData<>(); private boolean mIsPackageExist = true; @UiThread public LiveData getIsPackageExistLiveData() { if (mIsPackageExistLiveData.getValue() == null) mIsPackageExistLiveData.setValue(mIsPackageExist); return mIsPackageExistLiveData; } @AnyThread public boolean isPackageExist() { return mIsPackageExist; } @NonNull private final MutableLiveData mPackageChanged = new MutableLiveData<>(); @UiThread public LiveData isPackageChanged() { if (mPackageChanged.getValue() == null) { mPackageChanged.setValue(false); } return mPackageChanged; } @AnyThread public void triggerPackageChange() { mExecutor.submit(this::setPackageChanged); } @WorkerThread @GuardedBy("blockerLocker") public void setPackageChanged() { // TODO: 16/3/23 Synchronization is needed somewhere setPackageInfo(true); if (mExternalApk || mExecutor.isShutdown() || mExecutor.isTerminated()) return; mExecutor.submit(() -> { synchronized (mBlockerLocker) { try { waitForBlockerOrExit(); // Reload app components mBlocker.reloadComponents(); } finally { mBlockerLocker.notifyAll(); } } }); } @AnyThread public boolean isExternalApk() { return mExternalApk; } @AnyThread public int getSplitCount() { if (mApkFile != null && mApkFile.isSplit()) { return mApkFile.getEntries().size() - 1; } return 0; } @WorkerThread @GuardedBy("blockerLocker") private void waitForBlockerOrExit() { if (mExternalApk) return; if (mBlocker == null) { try { while (mWaitForBlocker) mBlockerLocker.wait(); } catch (InterruptedException e) { e.printStackTrace(); Thread.currentThread().interrupt(); } } } @WorkerThread private void reloadComponents() { mExecutor.submit(() -> { Optional.ofNullable(mReceiver).ifPresent(PackageIntentReceiver::pauseWatcher); loadActivities(); loadServices(); loadReceivers(); loadProviders(); loadOverlays(); Optional.ofNullable(mReceiver).ifPresent(PackageIntentReceiver::resumeWatcher); }); } @SuppressLint("WrongConstant") @WorkerThread private void setPackageInfo(boolean reload) { // Package name cannot be null if (mPackageName == null) return; // Wait for component blocker to appear synchronized (mBlockerLocker) { waitForBlockerOrExit(); } if (!reload && mPackageInfo != null) return; try { try { mInstalledPackageInfo = PackageManagerCompat.getPackageInfo(mPackageName, PackageManager.GET_META_DATA | PackageManager.GET_PERMISSIONS | PackageManager.GET_ACTIVITIES | MATCH_DISABLED_COMPONENTS | PackageManager.GET_RECEIVERS | PackageManager.GET_PROVIDERS | MATCH_UNINSTALLED_PACKAGES | PackageManager.GET_SERVICES | PackageManager.GET_CONFIGURATIONS | GET_SIGNING_CERTIFICATES | PackageManager.GET_SHARED_LIBRARY_FILES | PackageManager.GET_URI_PERMISSION_PATTERNS | PackageManagerCompat.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES, mUserId); if (!ApplicationInfoCompat.isInstalled(mInstalledPackageInfo.applicationInfo)) { throw new ApkFile.ApkFileException("App not installed. It only has data."); } } catch (Throwable e) { Log.e(TAG, e); mInstalledPackageInfo = null; } if (mExternalApk) { // Do not get signatures via Android framework as it will simply return NULL without any clarifications. // All signatures are fetched using PackageUtils where a fallback method is used in case the PackageInfo // didn't load any signature. So, we should be safe from any harm. mPackageInfo = mPackageManager.getPackageArchiveInfo(mApkPath, PackageManager.GET_PERMISSIONS | PackageManager.GET_ACTIVITIES | PackageManager.GET_RECEIVERS | PackageManager.GET_PROVIDERS | PackageManager.GET_SERVICES | MATCH_DISABLED_COMPONENTS | PackageManager.GET_CONFIGURATIONS | PackageManager.GET_SHARED_LIBRARY_FILES | PackageManager.GET_URI_PERMISSION_PATTERNS | PackageManager.GET_META_DATA); if (mPackageInfo == null) { throw new PackageManager.NameNotFoundException("Package cannot be parsed"); } if (mInstalledPackageInfo == null) { Log.d(TAG, "%s not installed for user %d", mPackageName, mUserId); } mPackageInfo.applicationInfo.sourceDir = mApkPath; mPackageInfo.applicationInfo.publicSourceDir = mApkPath; } else { mPackageInfo = mInstalledPackageInfo; if (mPackageInfo == null) { throw new PackageManager.NameNotFoundException("Package not installed"); } } mIsPackageExistLiveData.postValue(mIsPackageExist = true); } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, e); mIsPackageExistLiveData.postValue(mIsPackageExist = false); } catch (Throwable e) { Log.e(TAG, e); } finally { mPackageChanged.postValue(true); } } @WorkerThread @Nullable private PackageInfo getPackageInfoInternal() { try { mPackageInfoWatcher.await(); } catch (InterruptedException e) { return null; } return mPackageInfo; } @AnyThread @Nullable public PackageInfo getPackageInfo() { return mPackageInfo; } @AnyThread @Nullable public PackageInfo getInstalledPackageInfo() { return mInstalledPackageInfo; } @NonNull public LiveData getUserInfo() { MutableLiveData userInfoMutableLiveData = new MutableLiveData<>(); mExecutor.submit(() -> { if (mExternalApk) { return; } final List userInfoList = Users.getUsers(); if (userInfoList.size() > 1) { for (UserInfo userInfo : userInfoList) { if (userInfo.id == mUserId) { userInfoMutableLiveData.postValue(userInfo); break; } } } }); return userInfoMutableLiveData; } @NonNull private final MutableLiveData>> mAppInfo = new MutableLiveData<>(); @WorkerThread private void loadAppInfo() { PackageInfo packageInfo = getPackageInfoInternal(); if (packageInfo == null) { mAppInfo.postValue(null); return; } AppDetailsItem appDetailsItem = new AppDetailsItem<>(packageInfo); appDetailsItem.name = packageInfo.packageName; List> appDetailsItems = Collections.singletonList(appDetailsItem); mAppInfo.postValue(appDetailsItems); } @NonNull private final MutableLiveData>> mActivities = new MutableLiveData<>(); @NonNull private final List> mActivityItems = new ArrayList<>(); @WorkerThread @GuardedBy("blockerLocker") private void loadActivities() { synchronized (mActivityItems) { mActivityItems.clear(); PackageInfo packageInfo = getPackageInfoInternal(); if (packageInfo == null || packageInfo.activities == null) { mActivities.postValue(mActivityItems); return; } CharSequence appLabel = packageInfo.applicationInfo.loadLabel(mPackageManager); boolean canStartAnyActivity = SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.START_ANY_ACTIVITY); boolean canStartViaAssist = UserHandleHidden.myUserId() == mUserId && SelfPermissions.checkSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS); for (ActivityInfo activityInfo : packageInfo.activities) { AppDetailsActivityItem componentItem = new AppDetailsActivityItem(activityInfo); componentItem.label = getComponentLabel(activityInfo, appLabel); synchronized (mBlockerLocker) { if (!mExternalApk) { componentItem.setRule(mBlocker.getComponent(activityInfo.name)); } } componentItem.setTracker(ComponentUtils.isTracker(activityInfo.name)); componentItem.setDisabled(isComponentDisabled(activityInfo)); // An activity is allowed to launch only if it's // 1) Not from an external APK // 2) Root enabled or the activity is exportable // 3) App or the activity is not disabled and/or blocked componentItem.canLaunch = !mExternalApk && (canStartAnyActivity || activityInfo.exported) && !componentItem.isDisabled() && !componentItem.isBlocked(); componentItem.canLaunchAssist = !mExternalApk && canStartViaAssist && !componentItem.isDisabled() && !componentItem.isBlocked(); mActivityItems.add(componentItem); } mActivities.postValue(filterAndSortComponents(mActivityItems)); } } @NonNull private final MutableLiveData>> mServices = new MutableLiveData<>(); @NonNull private final List> mServiceItems = new ArrayList<>(); @WorkerThread @GuardedBy("blockerLocker") private void loadServices() { synchronized (mServiceItems) { mServiceItems.clear(); PackageInfo packageInfo = getPackageInfoInternal(); if (packageInfo == null || packageInfo.services == null) { // There are no services mServices.postValue(Collections.emptyList()); return; } List runningServiceInfoList; runningServiceInfoList = ActivityManagerCompat.getRunningServices(mPackageName, mUserId); CharSequence appLabel = packageInfo.applicationInfo.loadLabel(mPackageManager); for (ServiceInfo serviceInfo : packageInfo.services) { AppDetailsServiceItem serviceItem = new AppDetailsServiceItem(serviceInfo); serviceItem.label = getComponentLabel(serviceInfo, appLabel); synchronized (mBlockerLocker) { if (!mExternalApk) { serviceItem.setRule(mBlocker.getComponent(serviceInfo.name)); } } serviceItem.setTracker(ComponentUtils.isTracker(serviceInfo.name)); serviceItem.setDisabled(isComponentDisabled(serviceInfo)); for (ActivityManager.RunningServiceInfo runningServiceInfo : runningServiceInfoList) { if (runningServiceInfo.service.getClassName().equals(serviceInfo.name)) { serviceItem.setRunningServiceInfo(runningServiceInfo); } } // A service is allowed to launch only if it's // 1) Not from an external APK // 2) Root enabled or the service is exportable without any permission // 3) App or the service is not disabled and/or blocked serviceItem.canLaunch = !mExternalApk && canLaunchService(serviceInfo) && !serviceItem.isDisabled() && !serviceItem.isBlocked(); mServiceItems.add(serviceItem); } mServices.postValue(filterAndSortComponents(mServiceItems)); } } @NonNull private final MutableLiveData>> mReceivers = new MutableLiveData<>(); @NonNull private final List> mReceiverItems = new ArrayList<>(); @WorkerThread @GuardedBy("blockerLocker") private void loadReceivers() { synchronized (mReceiverItems) { mReceiverItems.clear(); PackageInfo packageInfo = getPackageInfoInternal(); if (packageInfo == null || packageInfo.receivers == null) { // There are no receivers mReceivers.postValue(Collections.emptyList()); return; } CharSequence appLabel = packageInfo.applicationInfo.loadLabel(mPackageManager); for (ActivityInfo activityInfo : packageInfo.receivers) { AppDetailsComponentItem componentItem = new AppDetailsComponentItem(activityInfo); componentItem.label = getComponentLabel(activityInfo, appLabel); synchronized (mBlockerLocker) { if (!mExternalApk) { componentItem.setRule(mBlocker.getComponent(activityInfo.name)); } } componentItem.setTracker(ComponentUtils.isTracker(activityInfo.name)); componentItem.setDisabled(isComponentDisabled(activityInfo)); mReceiverItems.add(componentItem); } mReceivers.postValue(filterAndSortComponents(mReceiverItems)); } } @NonNull private final MutableLiveData>> mProviders = new MutableLiveData<>(); @NonNull private final List> mProviderItems = new ArrayList<>(); @WorkerThread @GuardedBy("blockerLocker") private void loadProviders() { synchronized (mProviderItems) { mProviderItems.clear(); PackageInfo packageInfo = getPackageInfoInternal(); if (packageInfo == null || packageInfo.providers == null) { // There are no providers mProviders.postValue(Collections.emptyList()); return; } CharSequence appLabel = packageInfo.applicationInfo.loadLabel(mPackageManager); for (ProviderInfo providerInfo : packageInfo.providers) { AppDetailsComponentItem componentItem = new AppDetailsComponentItem(providerInfo); componentItem.label = getComponentLabel(providerInfo, appLabel); synchronized (mBlockerLocker) { if (!mExternalApk) { componentItem.setRule(mBlocker.getComponent(providerInfo.name)); } } componentItem.setTracker(ComponentUtils.isTracker(providerInfo.name)); componentItem.setDisabled(isComponentDisabled(providerInfo)); mProviderItems.add(componentItem); } mProviders.postValue(filterAndSortComponents(mProviderItems)); } } @NonNull private CharSequence getComponentLabel(@NonNull ComponentInfo componentInfo, @NonNull CharSequence appLabel) { CharSequence componentLabel = componentInfo.loadLabel(mPackageManager); if (componentLabel.equals(componentInfo.name) || componentLabel.equals(appLabel)) { // Component label is as good as null componentLabel = null; } return componentLabel != null ? componentLabel : Utils.camelCaseToSpaceSeparatedString( Utils.getLastComponent(componentInfo.name)); } @SuppressLint("SwitchIntDef") @WorkerThread private void sortComponents(List> appDetailsItems) { // First sort by name Collections.sort(appDetailsItems, (o1, o2) -> o1.name.compareToIgnoreCase(o2.name)); if (mSortOrderComponents == AppDetailsFragment.SORT_BY_NAME) return; Collections.sort(appDetailsItems, (o1, o2) -> { switch (mSortOrderComponents) { // No need to sort by name since we've already done it case AppDetailsFragment.SORT_BY_BLOCKED: return -Boolean.compare( ((AppDetailsComponentItem) o1).isBlocked(), ((AppDetailsComponentItem) o2).isBlocked()); case AppDetailsFragment.SORT_BY_TRACKERS: return -Boolean.compare( ((AppDetailsComponentItem) o1).isTracker(), ((AppDetailsComponentItem) o2).isTracker()); } return 0; }); } public boolean isComponentDisabled(@NonNull ComponentInfo componentInfo) { if (mInstalledPackageInfo == null || FreezeUtils.isFrozen(mInstalledPackageInfo.applicationInfo)) { return true; } ComponentName componentName = new ComponentName(componentInfo.packageName, componentInfo.name); try { int componentEnabledSetting = PackageManagerCompat.getComponentEnabledSetting(componentName, mUserId); switch (componentEnabledSetting) { case PackageManager.COMPONENT_ENABLED_STATE_DISABLED: case PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED: case PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER: return true; case PackageManager.COMPONENT_ENABLED_STATE_ENABLED: return false; case PackageManager.COMPONENT_ENABLED_STATE_DEFAULT: default: } } catch (Throwable ignore) { } return !componentInfo.isEnabled(); } private static boolean canLaunchService(@NonNull ServiceInfo info) { if (info.exported && info.permission == null) { return true; } int uid = Users.getSelfOrRemoteUid(); if (uid == Ops.ROOT_UID || (uid == Ops.SYSTEM_UID && info.permission == null)) { return true; } if (info.permission == null) { return false; } return SelfPermissions.checkSelfOrRemotePermission(info.permission, uid); } @NonNull private final MutableLiveData> mAppOps = new MutableLiveData<>(); @NonNull private final List mAppOpItems = new ArrayList<>(); @WorkerThread public void setAppOp(AppDetailsAppOpItem appDetailsItem) { synchronized (mAppOpItems) { for (int i = 0; i < mAppOpItems.size(); ++i) { if (mAppOpItems.get(i).name.equals(appDetailsItem.name)) { mAppOpItems.set(i, appDetailsItem); break; } } } } @WorkerThread private void loadAppOps() { PackageInfo packageInfo = getPackageInfoInternal(); if (packageInfo == null || mExternalApk || !SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.GET_APP_OPS_STATS)) { mAppOps.postValue(Collections.emptyList()); return; } boolean canGetGrantRevokeRuntimePermissions = SelfPermissions.checkGetGrantRevokeRuntimePermissions(); synchronized (mAppOpItems) { mAppOpItems.clear(); try { int uid = packageInfo.applicationInfo.uid; String packageName = packageInfo.packageName; HashMap opToOpEntryMap = new HashMap<>(AppOpsManagerCompat._NUM_OP); for (AppOpsManagerCompat.OpEntry opEntry : AppOpsManagerCompat .getConfiguredOpsForPackage(mAppOpsManager, packageName, uid)) { if (opToOpEntryMap.get(opEntry.getOp()) == null) { opToOpEntryMap.put(opEntry.getOp(), opEntry); } } // Include from permissions List permissions = getRawPermissions(); HashSet otherOps = new HashSet<>(AppOpsManagerCompat._NUM_OP); for (String permission : permissions) { int op = AppOpsManagerCompat.permissionToOpCode(permission); if (op == AppOpsManagerCompat.OP_NONE || op >= AppOpsManagerCompat._NUM_OP || opToOpEntryMap.get(op) != null) { // Invalid/unsupported app operation continue; } otherOps.add(op); } // Include defaults i.e. app ops without any associated permissions if requested if (Prefs.AppDetailsPage.displayDefaultAppOps()) { for (int op : AppOpsManagerCompat.getOpsWithoutPermissions()) { if (op == AppOpsManagerCompat.OP_NONE || op >= AppOpsManagerCompat._NUM_OP || opToOpEntryMap.get(op) != null) { // Invalid/unsupported app operation continue; } otherOps.add(op); } } for (AppOpsManagerCompat.OpEntry entry : opToOpEntryMap.values()) { AppDetailsAppOpItem appDetailsItem; String permissionName = AppOpsManagerCompat.opToPermission(entry.getOp()); if (permissionName != null) { boolean isGranted = PermissionCompat.checkPermission(permissionName, packageName, mUserId) == PackageManager.PERMISSION_GRANTED; int permissionFlags = canGetGrantRevokeRuntimePermissions ? PermissionCompat.getPermissionFlags(permissionName, packageName, mUserId) : PermissionCompat.FLAG_PERMISSION_NONE; PermissionInfo permissionInfo = PermissionCompat.getPermissionInfo(permissionName, packageName, 0); if (permissionInfo == null) { permissionInfo = new PermissionInfo(); permissionInfo.name = permissionName; } appDetailsItem = new AppDetailsAppOpItem(entry, permissionInfo, isGranted, permissionFlags, permissions.contains(permissionName)); } else { appDetailsItem = new AppDetailsAppOpItem(entry); } mAppOpItems.add(appDetailsItem); } // Add other ops for (int op : otherOps) { AppDetailsAppOpItem appDetailsItem; String permissionName = AppOpsManagerCompat.opToPermission(op); if (permissionName != null) { boolean isGranted = PermissionCompat.checkPermission(permissionName, packageName, mUserId) == PackageManager.PERMISSION_GRANTED; int permissionFlags = canGetGrantRevokeRuntimePermissions ? PermissionCompat.getPermissionFlags(permissionName, packageName, mUserId) : PermissionCompat.FLAG_PERMISSION_NONE; PermissionInfo permissionInfo = PermissionCompat.getPermissionInfo(permissionName, packageName, 0); if (permissionInfo == null) { permissionInfo = new PermissionInfo(); permissionInfo.name = permissionName; } appDetailsItem = new AppDetailsAppOpItem(op, permissionInfo, isGranted, permissionFlags, permissions.contains(permissionName)); } else { appDetailsItem = new AppDetailsAppOpItem(op); } mAppOpItems.add(appDetailsItem); } } catch (Throwable e) { e.printStackTrace(); } } filterAndSortItemsInternal(AppDetailsFragment.APP_OPS); } @NonNull private final MutableLiveData>> mUsesPermissions = new MutableLiveData<>(); private final List mUsesPermissionItems = new ArrayList<>(); @WorkerThread public void setUsesPermission(AppDetailsPermissionItem appDetailsPermissionItem) { AppDetailsPermissionItem permissionItem; synchronized (mUsesPermissionItems) { for (int i = 0; i < mUsesPermissionItems.size(); ++i) { permissionItem = mUsesPermissionItems.get(i); if (permissionItem.name.equals(appDetailsPermissionItem.name)) { mUsesPermissionItems.set(i, appDetailsPermissionItem); break; } } } } @SuppressLint("SwitchIntDef") @WorkerThread private void loadUsesPermissions() { synchronized (mUsesPermissionItems) { mUsesPermissionItems.clear(); PackageInfo packageInfo = getPackageInfoInternal(); if (packageInfo == null || packageInfo.requestedPermissions == null) { // No requested permissions mUsesPermissions.postValue(Collections.emptyList()); return; } List opEntries = ExUtils.requireNonNullElse(() -> AppOpsManagerCompat .getConfiguredOpsForPackage(mAppOpsManager, packageInfo.packageName, packageInfo.applicationInfo.uid), Collections.emptyList()); for (int i = 0; i < packageInfo.requestedPermissions.length; ++i) { AppDetailsPermissionItem permissionItem = getPermissionItem(packageInfo.requestedPermissions[i], (packageInfo.requestedPermissionsFlags[i] & PackageInfo.REQUESTED_PERMISSION_GRANTED) != 0, opEntries); if (permissionItem != null) { mUsesPermissionItems.add(permissionItem); } } } filterAndSortItemsInternal(AppDetailsFragment.USES_PERMISSIONS); } @WorkerThread public List getRawPermissions() { List rawPermissions = new ArrayList<>(); PackageInfo packageInfo = getPackageInfoInternal(); if (packageInfo != null && packageInfo.requestedPermissions != null) { rawPermissions.addAll(Arrays.asList(packageInfo.requestedPermissions)); } return rawPermissions; } @Nullable private AppDetailsPermissionItem getPermissionItem(@NonNull String permissionName, boolean isGranted, @NonNull List opEntries) { PackageInfo packageInfo = getPackageInfoInternal(); if (packageInfo == null) return null; try { PermissionInfo permissionInfo = PermissionCompat.getPermissionInfo(permissionName, packageInfo.packageName, PackageManager.GET_META_DATA); if (permissionInfo == null) { Log.d(TAG, "Couldn't fetch info for permission %s", permissionName); permissionInfo = new PermissionInfo(); permissionInfo.name = permissionName; } int flags = permissionInfo.flags; int appOp = AppOpsManagerCompat.permissionToOpCode(permissionName); int permissionFlags; boolean appOpAllowed = false; if (!mExternalApk && SelfPermissions.checkGetGrantRevokeRuntimePermissions()) { permissionFlags = PermissionCompat.getPermissionFlags( permissionName, packageInfo.packageName, mUserId); } else permissionFlags = PermissionCompat.FLAG_PERMISSION_NONE; if (!mExternalApk && appOp != AppOpsManagerCompat.OP_NONE) { int mode = AppOpsManagerCompat.getModeFromOpEntriesOrDefault(appOp, opEntries); appOpAllowed = mode == AppOpsManager.MODE_ALLOWED; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { appOpAllowed |= mode == AppOpsManager.MODE_FOREGROUND; } } int protection = PermissionInfoCompat.getProtection(permissionInfo); int protectionFlags = PermissionInfoCompat.getProtectionFlags(permissionInfo); Permission permission; if (protection == PermissionInfo.PROTECTION_DANGEROUS && PermUtils.systemSupportsRuntimePermissions()) { permission = new RuntimePermission(permissionName, isGranted, appOp, appOpAllowed, permissionFlags); } else if ((protectionFlags & PermissionInfo.PROTECTION_FLAG_DEVELOPMENT) != 0) { permission = new DevelopmentPermission(permissionName, isGranted, appOp, appOpAllowed, permissionFlags); } else { permission = new ReadOnlyPermission(permissionName, isGranted, appOp, appOpAllowed, permissionFlags); } AppDetailsPermissionItem appDetailsItem = new AppDetailsPermissionItem(permissionInfo, permission, flags); appDetailsItem.name = permissionName; return appDetailsItem; } catch (Throwable th) { th.printStackTrace(); return null; } } @NonNull private final MutableLiveData>> mPermissions = new MutableLiveData<>(); private final List> mPermissionItems = new ArrayList<>(); @WorkerThread private void loadPermissions() { synchronized (mPermissionItems) { mPermissionItems.clear(); PackageInfo packageInfo = getPackageInfoInternal(); if (packageInfo == null) { // No custom permissions mPermissions.postValue(mPermissionItems); return; } Set visitedPerms = new HashSet<>(); if (packageInfo.permissions != null) { for (PermissionInfo permissionInfo : packageInfo.permissions) { AppDetailsDefinedPermissionItem appDetailsItem = new AppDetailsDefinedPermissionItem(permissionInfo, false); mPermissionItems.add(appDetailsItem); visitedPerms.add(permissionInfo.name); } } if (packageInfo.activities != null) { for (ActivityInfo activityInfo : packageInfo.activities) { if (activityInfo.permission != null && !visitedPerms.contains(activityInfo.permission)) { try { PermissionInfo permissionInfo = PermissionCompat.getPermissionInfo(activityInfo.permission, packageInfo.packageName, PackageManager.GET_META_DATA); if (permissionInfo == null) { Log.d(TAG, "Couldn't fetch info for permission %s", activityInfo.permission); permissionInfo = new PermissionInfo(); permissionInfo.name = activityInfo.permission; } AppDetailsDefinedPermissionItem appDetailsItem = new AppDetailsDefinedPermissionItem(permissionInfo, true); mPermissionItems.add(appDetailsItem); visitedPerms.add(permissionInfo.name); } catch (RemoteException e) { e.printStackTrace(); } } } } if (packageInfo.services != null) { for (ServiceInfo serviceInfo : packageInfo.services) { if (serviceInfo.permission != null && !visitedPerms.contains(serviceInfo.permission)) { try { PermissionInfo permissionInfo = PermissionCompat.getPermissionInfo(serviceInfo.permission, packageInfo.packageName, PackageManager.GET_META_DATA); if (permissionInfo == null) { Log.d(TAG, "Couldn't fetch info for permission %s", serviceInfo.permission); permissionInfo = new PermissionInfo(); permissionInfo.name = serviceInfo.permission; } AppDetailsDefinedPermissionItem appDetailsItem = new AppDetailsDefinedPermissionItem(permissionInfo, true); mPermissionItems.add(appDetailsItem); visitedPerms.add(permissionInfo.name); } catch (RemoteException e) { e.printStackTrace(); } } } } if (packageInfo.providers != null) { for (ProviderInfo providerInfo : packageInfo.providers) { if (providerInfo.readPermission != null && !visitedPerms.contains(providerInfo.readPermission)) { try { PermissionInfo permissionInfo = PermissionCompat.getPermissionInfo(providerInfo.readPermission, packageInfo.packageName, PackageManager.GET_META_DATA); if (permissionInfo == null) { Log.d(TAG, "Couldn't fetch info for permission %s", providerInfo.readPermission); permissionInfo = new PermissionInfo(); permissionInfo.name = providerInfo.readPermission; } AppDetailsDefinedPermissionItem appDetailsItem = new AppDetailsDefinedPermissionItem(permissionInfo, true); mPermissionItems.add(appDetailsItem); visitedPerms.add(permissionInfo.name); } catch (RemoteException e) { e.printStackTrace(); } } if (providerInfo.writePermission != null && !visitedPerms.contains(providerInfo.writePermission)) { try { PermissionInfo permissionInfo = PermissionCompat.getPermissionInfo(providerInfo.writePermission, packageInfo.packageName, PackageManager.GET_META_DATA); if (permissionInfo == null) { Log.d(TAG, "Couldn't fetch info for permission %s", providerInfo.writePermission); permissionInfo = new PermissionInfo(); permissionInfo.name = providerInfo.writePermission; } AppDetailsDefinedPermissionItem appDetailsItem = new AppDetailsDefinedPermissionItem(permissionInfo, true); mPermissionItems.add(appDetailsItem); visitedPerms.add(permissionInfo.name); } catch (RemoteException e) { e.printStackTrace(); } } } } if (packageInfo.receivers != null) { for (ActivityInfo activityInfo : packageInfo.receivers) { if (activityInfo.permission != null && !visitedPerms.contains(activityInfo.permission)) { try { PermissionInfo permissionInfo = PermissionCompat.getPermissionInfo(activityInfo.permission, packageInfo.packageName, PackageManager.GET_META_DATA); if (permissionInfo == null) { Log.d(TAG, "Couldn't fetch info for permission %s", activityInfo.permission); permissionInfo = new PermissionInfo(); permissionInfo.name = activityInfo.permission; } AppDetailsDefinedPermissionItem appDetailsItem = new AppDetailsDefinedPermissionItem(permissionInfo, true); mPermissionItems.add(appDetailsItem); visitedPerms.add(permissionInfo.name); } catch (RemoteException e) { e.printStackTrace(); } } } } mPermissions.postValue(filterAndSortPermissions(mPermissionItems)); } } @NonNull private final MutableLiveData> mFeatures = new MutableLiveData<>(); @WorkerThread private void loadFeatures() { List appDetailsItems = new ArrayList<>(); PackageInfo packageInfo = getPackageInfoInternal(); if (packageInfo == null || packageInfo.reqFeatures == null) { // No required features mFeatures.postValue(appDetailsItems); return; } for (FeatureInfo fi : packageInfo.reqFeatures) { if (fi.name == null) fi.name = AppDetailsFeatureItem.OPEN_GL_ES; } for (FeatureInfo featureInfo : packageInfo.reqFeatures) { String name = featureInfo.name; boolean isAvailable; if (name == null) { // At most, only one name could be null name = AppDetailsFeatureItem.OPEN_GL_ES; ActivityManager activityManager = (ActivityManager) getApplication().getSystemService(Context.ACTIVITY_SERVICE); int glEsVersion = activityManager.getDeviceConfigurationInfo().reqGlEsVersion; isAvailable = featureInfo.reqGlEsVersion <= glEsVersion; } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { isAvailable = mPackageManager.hasSystemFeature(featureInfo.name, featureInfo.version); } else { isAvailable = mPackageManager.hasSystemFeature(featureInfo.name); } AppDetailsFeatureItem appDetailsItem = new AppDetailsFeatureItem(featureInfo, isAvailable); appDetailsItems.add(appDetailsItem); appDetailsItem.name = name; } Collections.sort(appDetailsItems, (o1, o2) -> o1.name.compareToIgnoreCase(o2.name)); mFeatures.postValue(appDetailsItems); } @NonNull private final MutableLiveData>> mConfigurations = new MutableLiveData<>(); @WorkerThread private void loadConfigurations() { List> appDetailsItems = new ArrayList<>(); PackageInfo packageInfo = getPackageInfoInternal(); if (packageInfo != null && packageInfo.configPreferences != null) { for (ConfigurationInfo configurationInfo : packageInfo.configPreferences) { AppDetailsItem appDetailsItem = new AppDetailsItem<>(configurationInfo); appDetailsItems.add(appDetailsItem); } } mConfigurations.postValue(appDetailsItems); } @NonNull private final MutableLiveData>> mSignatures = new MutableLiveData<>(); private ApkVerifier.Result mApkVerifierResult; @AnyThread public ApkVerifier.Result getApkVerifierResult() { return mApkVerifierResult; } @WorkerThread private void loadSignatures() { List> appDetailsItems = new ArrayList<>(); PackageInfo packageInfo = getPackageInfoInternal(); if (packageInfo == null || mApkFile == null) { mSignatures.postValue(appDetailsItems); return; } try { File idsigFile = mApkFile.getIdsigFile(); ApkVerifier.Builder builder = new ApkVerifier.Builder(mApkFile.getBaseEntry().getFile(false)) .setMaxCheckedPlatformVersion(Build.VERSION.SDK_INT); if (idsigFile != null) { builder.setV4SignatureFile(idsigFile); } ApkVerifier apkVerifier = builder.build(); mApkVerifierResult = apkVerifier.verify(); SignerInfo signerInfo = new SignerInfo(mApkVerifierResult); // Get signer certificates X509Certificate[] certificates = signerInfo.getCurrentSignerCerts(); if (certificates != null) { for (X509Certificate certificate : certificates) { AppDetailsItem item = new AppDetailsItem<>(certificate); item.name = "Signer Certificate"; appDetailsItems.add(item); } } else { //noinspection ConstantConditions Null is deliberately set here to get at least one row appDetailsItems.add(new AppDetailsItem<>(null)); } // Get source stamp certificate if (mApkVerifierResult.isSourceStampVerified()) { X509Certificate certificate = signerInfo.getSourceStampCert(); if (certificate != null) { AppDetailsItem item = new AppDetailsItem<>(certificate); item.name = "SourceStamp Certificate"; appDetailsItems.add(item); } } // Get source lineage certificates certificates = signerInfo.getSignerCertsInLineage(); if (certificates != null) { for (X509Certificate certificate : certificates) { AppDetailsItem item = new AppDetailsItem<>(certificate); item.name = "Certificate for Lineage"; appDetailsItems.add(item); } } } catch (Exception e) { e.printStackTrace(); } mSignatures.postValue(appDetailsItems); } @NonNull private final MutableLiveData>> mSharedLibraries = new MutableLiveData<>(); @WorkerThread private void loadSharedLibraries() { List> appDetailsItems = new ArrayList<>(); PackageInfo packageInfo = getPackageInfoInternal(); if (packageInfo == null || mApkFile == null) { mSharedLibraries.postValue(appDetailsItems); return; } // Add shared libraries including the static shared libraries (which are basically APK files) ApplicationInfo info = packageInfo.applicationInfo; if (info.sharedLibraryFiles != null) { for (String sharedLibrary : info.sharedLibraryFiles) { File sharedLib = new File(sharedLibrary); AppDetailsLibraryItem appDetailsItem = null; if (sharedLib.exists() && sharedLib.getName().endsWith(".apk")) { // APK file PackageInfo packageArchiveInfo = mPackageManager.getPackageArchiveInfo(sharedLibrary, 0); if (packageArchiveInfo != null) { appDetailsItem = new AppDetailsLibraryItem<>(packageArchiveInfo); appDetailsItem.name = packageArchiveInfo.applicationInfo.loadLabel(mPackageManager).toString(); appDetailsItem.type = "APK"; } } if (appDetailsItem == null) { appDetailsItem = new AppDetailsLibraryItem<>(sharedLib); appDetailsItem.name = sharedLib.getName(); appDetailsItem.type = sharedLibrary.endsWith(".so") ? "SO" : "JAR"; } appDetailsItem.path = sharedLib; appDetailsItem.size = sharedLib.length(); appDetailsItems.add(appDetailsItem); } } // Add native libraries (shared objects) List entries = mApkFile.getEntries(); for (ApkFile.Entry entry : entries) { if (entry.type == ApkFile.APK_BASE || entry.type == ApkFile.APK_SPLIT_FEATURE || entry.type == ApkFile.APK_SPLIT_ABI || entry.type == ApkFile.APK_SPLIT_UNKNOWN) { // Scan for .so files NativeLibraries nativeLibraries; try (InputStream is = entry.getInputStream(false)) { try { nativeLibraries = new NativeLibraries(is); } catch (IOException e) { // Maybe zip error, Try without InputStream nativeLibraries = new NativeLibraries(entry.getFile(false)); } for (NativeLibraries.NativeLib nativeLib : nativeLibraries.getLibs()) { AppDetailsLibraryItem appDetailsItem = new AppDetailsLibraryItem<>(nativeLib); appDetailsItem.name = nativeLib.getName(); if (nativeLib instanceof NativeLibraries.ElfLib) { switch (((NativeLibraries.ElfLib) nativeLib).getType()) { case NativeLibraries.ElfLib.TYPE_DYN: appDetailsItem.type = "SHARED"; break; case NativeLibraries.ElfLib.TYPE_EXEC: appDetailsItem.type = "EXEC"; break; default: appDetailsItem.type = "SO"; } } else appDetailsItem.type = "⚠️"; appDetailsItems.add(appDetailsItem); } } catch (Throwable th) { Log.e(TAG, th); } } } Collections.sort(appDetailsItems, (o1, o2) -> o1.name.compareToIgnoreCase(o2.name)); mSharedLibraries.postValue(appDetailsItems); } @NonNull private final MutableLiveData> mOverlays = new MutableLiveData<>(); @WorkerThread private void loadOverlays() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || mPackageName == null || mExternalApk || !SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.CHANGE_OVERLAY_PACKAGES)) { mOverlays.postValue(Collections.emptyList()); return; } final List overlays = ExUtils.requireNonNullElse(() -> OverlayManagerCompact .getOverlayManager().getOverlayInfosForTarget(mPackageName, mUserId), Collections.emptyList()); List overlayItems = new ArrayList<>(overlays.size()); for (OverlayInfo overlay : overlays) { overlayItems.add(new AppDetailsOverlayItem(overlay)); } mOverlays.postValue(overlayItems); } /** * Helper class to look for interesting changes to the installed apps * so that the loader can be updated. */ public static class PackageIntentReceiver extends PackageChangeReceiver { final AppDetailsViewModel mModel; volatile boolean mPauseWatcher = false; int mChangeCount = 0; public PackageIntentReceiver(@NonNull AppDetailsViewModel model) { super(model.getApplication()); mModel = model; } public void resumeWatcher() { if (mChangeCount > 0) { mChangeCount = 0; mModel.setPackageChanged(); } mPauseWatcher = false; } public void pauseWatcher() { mChangeCount = 0; mPauseWatcher = true; } @Override @WorkerThread protected void onPackageChanged(Intent intent, @Nullable Integer uid, @Nullable String[] packages) { boolean packageChanged = false; if (uid != null) { if (mModel.mPackageInfo != null && mModel.mPackageInfo.applicationInfo.uid == uid) { Log.d(TAG, "Package is changed."); packageChanged = true; } } else if (packages != null) { for (String packageName : packages) { if (packageName.equals(mModel.mPackageName)) { Log.d(TAG, "Package availability changed."); packageChanged = true; break; } } } if (packageChanged) { if (mPauseWatcher) { ++mChangeCount; } else mModel.setPackageChanged(); } } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/details/IconPickerDialogFragment.java ================================================ // SPDX-License-Identifier: ISC AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.details; import android.app.Application; import android.app.Dialog; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageItemInfo; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.view.View; import android.view.ViewGroup; import android.widget.AbsListView; import android.widget.BaseAdapter; import android.widget.GridView; import android.widget.ImageView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatImageView; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.FragmentActivity; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModelProvider; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.util.List; import java.util.TreeSet; import java.util.concurrent.Future; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.self.imagecache.ImageLoader; import io.github.muntashirakon.AppManager.utils.ResourceUtil; import io.github.muntashirakon.AppManager.utils.ThreadUtils; // Copyright 2017 Adam M. Szalkowski public class IconPickerDialogFragment extends DialogFragment { public static final String TAG = "IconPickerDialogFragment"; private IconPickerListener mListener; private IconListingAdapter mAdapter; private IconPickerViewModel mModel; public void attachIconPickerListener(IconPickerListener listener) { mListener = listener; } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); mModel = new ViewModelProvider(this).get(IconPickerViewModel.class); mModel.getIconsLiveData().observe(this, icons -> { if (mAdapter == null) return; mAdapter.mIcons = icons; mAdapter.notifyDataSetChanged(); }); } @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { mAdapter = new IconListingAdapter(requireActivity()); GridView grid = (GridView) View.inflate(requireActivity(), R.layout.dialog_icon_picker, null); grid.setAdapter(mAdapter); grid.setOnItemClickListener((view, item, index, id) -> { if (mListener != null) { mListener.iconPicked((IconItemInfo) view.getAdapter().getItem(index)); if (getDialog() != null) getDialog().dismiss(); } }); mModel.resolveIcons(); return new MaterialAlertDialogBuilder(requireActivity()) .setTitle(R.string.icon_picker) .setView(grid) .setNegativeButton(R.string.cancel, null).create(); } public interface IconPickerListener { void iconPicked(PackageItemInfo icon); } static class IconListingAdapter extends BaseAdapter { private IconItemInfo[] mIcons; private final FragmentActivity mActivity; public IconListingAdapter(@NonNull FragmentActivity activity) { mActivity = activity; } @Override public int getCount() { return mIcons == null ? 0 : mIcons.length; } @Override public Object getItem(int position) { return mIcons[position]; } @Override public long getItemId(int position) { return 0; } @Override public View getView(int position, View convertView, ViewGroup parent) { ImageView view; if (convertView == null) { view = (ImageView) (convertView = new AppCompatImageView(mActivity)); int size = mActivity.getResources().getDimensionPixelSize(R.dimen.icon_size); convertView.setLayoutParams(new AbsListView.LayoutParams(size, size)); } else { view = (ImageView) convertView; } IconItemInfo info = mIcons[position]; view.setTag(info.packageName); ImageLoader.getInstance().displayImage(info.packageName, info, view); return convertView; } } public static class IconPickerViewModel extends AndroidViewModel { private final PackageManager mPm; private final MutableLiveData mIconsLiveData = new MutableLiveData<>(); @Nullable private Future mIconLoaderResult; public IconPickerViewModel(@NonNull Application application) { super(application); mPm = application.getPackageManager(); } @Override protected void onCleared() { if (mIconLoaderResult != null) { mIconLoaderResult.cancel(true); } super.onCleared(); } public LiveData getIconsLiveData() { return mIconsLiveData; } public void resolveIcons() { if (mIconLoaderResult != null) { mIconLoaderResult.cancel(true); } mIconLoaderResult = ThreadUtils.postOnBackgroundThread(() -> { TreeSet icons = new TreeSet<>(); List installedPackages = mPm.getInstalledPackages(0); for (PackageInfo pack : installedPackages) { try { String iconResourceName = mPm.getResourcesForApplication(pack.packageName) .getResourceName(pack.applicationInfo.icon); if (iconResourceName != null) { icons.add(new IconItemInfo(getApplication(), pack.packageName, iconResourceName)); } } catch (PackageManager.NameNotFoundException | RuntimeException ignored) { } if (ThreadUtils.isInterrupted()) { return; } } mIconsLiveData.postValue(icons.toArray(new IconItemInfo[0])); }); } } private static class IconItemInfo extends PackageItemInfo implements Comparable { private final String mIconResourceString; private final Context mContext; public IconItemInfo(Context context, String packageName, String iconResourceString) { mContext = context; this.packageName = packageName; this.name = mIconResourceString = iconResourceString; } @Override public Drawable loadIcon(@NonNull PackageManager pm) { try { Drawable drawable = ResourceUtil.getResourceFromName(pm, mIconResourceString).getDrawable(mContext.getTheme()); if (drawable != null) { return drawable; } } catch (Exception ignore) { } return pm.getDefaultActivityIcon(); } @Override public int compareTo(@NonNull IconItemInfo o) { return mIconResourceString.compareTo(o.mIconResourceString); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/details/PackageItemShortcutInfo.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.details; import android.annotation.UserIdInt; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.PackageItemInfo; import android.os.Parcel; import android.os.Parcelable; import android.os.UserHandleHidden; import androidx.annotation.NonNull; import androidx.core.os.ParcelCompat; import java.util.Objects; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.shortcut.ShortcutInfo; @SuppressWarnings("rawtypes") public class PackageItemShortcutInfo extends ShortcutInfo { private final T mPackageItemInfo; private final Class mClazz; @UserIdInt private final int mUserId; private final boolean mLaunchViaAssist; public PackageItemShortcutInfo(@NonNull T packageItemInfo, @NonNull Class clazz, @UserIdInt int userId) { this(packageItemInfo, clazz, userId, false); } public PackageItemShortcutInfo(@NonNull T packageItemInfo, @NonNull Class clazz, @UserIdInt int userId, boolean launchViaAssist) { mPackageItemInfo = packageItemInfo; mClazz = clazz; mUserId = userId; if (packageItemInfo instanceof ActivityInfo) { mLaunchViaAssist = launchViaAssist; } else mLaunchViaAssist = false; } @SuppressWarnings("unchecked") public PackageItemShortcutInfo(Parcel in) { super(in); mClazz = (Class) Objects.requireNonNull(ParcelCompat.readSerializable(in, Class.class.getClassLoader(), Class.class)); mPackageItemInfo = ParcelCompat.readParcelable(in, mClazz.getClassLoader(), mClazz); mUserId = in.readInt(); mLaunchViaAssist = ParcelCompat.readBoolean(in); } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeSerializable(mClazz); dest.writeParcelable(mPackageItemInfo, flags); dest.writeInt(mUserId); ParcelCompat.writeBoolean(dest, mLaunchViaAssist); } @Override public Intent toShortcutIntent(@NonNull Context context) { return requireProxy() ? getProxyIntent(context) : getIntent(); } public static final Creator CREATOR = new Creator() { @Override public PackageItemShortcutInfo createFromParcel(Parcel source) { return new PackageItemShortcutInfo(source); } @Override public PackageItemShortcutInfo[] newArray(int size) { return new PackageItemShortcutInfo[size]; } }; @NonNull private Intent getIntent() { Intent intent = new Intent(); intent.setClassName(mPackageItemInfo.packageName, mPackageItemInfo.name); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); return intent; } @NonNull private Intent getProxyIntent(@NonNull Context context) { return ActivityLauncherShortcutActivity.getShortcutIntent(context, mPackageItemInfo.packageName, mPackageItemInfo.name, mUserId, mLaunchViaAssist); } private boolean requireProxy() { return !BuildConfig.APPLICATION_ID.equals(mPackageItemInfo.packageName) || mUserId != UserHandleHidden.myUserId(); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/details/info/ActionItem.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.details.info; import android.content.Context; import android.content.res.ColorStateList; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.StringRes; import com.google.android.material.button.MaterialButton; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.utils.appearance.ColorCodes; class ActionItem { @StringRes private final int mTitleRes; @DrawableRes private final int mIconRes; private View.OnClickListener mOnClickListener; private View.OnLongClickListener mOnLongClickListener; public ActionItem(@StringRes int titleRes, @DrawableRes int iconRes) { mTitleRes = titleRes; mIconRes = iconRes; } public ActionItem setOnClickListener(View.OnClickListener clickListener) { mOnClickListener = clickListener; return this; } public ActionItem setOnLongClickListener(View.OnLongClickListener longClickListener) { mOnLongClickListener = longClickListener; return this; } public MaterialButton toActionButton(@NonNull Context context, @NonNull ViewGroup parent) { MaterialButton button = (MaterialButton) LayoutInflater.from(context).inflate(R.layout.item_app_info_action, parent, false); button.setBackgroundTintList(ColorStateList.valueOf(ColorCodes.getListItemColor1(context))); button.setText(mTitleRes); button.setIconResource(mIconRes); if (mOnClickListener != null) { button.setOnClickListener(mOnClickListener); } if (mOnLongClickListener != null) { button.setOnLongClickListener(mOnLongClickListener); } return button; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/details/info/AppInfoFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.details.info; import static io.github.muntashirakon.AppManager.compat.ApplicationInfoCompat.HIDDEN_API_ENFORCEMENT_BLACK; import static io.github.muntashirakon.AppManager.compat.ApplicationInfoCompat.HIDDEN_API_ENFORCEMENT_DEFAULT; import static io.github.muntashirakon.AppManager.compat.ApplicationInfoCompat.HIDDEN_API_ENFORCEMENT_DISABLED; import static io.github.muntashirakon.AppManager.compat.ApplicationInfoCompat.HIDDEN_API_ENFORCEMENT_ENABLED; import static io.github.muntashirakon.AppManager.compat.ApplicationInfoCompat.HIDDEN_API_ENFORCEMENT_JUST_WARN; import static io.github.muntashirakon.AppManager.compat.ManifestCompat.permission.TERMUX_RUN_COMMAND; import static io.github.muntashirakon.AppManager.utils.UIUtils.displayLongToast; import static io.github.muntashirakon.AppManager.utils.UIUtils.displayShortToast; import static io.github.muntashirakon.AppManager.utils.UIUtils.getBitmapFromDrawable; import static io.github.muntashirakon.AppManager.utils.UIUtils.getColoredText; import static io.github.muntashirakon.AppManager.utils.UIUtils.getDimmedBitmap; import static io.github.muntashirakon.AppManager.utils.UIUtils.getSmallerText; import static io.github.muntashirakon.AppManager.utils.UIUtils.getStyledKeyValue; import static io.github.muntashirakon.AppManager.utils.UIUtils.getTitleText; import static io.github.muntashirakon.AppManager.utils.Utils.openAsFolderInFM; import android.Manifest; import android.app.ActivityManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.graphics.Bitmap; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.UserHandleHidden; import android.provider.Settings; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.format.Formatter; import android.util.Pair; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.webkit.MimeTypeMap; import android.widget.ImageView; import android.widget.TextView; import androidx.activity.result.ActivityResult; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.GuardedBy; import androidx.annotation.MainThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.annotation.WorkerThread; import androidx.collection.ArrayMap; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import androidx.core.content.pm.PackageInfoCompat; import androidx.core.view.MenuProvider; import androidx.fragment.app.Fragment; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.checkbox.MaterialCheckBox; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.progressindicator.LinearProgressIndicator; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.OutputStream; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.Optional; import java.util.concurrent.Future; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.accessibility.AccessibilityMultiplexer; import io.github.muntashirakon.AppManager.accessibility.NoRootAccessibilityService; import io.github.muntashirakon.AppManager.apk.ApkFile; import io.github.muntashirakon.AppManager.apk.ApkSource; import io.github.muntashirakon.AppManager.apk.ApkUtils; import io.github.muntashirakon.AppManager.apk.behavior.FreezeUnfreeze; import io.github.muntashirakon.AppManager.apk.dexopt.DexOptDialog; import io.github.muntashirakon.AppManager.apk.behavior.FreezeUnfreezeShortcutInfo; import io.github.muntashirakon.AppManager.apk.installer.PackageInstallerActivity; import io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat; import io.github.muntashirakon.AppManager.apk.signing.SignerInfo; import io.github.muntashirakon.AppManager.apk.whatsnew.WhatsNewDialogFragment; import io.github.muntashirakon.AppManager.backup.dialog.BackupRestoreDialogFragment; import io.github.muntashirakon.AppManager.batchops.BatchOpsManager; import io.github.muntashirakon.AppManager.batchops.BatchOpsService; import io.github.muntashirakon.AppManager.batchops.BatchQueueItem; import io.github.muntashirakon.AppManager.compat.ActivityManagerCompat; import io.github.muntashirakon.AppManager.compat.ApplicationInfoCompat; import io.github.muntashirakon.AppManager.compat.DeviceIdleManagerCompat; import io.github.muntashirakon.AppManager.compat.DomainVerificationManagerCompat; import io.github.muntashirakon.AppManager.compat.InstallSourceInfoCompat; import io.github.muntashirakon.AppManager.compat.ManifestCompat; import io.github.muntashirakon.AppManager.compat.NetworkPolicyManagerCompat; import io.github.muntashirakon.AppManager.compat.PackageInfoCompat2; import io.github.muntashirakon.AppManager.compat.PackageManagerCompat; import io.github.muntashirakon.AppManager.compat.SensorServiceCompat; import io.github.muntashirakon.AppManager.debloat.BloatwareDetailsDialog; import io.github.muntashirakon.AppManager.details.AppDetailsActivity; import io.github.muntashirakon.AppManager.details.AppDetailsFragment; import io.github.muntashirakon.AppManager.details.AppDetailsViewModel; import io.github.muntashirakon.AppManager.details.manifest.ManifestViewerActivity; import io.github.muntashirakon.AppManager.details.struct.AppDetailsItem; import io.github.muntashirakon.AppManager.fm.FmProvider; import io.github.muntashirakon.AppManager.fm.dialogs.OpenWithDialogFragment; import io.github.muntashirakon.AppManager.logcat.LogViewerActivity; import io.github.muntashirakon.AppManager.logcat.helper.ServiceHelper; import io.github.muntashirakon.AppManager.logcat.struct.SearchCriteria; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.magisk.MagiskDenyList; import io.github.muntashirakon.AppManager.magisk.MagiskHide; import io.github.muntashirakon.AppManager.magisk.MagiskProcess; import io.github.muntashirakon.AppManager.profiles.AddToProfileDialogFragment; import io.github.muntashirakon.AppManager.rules.RulesTypeSelectionDialogFragment; import io.github.muntashirakon.AppManager.rules.compontents.ComponentsBlocker; import io.github.muntashirakon.AppManager.rules.struct.ComponentRule; import io.github.muntashirakon.AppManager.runner.Runner; import io.github.muntashirakon.AppManager.runner.RunnerUtils; import io.github.muntashirakon.AppManager.scanner.ScannerActivity; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.self.imagecache.ImageLoader; import io.github.muntashirakon.AppManager.settings.FeatureController; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.sharedpref.SharedPrefsActivity; import io.github.muntashirakon.AppManager.shortcut.CreateShortcutDialogFragment; import io.github.muntashirakon.AppManager.ssaid.ChangeSsaidDialog; import io.github.muntashirakon.AppManager.types.PackageSizeInfo; import io.github.muntashirakon.AppManager.types.UserPackagePair; import io.github.muntashirakon.AppManager.uri.GrantUriUtils; import io.github.muntashirakon.AppManager.usage.AppUsageStatsManager; import io.github.muntashirakon.AppManager.users.UserInfo; import io.github.muntashirakon.AppManager.users.Users; import io.github.muntashirakon.AppManager.utils.ArrayUtils; import io.github.muntashirakon.AppManager.utils.BetterActivityResult; import io.github.muntashirakon.AppManager.utils.ClipboardUtils; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.DateUtils; import io.github.muntashirakon.AppManager.utils.DigestUtils; import io.github.muntashirakon.AppManager.utils.ExUtils; import io.github.muntashirakon.AppManager.utils.FreezeUtils; import io.github.muntashirakon.AppManager.utils.IntentUtils; import io.github.muntashirakon.AppManager.utils.KeyStoreUtils; import io.github.muntashirakon.AppManager.utils.LangUtils; import io.github.muntashirakon.AppManager.utils.PackageUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.AppManager.utils.Utils; import io.github.muntashirakon.AppManager.utils.appearance.ColorCodes; import io.github.muntashirakon.dialog.DialogTitleBuilder; import io.github.muntashirakon.dialog.ScrollableDialogBuilder; import io.github.muntashirakon.dialog.SearchableFlagsDialogBuilder; import io.github.muntashirakon.dialog.SearchableItemsDialogBuilder; import io.github.muntashirakon.dialog.SearchableMultiChoiceDialogBuilder; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; import io.github.muntashirakon.widget.SwipeRefreshLayout; public class AppInfoFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener, MenuProvider { public static final String TAG = "AppInfoFragment"; private static final String PACKAGE_NAME_AURORA_STORE = "com.aurora.store"; private PackageManager mPackageManager; private String mPackageName; private int mUserId; @Nullable private String mInstallerPackageName; private PackageInfo mPackageInfo; @Nullable private PackageInfo mInstalledPackageInfo; private AppDetailsActivity mActivity; private ApplicationInfo mApplicationInfo; private ViewGroup mHorizontalLayout; private ViewGroup mTagCloud; private SwipeRefreshLayout mSwipeRefresh; private CharSequence mAppLabel; private LinearProgressIndicator mProgressIndicator; private AppDetailsViewModel mMainModel; private AppInfoViewModel mAppInfoModel; private AppInfoRecyclerAdapter mAdapter; // Headers private TextView mLabelView; private TextView mPackageNameView; private TextView mVersionView; private ImageView mIconView; private List mMagiskHiddenProcesses; private List mMagiskDeniedProcesses; private Future mTagCloudFuture; private Future mActionsFuture; private Future mListFuture; private Future mMenuPreparationResult; private boolean mIsExternalApk; private int mLoadedItemCount; @GuardedBy("mListItems") private final List mListItems = new ArrayList<>(); private final BetterActivityResult mExport = BetterActivityResult .registerForActivityResult(this, new ActivityResultContracts.CreateDocument("*/*")); private final BetterActivityResult mRequestPerm = BetterActivityResult .registerForActivityResult(this, new ActivityResultContracts.RequestPermission()); private final BetterActivityResult mActivityLauncher = BetterActivityResult .registerActivityForResult(this); @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); mAppInfoModel = new ViewModelProvider(this).get(AppInfoViewModel.class); mMainModel = new ViewModelProvider(requireActivity()).get(AppDetailsViewModel.class); } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.pager_app_info, container, false); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); mActivity = (AppDetailsActivity) requireActivity(); mAppInfoModel.setMainModel(mMainModel); mPackageManager = mActivity.getPackageManager(); // Swipe refresh mSwipeRefresh = view.findViewById(R.id.swipe_refresh); mSwipeRefresh.setOnRefreshListener(this); // Recycler view RecyclerView recyclerView = view.findViewById(android.R.id.list); recyclerView.setLayoutManager(new LinearLayoutManager(mActivity)); // Horizontal view mHorizontalLayout = view.findViewById(R.id.horizontal_layout); // Progress indicator mProgressIndicator = view.findViewById(R.id.progress_linear); mProgressIndicator.setVisibilityAfterHide(View.GONE); showProgressIndicator(true); // Header mTagCloud = view.findViewById(R.id.tag_cloud); mLabelView = view.findViewById(R.id.label); mPackageNameView = view.findViewById(R.id.packageName); mIconView = view.findViewById(R.id.icon); mVersionView = view.findViewById(R.id.version); mAdapter = new AppInfoRecyclerAdapter(requireContext()); recyclerView.setAdapter(mAdapter); mActivity.addMenuProvider(this, getViewLifecycleOwner(), Lifecycle.State.RESUMED); // Set observer mMainModel.get(AppDetailsFragment.APP_INFO).observe(getViewLifecycleOwner(), appDetailsItems -> { mLoadedItemCount = 0; if (appDetailsItems == null || appDetailsItems.isEmpty() || !mMainModel.isPackageExist()) { showProgressIndicator(false); return; } ++mLoadedItemCount; AppDetailsItem appDetailsItem = appDetailsItems.get(0); mPackageInfo = (PackageInfo) appDetailsItem.item; mApplicationInfo = mPackageInfo.applicationInfo; mPackageName = appDetailsItem.name; mUserId = mMainModel.getUserId(); mInstalledPackageInfo = mMainModel.getInstalledPackageInfo(); mIsExternalApk = mMainModel.isExternalApk(); if (!mIsExternalApk) { mInstallerPackageName = PackageManagerCompat.getInstallerPackageName(mPackageName, mUserId); } // Set icon ImageLoader.getInstance().displayImage(mPackageName, mApplicationInfo, mIconView); // Set package name mPackageNameView.setText(mPackageName); mPackageNameView.setOnClickListener(v -> Utils.copyToClipboard(ContextUtils.getContext(), "Package name", mPackageName)); // Set App Version CharSequence version = getString(R.string.version_name_with_code, mPackageInfo.versionName, PackageInfoCompat.getLongVersionCode(mPackageInfo)); mVersionView.setText(version); // Load app label mAppInfoModel.loadAppLabel(mApplicationInfo); // Load tag cloud mAppInfoModel.loadTagCloud(mPackageInfo, mIsExternalApk); // Load horizontal actions setupHorizontalActions(); // Load other info mAppInfoModel.loadAppInfo(mPackageInfo, mIsExternalApk); }); mAppInfoModel.getAppLabel().observe(getViewLifecycleOwner(), appLabel -> { ++mLoadedItemCount; if (mLoadedItemCount >= 4) { showProgressIndicator(false); } mAppLabel = appLabel; // Set Application Name, aka Label mLabelView.setText(mAppLabel); }); mMainModel.getFreezeTypeLiveData().observe(getViewLifecycleOwner(), freezeType -> { int freezeTypeN = Optional.ofNullable(freezeType) .orElse(Prefs.Blocking.getDefaultFreezingMethod()); showFreezeDialog(freezeTypeN, freezeType != null); }); mIconView.setOnClickListener(v -> { ThreadUtils.postOnBackgroundThread(() -> { String data = ClipboardUtils.readHashValueFromClipboard(ContextUtils.getContext()); if (data != null) { SignerInfo signerInfo = PackageUtils.getSignerInfo(mPackageInfo, mIsExternalApk); if (signerInfo != null) { X509Certificate[] certs = signerInfo.getCurrentSignerCerts(); if (certs != null && certs.length == 1) { try { Pair[] digests = DigestUtils.getDigests(certs[0].getEncoded()); for (Pair digest : digests) { if (digest.second.equals(data)) { if (digest.first.equals(DigestUtils.MD5) || digest.first.equals(DigestUtils.SHA_1)) { ThreadUtils.postOnMainThread(() -> displayLongToast(R.string.verified_using_unreliable_hash)); } else ThreadUtils.postOnMainThread(() -> displayLongToast(R.string.verified)); return; } } } catch (CertificateEncodingException ignore) { } } } ThreadUtils.postOnMainThread(() -> displayLongToast(R.string.not_verified)); } }); }); mAppInfoModel.getTagCloud().observe(getViewLifecycleOwner(), this::setupTagCloud); mAppInfoModel.getAppInfo().observe(getViewLifecycleOwner(), this::setupVerticalView); mAppInfoModel.getInstallExistingResult().observe(getViewLifecycleOwner(), statusMessagePair -> new MaterialAlertDialogBuilder(requireActivity()) .setTitle(mAppLabel) .setIcon(mApplicationInfo.loadIcon(mPackageManager)) .setMessage(statusMessagePair.second) .setNegativeButton(R.string.close, null) .show()); mMainModel.getTagsAlteredLiveData().observe(getViewLifecycleOwner(), altered -> { // Reload tag cloud mAppInfoModel.loadTagCloud(mPackageInfo, mIsExternalApk); }); } @Override public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { if (mMainModel != null && !mMainModel.isExternalApk()) { inflater.inflate(R.menu.fragment_app_info_actions, menu); } } @Override public void onPrepareMenu(@NonNull Menu menu) { if (mIsExternalApk) return; MenuItem magiskHideMenu = menu.findItem(R.id.action_magisk_hide); MenuItem magiskDenyListMenu = menu.findItem(R.id.action_magisk_denylist); MenuItem openInTermuxMenu = menu.findItem(R.id.action_open_in_termux); MenuItem runInTermuxMenu = menu.findItem(R.id.action_run_in_termux); MenuItem batteryOptMenu = menu.findItem(R.id.action_battery_opt); MenuItem sensorsMenu = menu.findItem(R.id.action_sensor); MenuItem netPolicyMenu = menu.findItem(R.id.action_net_policy); MenuItem installMenu = menu.findItem(R.id.action_install); MenuItem optimizeMenu = menu.findItem(R.id.action_optimize); mMenuPreparationResult = ThreadUtils.postOnBackgroundThread(() -> { boolean magiskHideAvailable = MagiskHide.available(); boolean magiskDenyListAvailable = MagiskDenyList.available(); boolean rootAvailable = RunnerUtils.isRootAvailable(); if (ThreadUtils.isInterrupted()) { return; } ThreadUtils.postOnMainThread(() -> { if (magiskHideMenu != null) { magiskHideMenu.setVisible(magiskHideAvailable); } if (magiskDenyListMenu != null) { magiskDenyListMenu.setVisible(magiskDenyListAvailable); } if (openInTermuxMenu != null) { openInTermuxMenu.setVisible(rootAvailable); } }); }); boolean isDebuggable; if (mApplicationInfo != null) { isDebuggable = (mApplicationInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0; } else isDebuggable = false; if (runInTermuxMenu != null) { runInTermuxMenu.setVisible(isDebuggable); } if (batteryOptMenu != null) { batteryOptMenu.setVisible(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M); } if (sensorsMenu != null) { sensorsMenu.setVisible(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.MANAGE_SENSORS)); } if (netPolicyMenu != null) { netPolicyMenu.setVisible(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N); } if (installMenu != null) { installMenu.setVisible(Users.getUsersIds().length > 1 && SelfPermissions.canInstallExistingPackages()); } if (optimizeMenu != null) { optimizeMenu.setVisible(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && (SelfPermissions.isSystemOrRootOrShell() || BuildConfig.APPLICATION_ID.equals(mInstallerPackageName))); } } @Override public boolean onMenuItemSelected(@NonNull MenuItem item) { int itemId = item.getItemId(); if (itemId == R.id.action_refresh_detail) { refreshDetails(); } else if (itemId == R.id.action_share_apk) { showProgressIndicator(true); ThreadUtils.postOnBackgroundThread(() -> { try { Path tmpApkSource = ApkUtils.getSharableApkFile(requireContext(), mPackageInfo); ThreadUtils.postOnMainThread(() -> { showProgressIndicator(false); Context ctx = ContextUtils.getContext(); Intent intent = new Intent(Intent.ACTION_SEND) .setType("application/*") .putExtra(Intent.EXTRA_STREAM, FmProvider.getContentUri(tmpApkSource)) .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); ctx.startActivity(Intent.createChooser(intent, ctx.getString(R.string.share_apk)) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); }); } catch (Exception e) { Log.e(TAG, e); displayLongToast(R.string.failed_to_extract_apk_file); } }); } else if (itemId == R.id.action_backup) { if (mMainModel == null) return true; BackupRestoreDialogFragment fragment = BackupRestoreDialogFragment.getInstanceWithPref( Collections.singletonList(new UserPackagePair(mPackageName, mUserId)), mUserId); fragment.setOnActionBeginListener(mode -> showProgressIndicator(true)); fragment.setOnActionCompleteListener((mode, failedPackages) -> { showProgressIndicator(false); mMainModel.getTagsAlteredLiveData().setValue(true); }); fragment.show(getParentFragmentManager(), BackupRestoreDialogFragment.TAG); } else if (itemId == R.id.action_view_settings) { try { ActivityManagerCompat.startActivity(IntentUtils.getAppDetailsSettings(mPackageName), mUserId); } catch (Throwable th) { UIUtils.displayLongToast("Error: " + th.getLocalizedMessage()); } } else if (itemId == R.id.action_export_blocking_rules) { final String fileName = "app_manager_rules_export-" + DateUtils.formatDateTime(mActivity, System.currentTimeMillis()) + ".am.tsv"; mExport.launch(fileName, uri -> { if (uri == null || mMainModel == null) { // Back button pressed. return; } RulesTypeSelectionDialogFragment dialogFragment = new RulesTypeSelectionDialogFragment(); Bundle exportArgs = new Bundle(); ArrayList packages = new ArrayList<>(); packages.add(mPackageName); exportArgs.putInt(RulesTypeSelectionDialogFragment.ARG_MODE, RulesTypeSelectionDialogFragment.MODE_EXPORT); exportArgs.putParcelable(RulesTypeSelectionDialogFragment.ARG_URI, uri); exportArgs.putStringArrayList(RulesTypeSelectionDialogFragment.ARG_PKG, packages); exportArgs.putIntArray(RulesTypeSelectionDialogFragment.ARG_USERS, new int[]{mUserId}); dialogFragment.setArguments(exportArgs); dialogFragment.show(mActivity.getSupportFragmentManager(), RulesTypeSelectionDialogFragment.TAG); }); } else if (itemId == R.id.action_open_in_termux) { if (SelfPermissions.checkSelfPermission(TERMUX_RUN_COMMAND)) { openInTermux(); } else { mRequestPerm.launch(TERMUX_RUN_COMMAND, granted -> { if (granted) openInTermux(); }); } } else if (itemId == R.id.action_run_in_termux) { if (SelfPermissions.checkSelfPermission(TERMUX_RUN_COMMAND)) { runInTermux(); } else { mRequestPerm.launch(TERMUX_RUN_COMMAND, granted -> { if (granted) runInTermux(); }); } } else if (itemId == R.id.action_magisk_hide) { displayMagiskHideDialog(); } else if (itemId == R.id.action_magisk_denylist) { displayMagiskDenyListDialog(); } else if (itemId == R.id.action_battery_opt) { if (SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.DEVICE_POWER)) { new MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.battery_optimization) .setMessage(R.string.choose_what_to_do) .setPositiveButton(R.string.enable, (dialog, which) -> { if (DeviceIdleManagerCompat.enableBatteryOptimization(mPackageName)) { UIUtils.displayShortToast(R.string.done); mMainModel.getTagsAlteredLiveData().setValue(true); } else { UIUtils.displayShortToast(R.string.failed); } }) .setNegativeButton(R.string.disable, (dialog, which) -> { if (DeviceIdleManagerCompat.disableBatteryOptimization(mPackageName)) { UIUtils.displayShortToast(R.string.done); mMainModel.getTagsAlteredLiveData().setValue(true); } else { UIUtils.displayShortToast(R.string.failed); } }) .show(); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { try { startActivity(IntentUtils.getBatteryOptSettings(mPackageName)); } catch (Throwable th) { UIUtils.displayShortToast("No DEVICE_POWER permission."); } } } else if (itemId == R.id.action_sensor) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.MANAGE_SENSORS)) { new MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.sensors) .setMessage(R.string.choose_what_to_do) .setPositiveButton(R.string.enable, (dialog, which) -> ThreadUtils.postOnBackgroundThread(() -> { try { SensorServiceCompat.enableSensor(mPackageName, mUserId, true); mMainModel.getTagsAlteredLiveData().postValue(true); ThreadUtils.postOnMainThread(() -> UIUtils.displayShortToast(R.string.done)); } catch (IOException e) { ThreadUtils.postOnMainThread(() -> UIUtils.displayLongToast( getString(R.string.failed) + LangUtils.getSeparatorString() + e.getMessage())); } })) .setNegativeButton(R.string.disable, (dialog, which) -> ThreadUtils.postOnBackgroundThread(() -> { try { SensorServiceCompat.enableSensor(mPackageName, mUserId, false); mMainModel.getTagsAlteredLiveData().postValue(true); ThreadUtils.postOnMainThread(() -> UIUtils.displayShortToast(R.string.done)); } catch (IOException e) { ThreadUtils.postOnMainThread(() -> UIUtils.displayLongToast( getString(R.string.failed) + LangUtils.getSeparatorString() + e.getMessage())); } })) .show(); } else { Log.e(TAG, "No sensor permission."); } } else if (itemId == R.id.action_net_policy) { if (!UserHandleHidden.isApp(mApplicationInfo.uid)) { UIUtils.displayLongToast(R.string.netpolicy_cannot_be_modified_for_core_apps); return true; } if (!SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.MANAGE_NETWORK_POLICY)) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { try { startActivity(IntentUtils.getNetPolicySettings(mPackageName)); } catch (Throwable th) { UIUtils.displayShortToast("No MANAGE_NETWORK_POLICY permission."); } } return true; } ArrayMap netPolicyMap = NetworkPolicyManagerCompat.getAllReadablePolicies(ContextUtils.getContext()); Integer[] polices = new Integer[netPolicyMap.size()]; CharSequence[] policyStrings = new String[netPolicyMap.size()]; int selectedPolicies = NetworkPolicyManagerCompat.getUidPolicy(mApplicationInfo.uid); for (int i = 0; i < netPolicyMap.size(); ++i) { polices[i] = netPolicyMap.keyAt(i); policyStrings[i] = netPolicyMap.valueAt(i); } new SearchableFlagsDialogBuilder<>(mActivity, polices, policyStrings, selectedPolicies) .setTitle(R.string.net_policy) .showSelectAll(false) .setNegativeButton(R.string.cancel, null) .setPositiveButton(R.string.save, (dialog, which, selections) -> { int flags = 0; for (int flag : selections) { flags |= flag; } NetworkPolicyManagerCompat.setUidPolicy(mApplicationInfo.uid, flags); mMainModel.getTagsAlteredLiveData().setValue(true); }) .show(); } else if (itemId == R.id.action_extract_icon) { String iconName = mAppLabel + "_icon.png"; mExport.launch(iconName, uri -> { if (uri == null) { // Back button pressed. return; } ThreadUtils.postOnBackgroundThread(() -> { try (OutputStream outputStream = Paths.get(uri).openOutputStream()) { if (outputStream == null) { throw new IOException("Unable to open output stream."); } Bitmap bitmap = getBitmapFromDrawable(mApplicationInfo.loadIcon(mPackageManager)); bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream); outputStream.flush(); ThreadUtils.postOnMainThread(() -> displayShortToast(R.string.saved_successfully)); } catch (IOException e) { Log.e(TAG, e); ThreadUtils.postOnMainThread(() -> displayShortToast(R.string.saving_failed)); } }); }); } else if (itemId == R.id.action_install) { List users = Users.getUsers(); CharSequence[] userNames = new String[users.size()]; int i = 0; for (UserInfo info : users) { userNames[i++] = info.toLocalizedString(requireContext()); } new SearchableItemsDialogBuilder<>(mActivity, userNames) .setTitle(R.string.select_user) .setOnItemClickListener((dialog, which, item1) -> { mAppInfoModel.installExisting(mPackageName, users.get(which).id); dialog.dismiss(); }) .setNegativeButton(R.string.cancel, null) .show(); } else if (itemId == R.id.action_add_to_profile) { AddToProfileDialogFragment dialog = AddToProfileDialogFragment.getInstance(new String[]{mPackageName}); dialog.show(getChildFragmentManager(), AddToProfileDialogFragment.TAG); } else if (itemId == R.id.action_optimize) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && (SelfPermissions.isSystemOrRootOrShell() || BuildConfig.APPLICATION_ID.equals(mInstallerPackageName))) { DexOptDialog dialog = DexOptDialog.getInstance(new String[]{mPackageName}); dialog.show(getChildFragmentManager(), DexOptDialog.TAG); } else UIUtils.displayShortToast(R.string.only_works_in_root_or_adb_mode); } else return false; return true; } @Override public void onMenuClosed(@NonNull Menu menu) { if (mMenuPreparationResult != null) { mMenuPreparationResult.cancel(true); } } @Override public void onStart() { super.onStart(); if (mActivity.searchView != null) mActivity.searchView.setVisibility(View.GONE); } @Override public void onRefresh() { mSwipeRefresh.setRefreshing(false); refreshDetails(); } @Override public void onResume() { super.onResume(); if (mActivity.searchView != null) mActivity.searchView.setVisibility(View.GONE); } @Override public void onDetach() { if (mTagCloudFuture != null) mTagCloudFuture.cancel(true); if (mActionsFuture != null) mActionsFuture.cancel(true); if (mListFuture != null) mListFuture.cancel(true); super.onDetach(); } private void openInTermux() { runWithTermux(new String[]{"su", "-", String.valueOf(mApplicationInfo.uid)}); } private void runInTermux() { runWithTermux(new String[]{"su", "-c", "run-as", mPackageName}); } private void runWithTermux(String[] command) { Intent intent = new Intent(); intent.setClassName("com.termux", "com.termux.app.RunCommandService"); intent.setAction("com.termux.RUN_COMMAND"); intent.putExtra("com.termux.RUN_COMMAND_PATH", Utils.TERMUX_LOGIN_PATH); intent.putExtra("com.termux.RUN_COMMAND_ARGUMENTS", command); intent.putExtra("com.termux.RUN_COMMAND_BACKGROUND", false); try { ActivityCompat.startForegroundService(mActivity, intent); } catch (Exception e) { UIUtils.displayLongToast("Error: " + e.getMessage()); } } private void install() { ApkSource apkSource = mMainModel != null ? mMainModel.getApkSource() : null; if (apkSource == null) return; try { startActivity(PackageInstallerActivity.getLaunchableInstance(requireContext(), apkSource)); } catch (Exception e) { UIUtils.displayLongToast("Error: " + e.getMessage()); } } @UiThread private void refreshDetails() { if (mMainModel == null || isDetached()) return; showProgressIndicator(true); mMainModel.triggerPackageChange(); } @MainThread private void setupTagCloud(@NonNull AppInfoViewModel.TagCloud tagCloud) { if (mTagCloudFuture != null) mTagCloudFuture.cancel(true); mTagCloudFuture = ThreadUtils.postOnBackgroundThread(() -> { List tagItems = getTagCloudItems(tagCloud); ThreadUtils.postOnMainThread(() -> { if (isDetached()) return; ++mLoadedItemCount; if (mLoadedItemCount >= 4) { showProgressIndicator(false); } mTagCloud.removeAllViews(); for (TagItem tagItem : tagItems) { if (isDetached()) return; mTagCloud.addView(tagItem.toChip(mTagCloud.getContext(), mTagCloud)); } }); }); } @WorkerThread @NonNull private List getTagCloudItems(@NonNull AppInfoViewModel.TagCloud tagCloud) { Objects.requireNonNull(mMainModel); Context context = mTagCloud.getContext(); List tagItems = new LinkedList<>(); // Add tracker chip if (!tagCloud.trackerComponents.isEmpty()) { CharSequence[] trackerComponentNames = new CharSequence[tagCloud.trackerComponents.size()]; int blockedColor = ColorCodes.getComponentTrackerBlockedIndicatorColor(context); for (int i = 0; i < trackerComponentNames.length; ++i) { ComponentRule rule = tagCloud.trackerComponents.get(i); trackerComponentNames[i] = rule.isBlocked() ? getColoredText(rule.name, blockedColor) : rule.name; } TagItem trackerTag = new TagItem(); tagItems.add(trackerTag); trackerTag.setText(getResources().getQuantityString(R.plurals.no_of_trackers, tagCloud.trackerComponents.size(), tagCloud.trackerComponents.size())) .setColor(tagCloud.areAllTrackersBlocked ? ColorCodes.getComponentTrackerBlockedIndicatorColor(context) : ColorCodes.getComponentTrackerIndicatorColor(context)) .setOnClickListener(v -> { if (!mIsExternalApk && SelfPermissions.canModifyAppComponentStates(mUserId, mPackageName, mMainModel.isTestOnlyApp())) { new SearchableMultiChoiceDialogBuilder<>(v.getContext(), tagCloud.trackerComponents, trackerComponentNames) .setTitle(R.string.trackers) .addSelections(tagCloud.trackerComponents) .setNegativeButton(R.string.cancel, null) .setPositiveButton(R.string.block, (dialog, which, selectedItems) -> { showProgressIndicator(true); ThreadUtils.postOnBackgroundThread(() -> { mMainModel.addRules(selectedItems, true); ThreadUtils.postOnMainThread(() -> { if (!isDetached()) { showProgressIndicator(false); } displayShortToast(R.string.done); }); }); }) .setNeutralButton(R.string.unblock, (dialog, which, selectedItems) -> { showProgressIndicator(true); ThreadUtils.postOnBackgroundThread(() -> { mMainModel.removeRules(selectedItems, true); ThreadUtils.postOnMainThread(() -> { if (!isDetached()) { showProgressIndicator(false); } displayShortToast(R.string.done); }); }); }) .show(); } else { new SearchableItemsDialogBuilder<>(v.getContext(), trackerComponentNames) .setTitle(R.string.trackers) .setNegativeButton(R.string.close, null) .show(); } }); } if (tagCloud.isSystemApp) { tagItems.add(new TagItem() .setTextRes(tagCloud.isSystemlessPath ? R.string.systemless_app : R.string.system_app)); if (tagCloud.isUpdatedSystemApp) { tagItems.add(new TagItem().setTextRes(R.string.updated_app)); } } else if (!mIsExternalApk) { tagItems.add(new TagItem().setTextRes(R.string.user_app)); } if (tagCloud.splitCount > 0) { TagItem splitTag = new TagItem(); tagItems.add(splitTag); splitTag.setText(getResources().getQuantityString(R.plurals.no_of_splits, tagCloud.splitCount, tagCloud.splitCount)) .setOnClickListener(v -> { ApkFile apkFile = mMainModel.getApkFile(); if (apkFile == null) { return; } // Display a list of apks List apkEntries = apkFile.getEntries(); CharSequence[] entryNames = new CharSequence[tagCloud.splitCount]; for (int i = 0; i < tagCloud.splitCount; ++i) { entryNames[i] = apkEntries.get(i + 1).toLocalizedString(v.getContext()); } new SearchableItemsDialogBuilder<>(v.getContext(), entryNames) .setTitle(R.string.splits) .setNegativeButton(R.string.close, null) .show(); }); } if (tagCloud.isDebuggable) { tagItems.add(new TagItem().setTextRes(R.string.debuggable)); } if (tagCloud.isTestOnly) { tagItems.add(new TagItem().setTextRes(R.string.test_only)); } if (!tagCloud.hasCode) { tagItems.add(new TagItem().setTextRes(R.string.no_code)); } if (tagCloud.isOverlay) { TagItem overlayTag = new TagItem(); tagItems.add(overlayTag); overlayTag.setTextRes(R.string.title_overlay) .setOnClickListener(v -> { Context ctx = v.getContext(); String target = Objects.requireNonNull(PackageInfoCompat2.getOverlayTarget(mPackageInfo)); String targetName = PackageInfoCompat2.getTargetOverlayableName(mPackageInfo); String category = PackageInfoCompat2.getOverlayCategory(mPackageInfo); int priority = PackageInfoCompat2.getOverlayPriority(mPackageInfo); boolean isStatic = PackageInfoCompat2.isStaticOverlayPackage(mPackageInfo); SpannableStringBuilder spannable = new SpannableStringBuilder(); if (targetName != null) { spannable.append(getStyledKeyValue(ctx, R.string.overlay_target, targetName)) .append("\n") .append(getSmallerText(target)); } else { spannable.append(getStyledKeyValue(ctx, R.string.overlay_target, target)); } if (category != null) { spannable.append("\n") .append(getSmallerText(getStyledKeyValue(ctx, R.string.overlay_category, category))); } if (!isStatic) { spannable.append("\n") .append(getSmallerText(getStyledKeyValue(ctx, R.string.priority, String.valueOf(priority)))); } // else static overlays have the highest priority new MaterialAlertDialogBuilder(ctx) .setTitle(R.string.title_overlay) .setMessage(spannable) .setNeutralButton(R.string.app_info, (dialog, which) -> { Intent appDetailsIntent = AppDetailsActivity.getIntent(ctx, target, mUserId); startActivity(appDetailsIntent); }) .setNegativeButton(R.string.close, null) .show(); }); } if (tagCloud.hasRequestedLargeHeap) { tagItems.add(new TagItem().setTextRes(R.string.requested_large_heap)); } if (tagCloud.hostsToOpen != null) { TagItem openLinksTag = new TagItem(); tagItems.add(openLinksTag); openLinksTag.setTextRes(R.string.app_info_tag_open_links) .setColor(tagCloud.canOpenLinks ? ColorCodes.getFailureColor(context) : ColorCodes.getSuccessColor(context)) .setOnClickListener(v -> { SearchableItemsDialogBuilder builder = new SearchableItemsDialogBuilder<>(v.getContext(), new ArrayList<>(tagCloud.hostsToOpen.keySet())) .setTitle(R.string.title_domains_supported_by_the_app) .setNegativeButton(R.string.close, null); if (SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION)) { // Enable/disable directly from the app builder.setPositiveButton(tagCloud.canOpenLinks ? R.string.disable : R.string.enable, (dialog, which) -> ThreadUtils.postOnBackgroundThread(() -> { try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { DomainVerificationManagerCompat.setDomainVerificationLinkHandlingAllowed( mPackageName, !tagCloud.canOpenLinks, mUserId); } mMainModel.getTagsAlteredLiveData().postValue(true); ThreadUtils.postOnMainThread(() -> UIUtils.displayShortToast(R.string.done)); } catch (Throwable th) { th.printStackTrace(); ThreadUtils.postOnMainThread(() -> UIUtils.displayShortToast(R.string.failed)); } })); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { builder.setPositiveButton(R.string.app_settings, (dialog, which) -> { try { startActivity(IntentUtils.getSettings(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS, mPackageName)); } catch (Throwable th) { ExUtils.exceptionAsIgnored(() -> startActivity(IntentUtils.getAppDetailsSettings(mPackageName))); } }); } builder.show(); }); } if (!tagCloud.runningServices.isEmpty()) { TagItem runningTag = new TagItem(); tagItems.add(runningTag); runningTag.setTextRes(R.string.running) .setColor(ColorCodes.getComponentRunningIndicatorColor(context)) .setOnClickListener(v -> displayRunningServices(tagCloud.runningServices, v.getContext())); } else if (tagCloud.isRunning) { TagItem runningTag = new TagItem(); tagItems.add(runningTag); runningTag.setTextRes(R.string.running) .setColor(ColorCodes.getComponentRunningIndicatorColor(context)); } if (tagCloud.isForceStopped) { tagItems.add(new TagItem() .setTextRes(R.string.stopped) .setColor(ColorCodes.getAppForceStoppedIndicatorColor(context))); } if (!tagCloud.isAppEnabled) { tagItems.add(new TagItem() .setTextRes(R.string.disabled_app) .setColor(ColorCodes.getAppDisabledIndicatorColor(context))); } if (tagCloud.isAppSuspended) { tagItems.add(new TagItem() .setTextRes(R.string.suspended) .setColor(ColorCodes.getAppSuspendedIndicatorColor(context))); } if (tagCloud.isAppHidden) { tagItems.add(new TagItem() .setTextRes(R.string.hidden) .setColor(ColorCodes.getAppHiddenIndicatorColor(context))); } mMagiskHiddenProcesses = tagCloud.magiskHiddenProcesses; if (tagCloud.isMagiskHideEnabled) { tagItems.add(new TagItem() .setTextRes(R.string.magisk_hide_enabled) .setOnClickListener(v -> displayMagiskHideDialog())); } mMagiskDeniedProcesses = tagCloud.magiskDeniedProcesses; if (tagCloud.isMagiskDenyListEnabled) { tagItems.add(new TagItem() .setTextRes(R.string.magisk_denylist) .setOnClickListener(v -> displayMagiskDenyListDialog())); } if (tagCloud.canWriteAndExecute) { TagItem wxItem = new TagItem(); tagItems.add(wxItem); wxItem.setText("WX") .setColor(ColorCodes.getAppWriteAndExecuteIndicatorColor(context)) .setOnClickListener(v -> new ScrollableDialogBuilder(v.getContext()) .setTitle("WX") .setMessage(R.string.app_can_write_and_execute_in_same_place) .enableAnchors() .setNegativeButton(R.string.close, null) .show()); } if (tagCloud.bloatwareRemovalType != 0) { TagItem bloatwareTag = new TagItem(); tagItems.add(bloatwareTag); bloatwareTag.setText("Bloatware") .setColor(ColorCodes.getBloatwareIndicatorColor(context, tagCloud.bloatwareRemovalType)) .setOnClickListener(v -> { BloatwareDetailsDialog dialog = BloatwareDetailsDialog.getInstance(mPackageName); dialog.show(getChildFragmentManager(), BloatwareDetailsDialog.TAG); }); } if (tagCloud.hasKeyStoreItems) { TagItem keyStoreTag = new TagItem(); tagItems.add(keyStoreTag); keyStoreTag.setTextRes(R.string.keystore) .setOnClickListener(view -> new SearchableItemsDialogBuilder<>(view.getContext(), KeyStoreUtils .getKeyStoreFiles(mApplicationInfo.uid, mUserId)) .setTitle(R.string.keystore) .setNegativeButton(R.string.close, null) .show()); if (tagCloud.hasMasterKeyInKeyStore) { keyStoreTag.setColor(ColorCodes.getAppKeystoreIndicatorColor(context)); } } if (!tagCloud.backups.isEmpty()) { TagItem backupTag = new TagItem(); tagItems.add(backupTag); backupTag.setTextRes(R.string.backup) .setOnClickListener(v -> { BackupRestoreDialogFragment fragment = BackupRestoreDialogFragment.getInstance( Collections.singletonList(new UserPackagePair(mPackageName, mUserId)), BackupRestoreDialogFragment.MODE_RESTORE | BackupRestoreDialogFragment.MODE_DELETE); fragment.setOnActionBeginListener(mode -> showProgressIndicator(true)); fragment.setOnActionCompleteListener((mode, failedPackages) -> showProgressIndicator(false)); fragment.show(getParentFragmentManager(), BackupRestoreDialogFragment.TAG); }); } if (!tagCloud.isBatteryOptimized) { TagItem batteryOptTag = new TagItem(); tagItems.add(batteryOptTag); batteryOptTag.setTextRes(R.string.no_battery_optimization) .setColor(ColorCodes.getAppNoBatteryOptimizationIndicatorColor(context)); if (SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.DEVICE_POWER)) { batteryOptTag.setOnClickListener(v -> new MaterialAlertDialogBuilder(v.getContext()) .setTitle(R.string.battery_optimization) .setMessage(R.string.enable_battery_optimization) .setNegativeButton(R.string.no, null) .setPositiveButton(R.string.yes, (dialog, which) -> { if (DeviceIdleManagerCompat.enableBatteryOptimization(mPackageName)) { UIUtils.displayShortToast(R.string.done); mMainModel.getTagsAlteredLiveData().setValue(true); } else { UIUtils.displayShortToast(R.string.failed); } }) .show()); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { batteryOptTag.setOnClickListener(v -> ExUtils.exceptionAsIgnored(() -> startActivity(IntentUtils.getBatteryOptSettings(mPackageName)))); } } if (!tagCloud.sensorsEnabled) { TagItem sensorsTag = new TagItem(); tagItems.add(sensorsTag); sensorsTag.setTextRes(R.string.tag_sensors_disabled); } if (tagCloud.netPolicies > 0) { String[] readablePolicies = NetworkPolicyManagerCompat.getReadablePolicies(context, tagCloud.netPolicies) .values().toArray(new String[0]); TagItem netPolicyTag = new TagItem(); tagItems.add(netPolicyTag); netPolicyTag.setTextRes(R.string.has_net_policy) .setOnClickListener(v -> new SearchableItemsDialogBuilder<>(v.getContext(), readablePolicies) .setTitle(R.string.net_policy) .setNegativeButton(R.string.ok, null) .show()); } if (tagCloud.ssaid != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { TagItem ssaidTag = new TagItem(); tagItems.add(ssaidTag); ssaidTag.setTextRes(R.string.ssaid) .setColor(ColorCodes.getAppSsaidIndicatorColor(context)) .setOnClickListener(v -> { ChangeSsaidDialog changeSsaidDialog = ChangeSsaidDialog.getInstance(mPackageName, mApplicationInfo.uid, tagCloud.ssaid); changeSsaidDialog.setSsaidChangedInterface((newSsaid, isSuccessful) -> { displayLongToast(isSuccessful ? R.string.restart_to_reflect_changes : R.string.failed_to_change_ssaid); if (isSuccessful) tagCloud.ssaid = newSsaid; }); changeSsaidDialog.show(getChildFragmentManager(), ChangeSsaidDialog.TAG); }); } if (tagCloud.uriGrants != null) { TagItem safTag = new TagItem(); tagItems.add(safTag); safTag.setTextRes(R.string.saf) .setOnClickListener(v -> { CharSequence[] uriGrants = new CharSequence[tagCloud.uriGrants.size()]; for (int i = 0; i < tagCloud.uriGrants.size(); ++i) { uriGrants[i] = GrantUriUtils.toLocalisedString(v.getContext(), tagCloud.uriGrants.get(i).uri); } new SearchableItemsDialogBuilder<>(v.getContext(), uriGrants) .setTitle(R.string.saf) .setTextSelectable(true) .setListBackgroundColorOdd(ColorCodes.getListItemColor0(mActivity)) .setListBackgroundColorEven(ColorCodes.getListItemColor1(mActivity)) .setNegativeButton(R.string.close, null) .show(); }); } if (tagCloud.usesPlayAppSigning) { TagItem playAppSigningTag = new TagItem(); tagItems.add(playAppSigningTag); playAppSigningTag.setTextRes(R.string.uses_play_app_signing) .setColor(ColorCodes.getAppPlayAppSigningIndicatorColor(context)) .setOnClickListener(v -> new ScrollableDialogBuilder(mActivity) .setTitle(R.string.uses_play_app_signing) .setMessage(R.string.uses_play_app_signing_description) .setNegativeButton(R.string.close, null) .show()); } if (tagCloud.xposedModuleInfo != null) { TagItem xposedItem = new TagItem(); tagItems.add(xposedItem); xposedItem.setText("Xposed") .setOnClickListener(v -> new ScrollableDialogBuilder(v.getContext()) .setTitle(R.string.xposed_module_info) .setMessage(tagCloud.xposedModuleInfo.toLocalizedString(v.getContext())) .setNegativeButton(R.string.close, null) .show()); } if (tagCloud.staticSharedLibraryNames != null) { TagItem staticSharedLibraryTag = new TagItem(); tagItems.add(staticSharedLibraryTag); staticSharedLibraryTag.setTextRes(R.string.static_shared_library) .setOnClickListener(v -> new SearchableMultiChoiceDialogBuilder<>(v.getContext(), tagCloud.staticSharedLibraryNames, tagCloud.staticSharedLibraryNames) .setTitle(R.string.shared_libs) .setPositiveButton(R.string.close, null) .setNeutralButton(R.string.uninstall, (dialog, which, selectedItems) -> { int userId = mUserId; final boolean isSystemApp = ApplicationInfoCompat.isSystemApp(mApplicationInfo); new ScrollableDialogBuilder(mActivity, isSystemApp ? R.string.uninstall_system_app_message : R.string.uninstall_app_message) .setTitle(mAppLabel) .setPositiveButton(R.string.uninstall, (dialog1, which1, keepData) -> { if (selectedItems.size() == 1) { ThreadUtils.postOnBackgroundThread(() -> { PackageInstallerCompat installer = PackageInstallerCompat.getNewInstance(); installer.setAppLabel(mAppLabel); boolean uninstalled = installer.uninstall(selectedItems.get(0), userId, false); ThreadUtils.postOnMainThread(() -> { if (uninstalled) { displayLongToast(R.string.uninstalled_successfully, mAppLabel); mActivity.finish(); } else { displayLongToast(R.string.failed_to_uninstall, mAppLabel); } }); }); } else { ArrayList userIds = new ArrayList<>(selectedItems.size()); for (int i = 0; i < selectedItems.size(); ++i) { userIds.add(userId); } BatchQueueItem item = BatchQueueItem.getBatchOpQueue( BatchOpsManager.OP_UNINSTALL, selectedItems, userIds, null); Intent intent = BatchOpsService.getServiceIntent(mActivity, item); ContextCompat.startForegroundService(mActivity, intent); } }) .setNegativeButton(R.string.cancel, (dialog1, which1, keepData) -> { if (dialog != null) dialog.cancel(); }) .show(); }) .show()); } return tagItems; } private void displayRunningServices( @NonNull List runningServices, @NonNull Context ctx) { showProgressIndicator(true); ThreadUtils.postOnBackgroundThread(() -> { CharSequence[] runningServiceNames = new CharSequence[runningServices.size()]; for (int i = 0; i < runningServiceNames.length; ++i) { ActivityManager.RunningServiceInfo serviceInfo = runningServices.get(i); String title = serviceInfo.service.getShortClassName(); Spannable description = new SpannableStringBuilder() .append(getStyledKeyValue(ctx, R.string.process_name, serviceInfo.process)) .append("\n") .append(getStyledKeyValue(ctx, R.string.pid, String.valueOf(serviceInfo.pid))); runningServiceNames[i] = new SpannableStringBuilder() .append(title) .append("\n") .append(getSmallerText(description)); } boolean logViewerAvailable = FeatureController.isLogViewerEnabled() && SelfPermissions.checkSelfOrRemotePermission(Manifest.permission.DUMP); DialogTitleBuilder titleBuilder = new DialogTitleBuilder(ctx) .setTitle(R.string.running_services); if (logViewerAvailable) { titleBuilder.setSubtitle(R.string.running_services_logcat_hint); } ThreadUtils.postOnMainThread(() -> { if (isDetached()) return; showProgressIndicator(false); SearchableItemsDialogBuilder builder = new SearchableItemsDialogBuilder<>(mActivity, runningServiceNames) .setTitle(titleBuilder.build()); if (logViewerAvailable) { builder.setOnItemClickListener((dialog, which, item) -> { Intent logViewerIntent = new Intent(mActivity.getApplicationContext(), LogViewerActivity.class) .putExtra(LogViewerActivity.EXTRA_FILTER, SearchCriteria.PID_KEYWORD + runningServices.get(which).pid) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); mActivity.startActivity(logViewerIntent); }); } if (SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.FORCE_STOP_PACKAGES)) { builder.setNeutralButton(R.string.force_stop, (dialog, which) -> ThreadUtils.postOnBackgroundThread(() -> { try { PackageManagerCompat.forceStopPackage(mPackageName, mUserId); } catch (SecurityException e) { Log.e(TAG, e); ThreadUtils.postOnMainThread(() -> displayLongToast(R.string.failed_to_stop, mAppLabel)); } })); } builder.setNegativeButton(R.string.close, null); if (isDetached()) return; builder.show(); }); }); } @UiThread private void displayMagiskHideDialog() { SearchableMultiChoiceDialogBuilder builder; builder = getMagiskProcessDialog(mMagiskHiddenProcesses, (dialog, which, mp, isChecked) -> ThreadUtils.postOnBackgroundThread(() -> { mp.setEnabled(isChecked); if (MagiskHide.apply(mp, true)) { try (ComponentsBlocker cb = ComponentsBlocker.getMutableInstance(mPackageName, mUserId)) { cb.setMagiskHide(mp); mMainModel.getTagsAlteredLiveData().postValue(true); } } else { mp.setEnabled(!isChecked); ThreadUtils.postOnMainThread(() -> displayLongToast(isChecked ? R.string.failed_to_enable_magisk_hide : R.string.failed_to_disable_magisk_hide)); } })); if (builder != null) { builder.setTitle(R.string.magisk_hide_enabled).show(); } } @UiThread private void displayMagiskDenyListDialog() { SearchableMultiChoiceDialogBuilder builder; builder = getMagiskProcessDialog(mMagiskDeniedProcesses, (dialog, which, mp, isChecked) -> ThreadUtils.postOnBackgroundThread(() -> { mp.setEnabled(isChecked); if (MagiskDenyList.apply(mp, true)) { try (ComponentsBlocker cb = ComponentsBlocker.getMutableInstance(mPackageName, mUserId)) { cb.setMagiskDenyList(mp); mMainModel.getTagsAlteredLiveData().postValue(true); } } else { mp.setEnabled(!isChecked); ThreadUtils.postOnMainThread(() -> displayLongToast(isChecked ? R.string.failed_to_enable_magisk_deny_list : R.string.failed_to_disable_magisk_deny_list)); } })); if (builder != null) { builder.setTitle(R.string.magisk_denylist).show(); } } @Nullable public SearchableMultiChoiceDialogBuilder getMagiskProcessDialog( @Nullable List magiskProcesses, SearchableMultiChoiceDialogBuilder.OnMultiChoiceClickListener multiChoiceClickListener) { if (magiskProcesses == null || magiskProcesses.isEmpty()) { return null; } List selectedIndexes = new ArrayList<>(); CharSequence[] processes = new CharSequence[magiskProcesses.size()]; int i = 0; for (MagiskProcess mp : magiskProcesses) { SpannableStringBuilder sb = new SpannableStringBuilder(); if (mp.isIsolatedProcess()) { sb.append("\n").append(UIUtils.getSecondaryText(mActivity, getString(R.string.isolated))); if (mp.isRunning()) { sb.append(", ").append(UIUtils.getSecondaryText(mActivity, getString(R.string.running))); } } else if (mp.isRunning()) { sb.append("\n").append(UIUtils.getSecondaryText(mActivity, getString(R.string.running))); } processes[i] = new SpannableStringBuilder(mp.name).append(UIUtils.getSmallerText(sb)); if (mp.isEnabled()) { selectedIndexes.add(i); } i++; } return new SearchableMultiChoiceDialogBuilder<>(mActivity, magiskProcesses, processes) .addSelections(ArrayUtils.convertToIntArray(selectedIndexes)) .setTextSelectable(true) .setOnMultiChoiceClickListener(multiChoiceClickListener) .setNegativeButton(R.string.close, null); } @MainThread private void setupHorizontalActions() { if (mActionsFuture != null) { mActionsFuture.cancel(true); } mActionsFuture = ThreadUtils.postOnBackgroundThread(() -> { List actionItems = getHorizontalActions(); ThreadUtils.postOnMainThread(() -> { if (isDetached()) return; ++mLoadedItemCount; if (mLoadedItemCount >= 4) { showProgressIndicator(false); } mHorizontalLayout.removeAllViews(); for (ActionItem actionItem : actionItems) { if (isDetached()) return; mHorizontalLayout.addView(actionItem.toActionButton(mHorizontalLayout.getContext(), mHorizontalLayout)); } if (isDetached()) return; View v = mHorizontalLayout.getChildAt(0); if (v != null) v.requestFocus(); }); }); } @WorkerThread private List getHorizontalActions() { Objects.requireNonNull(mMainModel); List actionItems = new LinkedList<>(); if (!mIsExternalApk) { boolean isStaticSharedLib = ApplicationInfoCompat.isStaticSharedLibrary(mApplicationInfo); boolean isFrozen = FreezeUtils.isFrozen(mApplicationInfo); boolean canFreeze = !isStaticSharedLib && SelfPermissions.canFreezeUnfreezePackages(); // Set open Intent launchIntent = PackageManagerCompat.getLaunchIntentForPackage(mPackageName, mUserId); if (launchIntent != null && !isFrozen) { ActionItem launchAction = new ActionItem(R.string.launch_app, R.drawable.ic_open_in_new); actionItems.add(launchAction); launchAction.setOnClickListener(v -> { try { ActivityManagerCompat.startActivity(launchIntent, mUserId); } catch (Throwable th) { UIUtils.displayLongToast("Error: " + th.getLocalizedMessage()); } }); } // Set freeze/unfreeze if (canFreeze && !isFrozen) { ActionItem freezeAction = new ActionItem(R.string.freeze, R.drawable.ic_snowflake); actionItems.add(freezeAction); freezeAction.setOnClickListener(v -> { if (BuildConfig.APPLICATION_ID.equals(mPackageName)) { new MaterialAlertDialogBuilder(mActivity) .setMessage(R.string.are_you_sure) .setPositiveButton(R.string.yes, (d, w) -> freeze(true)) .setNegativeButton(R.string.no, null) .show(); } else freeze(true); }) .setOnLongClickListener(v -> { createFreezeShortcut(false); return true; }); } // Set uninstall ActionItem uninstallAction = new ActionItem(R.string.uninstall, R.drawable.ic_trash_can); actionItems.add(uninstallAction); uninstallAction.setOnClickListener(v -> { if (mUserId != UserHandleHidden.myUserId() && !SelfPermissions.checkSelfOrRemotePermission(Manifest.permission.DELETE_PACKAGES)) { // Could be for work profile try { Intent uninstallIntent = new Intent(Intent.ACTION_DELETE); uninstallIntent.setData(Uri.parse("package:" + mPackageName)); ActivityManagerCompat.startActivity(uninstallIntent, mUserId); // TODO: 19/8/24 Watch for uninstallation } catch (Throwable th) { UIUtils.displayLongToast("Error: " + th.getLocalizedMessage()); } return; } final boolean isSystemApp = (mApplicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; ScrollableDialogBuilder builder = new ScrollableDialogBuilder(mActivity, isSystemApp ? R.string.uninstall_system_app_message : R.string.uninstall_app_message) .setTitle(mAppLabel) // FIXME: 16/6/23 Does it even work without INSTALL_PACKAGES? .setCheckboxLabel(R.string.keep_data_and_app_signing_signatures) .setPositiveButton(R.string.uninstall, (dialog, which, keepData) -> ThreadUtils.postOnBackgroundThread(() -> { PackageInstallerCompat installer = PackageInstallerCompat.getNewInstance(); installer.setAppLabel(mAppLabel); boolean uninstalled = installer.uninstall(mPackageName, mUserId, keepData); ThreadUtils.postOnMainThread(() -> { if (uninstalled) { displayLongToast(R.string.uninstalled_successfully, mAppLabel); mActivity.finish(); } else { displayLongToast(R.string.failed_to_uninstall, mAppLabel); } }); })) .setNegativeButton(R.string.cancel, (dialog, which, keepData) -> { if (dialog != null) dialog.cancel(); }); if ((mApplicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0) { builder.setNeutralButton(R.string.uninstall_updates, (dialog, which, keepData) -> ThreadUtils.postOnBackgroundThread(() -> { PackageInstallerCompat installer = PackageInstallerCompat.getNewInstance(); installer.setAppLabel(mAppLabel); boolean isSuccessful = installer.uninstall(mPackageName, UserHandleHidden.USER_ALL, keepData); if (isSuccessful) { ThreadUtils.postOnMainThread(() -> displayLongToast(R.string.update_uninstalled_successfully, mAppLabel)); } else { ThreadUtils.postOnMainThread(() -> displayLongToast(R.string.failed_to_uninstall_updates, mAppLabel)); } })); } builder.show(); }); // Enable/disable app (root/ADB only) if (canFreeze && isFrozen) { // Enable app ActionItem unfreezeAction = new ActionItem(R.string.unfreeze, R.drawable.ic_snowflake_off); actionItems.add(unfreezeAction); unfreezeAction.setOnClickListener(v -> freeze(false)) .setOnLongClickListener(v -> { createFreezeShortcut(true); return true; }); } boolean accessibilityServiceRunning = UserHandleHidden.myUserId() == mUserId && ServiceHelper .checkIfServiceIsRunning(mActivity, NoRootAccessibilityService.class); if (!isStaticSharedLib && (SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.FORCE_STOP_PACKAGES) || accessibilityServiceRunning)) { // Force stop if (!ApplicationInfoCompat.isStopped(mApplicationInfo) && (SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.FORCE_STOP_PACKAGES) || accessibilityServiceRunning)) { ActionItem forceStopAction = new ActionItem(R.string.force_stop, R.drawable.ic_power_settings); actionItems.add(forceStopAction); forceStopAction.setOnClickListener(v -> { if (SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.FORCE_STOP_PACKAGES)) { ThreadUtils.postOnBackgroundThread(() -> { try { PackageManagerCompat.forceStopPackage(mPackageName, mUserId); } catch (SecurityException e) { Log.e(TAG, e); displayLongToast(R.string.failed_to_stop, mAppLabel); } }); } else { // Use accessibility AccessibilityMultiplexer.getInstance().enableForceStop(true); mActivityLauncher.launch(IntentUtils.getAppDetailsSettings(mPackageName), result -> { AccessibilityMultiplexer.getInstance().enableForceStop(false); refreshDetails(); }); } }); } } if (!isStaticSharedLib && (SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.CLEAR_APP_USER_DATA) || accessibilityServiceRunning)) { // Clear data ActionItem clearDataAction = new ActionItem(R.string.clear_data, R.drawable.ic_clear_data); actionItems.add(clearDataAction); clearDataAction.setOnClickListener(v -> new MaterialAlertDialogBuilder(mActivity) .setTitle(mAppLabel) .setMessage(R.string.clear_data_message) .setPositiveButton(R.string.clear, (dialog, which) -> { if (SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.CLEAR_APP_USER_DATA)) { ThreadUtils.postOnBackgroundThread(() -> { boolean success = PackageManagerCompat .clearApplicationUserData(mPackageName, mUserId); ThreadUtils.postOnMainThread(() -> { if (success) { UIUtils.displayShortToast(R.string.done); } else UIUtils.displayShortToast(R.string.failed); }); }); } else { // Use accessibility AccessibilityMultiplexer.getInstance().enableNavigateToStorageAndCache(true); AccessibilityMultiplexer.getInstance().enableClearData(true); mActivityLauncher.launch(IntentUtils.getAppDetailsSettings(mPackageName), result -> { AccessibilityMultiplexer.getInstance().enableNavigateToStorageAndCache(true); AccessibilityMultiplexer.getInstance().enableClearData(false); refreshDetails(); }); } }) .setNegativeButton(R.string.cancel, null) .show()); } if (!isStaticSharedLib && (SelfPermissions.canClearAppCache() || accessibilityServiceRunning)) { // Clear cache ActionItem clearCacheAction = new ActionItem(R.string.clear_cache, R.drawable.ic_clear_cache); actionItems.add(clearCacheAction); clearCacheAction.setOnClickListener(v -> { if (SelfPermissions.canClearAppCache()) { ThreadUtils.postOnBackgroundThread(() -> { boolean success = PackageManagerCompat .deleteApplicationCacheFilesAsUser(mPackageName, mUserId); ThreadUtils.postOnMainThread(() -> { if (success) { UIUtils.displayShortToast(R.string.done); } else UIUtils.displayShortToast(R.string.failed); }); }); } else { // Use accessibility AccessibilityMultiplexer.getInstance().enableNavigateToStorageAndCache(true); AccessibilityMultiplexer.getInstance().enableClearCache(true); mActivityLauncher.launch(IntentUtils.getAppDetailsSettings(mPackageName), result -> { AccessibilityMultiplexer.getInstance().enableNavigateToStorageAndCache(false); AccessibilityMultiplexer.getInstance().enableClearCache(false); refreshDetails(); }); } }); } else { // Display Android settings button ActionItem settingAction = new ActionItem(R.string.view_in_settings, R.drawable.ic_settings); actionItems.add(settingAction); settingAction.setOnClickListener(v -> { try { ActivityManagerCompat.startActivity(IntentUtils.getAppDetailsSettings(mPackageName), mUserId); } catch (Throwable th) { UIUtils.displayLongToast("Error: " + th.getLocalizedMessage()); } }); } } else if (FeatureController.isInstallerEnabled()) { if (mInstalledPackageInfo == null) { // App not installed ActionItem installAction = new ActionItem(R.string.install, R.drawable.ic_get_app); actionItems.add(installAction); installAction.setOnClickListener(v -> install()); } else { // App is installed long installedVersionCode = PackageInfoCompat.getLongVersionCode(mInstalledPackageInfo); long thisVersionCode = PackageInfoCompat.getLongVersionCode(mPackageInfo); if (installedVersionCode < thisVersionCode) { // Needs update ActionItem whatsNewAction = new ActionItem(R.string.whats_new, io.github.muntashirakon.ui.R.drawable.ic_information); actionItems.add(whatsNewAction); whatsNewAction.setOnClickListener(v -> { WhatsNewDialogFragment dialogFragment = WhatsNewDialogFragment .getInstance(mPackageInfo, mInstalledPackageInfo); dialogFragment.show(getChildFragmentManager(), WhatsNewDialogFragment.TAG); }); ActionItem updateAction = new ActionItem(R.string.update, R.drawable.ic_get_app); actionItems.add(updateAction); updateAction.setOnClickListener(v -> install()); } else if (installedVersionCode == thisVersionCode) { // Needs reinstall ActionItem reinstallAction = new ActionItem(R.string.reinstall, R.drawable.ic_get_app); actionItems.add(reinstallAction); reinstallAction.setOnClickListener(v -> install()); } else if (SelfPermissions.checkSelfOrRemotePermission(Manifest.permission.INSTALL_PACKAGES)) { // Needs downgrade ActionItem downgradeAction = new ActionItem(R.string.downgrade, R.drawable.ic_get_app); actionItems.add(downgradeAction); downgradeAction.setOnClickListener(v -> install()); } } } // Set manifest if (FeatureController.isManifestEnabled()) { ActionItem manifestAction = new ActionItem(R.string.manifest, R.drawable.ic_package); actionItems.add(manifestAction); manifestAction.setOnClickListener(v -> { Intent intent = new Intent(mActivity, ManifestViewerActivity.class); startActivityForSplit(intent); }); } // Set scanner if (FeatureController.isScannerEnabled()) { ActionItem scannerAction = new ActionItem(R.string.scanner, R.drawable.ic_security); actionItems.add(scannerAction); scannerAction.setOnClickListener(v -> { Intent intent = new Intent(mActivity, ScannerActivity.class); intent.putExtra(ScannerActivity.EXTRA_IS_EXTERNAL, mIsExternalApk); startActivityForSplit(intent); }); } // Root only features if (!mIsExternalApk) { // Shared prefs (root only) final List sharedPrefs = new ArrayList<>(); Path[] tmpPaths = getSharedPrefs(mApplicationInfo.dataDir); if (tmpPaths != null) sharedPrefs.addAll(Arrays.asList(tmpPaths)); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { tmpPaths = getSharedPrefs(mApplicationInfo.deviceProtectedDataDir); if (tmpPaths != null) sharedPrefs.addAll(Arrays.asList(tmpPaths)); } if (!sharedPrefs.isEmpty()) { CharSequence[] sharedPrefNames = new CharSequence[sharedPrefs.size()]; for (int i = 0; i < sharedPrefs.size(); ++i) { sharedPrefNames[i] = sharedPrefs.get(i).getName(); } ActionItem sharedPrefsAction = new ActionItem(R.string.shared_prefs, R.drawable.ic_view_list); actionItems.add(sharedPrefsAction); sharedPrefsAction.setOnClickListener(v -> new SearchableItemsDialogBuilder<>(mActivity, sharedPrefNames) .setTitle(R.string.shared_prefs) .setOnItemClickListener((dialog, which, item) -> { Intent intent = new Intent(mActivity, SharedPrefsActivity.class); intent.putExtra(SharedPrefsActivity.EXTRA_PREF_LOCATION, sharedPrefs.get(which).getUri()); intent.putExtra(SharedPrefsActivity.EXTRA_PREF_LABEL, mAppLabel); startActivity(intent); }) .setNegativeButton(R.string.ok, null) .show()); } // Databases (root only) final List databases = new ArrayList<>(); tmpPaths = getDatabases(mApplicationInfo.dataDir); if (tmpPaths != null) databases.addAll(Arrays.asList(tmpPaths)); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { tmpPaths = getDatabases(mApplicationInfo.deviceProtectedDataDir); if (tmpPaths != null) databases.addAll(Arrays.asList(tmpPaths)); } if (!databases.isEmpty()) { CharSequence[] databases2 = new CharSequence[databases.size()]; for (int i = 0; i < databases.size(); ++i) { databases2[i] = databases.get(i).getName(); } ActionItem dbAction = new ActionItem(R.string.databases, R.drawable.ic_database); actionItems.add(dbAction); dbAction.setOnClickListener(v -> new SearchableItemsDialogBuilder<>(v.getContext(), databases2) .setTitle(R.string.databases) .setOnItemClickListener((dialog, which, item) -> ThreadUtils.postOnBackgroundThread(() -> { // Vacuum database Runner.runCommand(new String[]{"sqlite3", databases.get(which).getFilePath(), "vacuum"}); ThreadUtils.postOnMainThread(() -> { OpenWithDialogFragment fragment = OpenWithDialogFragment.getInstance(databases.get(which), "application/vnd.sqlite3"); if (isDetached()) return; fragment.show(getChildFragmentManager(), OpenWithDialogFragment.TAG); }); })) .setNegativeButton(R.string.close, null) .show()); } } // End root only features // Set F-Droid Intent fdroidIntent = new Intent(Intent.ACTION_VIEW); fdroidIntent.setData(Uri.parse("https://f-droid.org/packages/" + mPackageName)); List resolvedActivities = mPackageManager.queryIntentActivities(fdroidIntent, 0); if (!resolvedActivities.isEmpty()) { ActionItem fdroidItem = new ActionItem(R.string.fdroid, R.drawable.ic_frost_fdroid); actionItems.add(fdroidItem); fdroidItem.setOnClickListener(v -> { try { startActivity(fdroidIntent); } catch (Exception ignored) { } }); } // Set Aurora Store try { PackageInfo auroraInfo = mPackageManager.getPackageInfo(PACKAGE_NAME_AURORA_STORE, 0); if (PackageInfoCompat.getLongVersionCode(auroraInfo) == 36L || !auroraInfo.applicationInfo.enabled) { // Aurora Store is disabled or the installed version has promotional apps throw new PackageManager.NameNotFoundException(); } ActionItem auroraStoreAction = new ActionItem(R.string.open_in_aurora_store, R.drawable.ic_frost_aurorastore); actionItems.add(auroraStoreAction); auroraStoreAction.setOnClickListener(v -> { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setPackage(PACKAGE_NAME_AURORA_STORE); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setData(Uri.parse("https://play.google.com/store/apps/details?id=" + mPackageName)); try { startActivity(intent); } catch (Exception ignored) { } }); } catch (PackageManager.NameNotFoundException ignored) { } return actionItems; } @UiThread private void startActivityForSplit(Intent intent) { if (mMainModel == null) return; ApkFile apkFile = mMainModel.getApkFile(); if (apkFile != null && apkFile.isSplit()) { // Display a list of apks List apkEntries = apkFile.getEntries(); CharSequence[] entryNames = new CharSequence[apkEntries.size()]; for (int i = 0; i < apkEntries.size(); ++i) { entryNames[i] = apkEntries.get(i).toShortLocalizedString(requireActivity()); } new SearchableItemsDialogBuilder<>(mActivity, entryNames) .setTitle(R.string.select_apk) .setOnItemClickListener((dialog, which, item) -> ThreadUtils.postOnBackgroundThread(() -> { try { File file = apkEntries.get(which).getFile(false); intent.setDataAndType(Uri.fromFile(file), MimeTypeMap.getSingleton() .getMimeTypeFromExtension("apk")); ThreadUtils.postOnMainThread(() -> { if (isDetached()) return; startActivity(intent); }); } catch (IOException e) { UIUtils.displayLongToast("Error: " + e.getMessage()); } })) .setNegativeButton(R.string.cancel, null) .show(); } else { // Open directly File file = new File(mApplicationInfo.publicSourceDir); intent.setDataAndType(Uri.fromFile(file), MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk")); startActivity(intent); } } @GuardedBy("mListItems") private void setPathsAndDirectories(@NonNull AppInfoViewModel.AppInfo appInfo) { synchronized (mListItems) { // Paths and directories mListItems.add(ListItem.newGroupStart(getString(R.string.paths_and_directories))); // Source directory (apk path) if (appInfo.sourceDir != null) { ListItem listItem = ListItem.newSelectableRegularItem(getString(R.string.source_dir), appInfo.sourceDir, openAsFolderInFM(requireContext(), appInfo.sourceDir)); listItem.setActionContentDescription(R.string.open); mListItems.add(listItem); } // Data dir if (appInfo.dataDir != null) { ListItem listItem = ListItem.newSelectableRegularItem(getString(R.string.data_dir), appInfo.dataDir, openAsFolderInFM(requireContext(), appInfo.dataDir)); listItem.setActionContentDescription(R.string.open); mListItems.add(listItem); } // Device-protected data dir if (appInfo.dataDeDir != null) { ListItem listItem = ListItem.newSelectableRegularItem(getString(R.string.dev_protected_data_dir), appInfo.dataDeDir, openAsFolderInFM(requireContext(), appInfo.dataDeDir)); listItem.setActionContentDescription(R.string.open); mListItems.add(listItem); } // External data dirs if (appInfo.extDataDirs.size() == 1) { ListItem listItem = ListItem.newSelectableRegularItem(getString(R.string.external_data_dir), appInfo.extDataDirs.get(0), openAsFolderInFM(requireContext(), appInfo.extDataDirs.get(0))); listItem.setActionContentDescription(R.string.open); mListItems.add(listItem); } else { for (int i = 0; i < appInfo.extDataDirs.size(); ++i) { ListItem listItem = ListItem.newSelectableRegularItem(getString(R.string.external_multiple_data_dir, i), appInfo.extDataDirs.get(i), openAsFolderInFM(requireContext(), appInfo.extDataDirs.get(i))); listItem.setActionContentDescription(R.string.open); mListItems.add(listItem); } } // Native JNI library dir if (appInfo.jniDir != null) { ListItem listItem = ListItem.newSelectableRegularItem(getString(R.string.native_library_dir), appInfo.jniDir, openAsFolderInFM(requireContext(), appInfo.jniDir)); listItem.setActionContentDescription(R.string.open); mListItems.add(listItem); } } } @GuardedBy("mListItems") private void setMoreInfo(AppInfoViewModel.AppInfo appInfo) { synchronized (mListItems) { // Set more info mListItems.add(ListItem.newGroupStart(getString(R.string.more_info))); // Set installed version info if (mIsExternalApk && mInstalledPackageInfo != null) { ListItem listItem = ListItem.newSelectableRegularItem(getString(R.string.installed_version), getString(R.string.version_name_with_code, mInstalledPackageInfo.versionName, PackageInfoCompat.getLongVersionCode(mInstalledPackageInfo)), v -> { Intent intent = AppDetailsActivity.getIntent(mActivity, mPackageName, UserHandleHidden.myUserId()); mActivity.startActivity(intent); }); listItem.setActionIcon(io.github.muntashirakon.ui.R.drawable.ic_information); listItem.setActionContentDescription(R.string.app_info); mListItems.add(listItem); } // SDK final StringBuilder sdk = new StringBuilder(); sdk.append(getString(R.string.sdk_max)).append(LangUtils.getSeparatorString()).append(String.format(Locale.getDefault(), "%d", mApplicationInfo.targetSdkVersion)); if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) { sdk.append(", ").append(getString(R.string.sdk_min)).append(LangUtils.getSeparatorString()) .append(String.format(Locale.getDefault(), "%d", mApplicationInfo.minSdkVersion)); } mListItems.add(ListItem.newSelectableRegularItem(getString(R.string.sdk), sdk.toString())); // Set Flags final StringBuilder flags = new StringBuilder(); if ((mPackageInfo.applicationInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) flags.append("FLAG_DEBUGGABLE"); if ((mPackageInfo.applicationInfo.flags & ApplicationInfo.FLAG_TEST_ONLY) != 0) flags.append(flags.length() == 0 ? "" : "|").append("FLAG_TEST_ONLY"); if ((mPackageInfo.applicationInfo.flags & ApplicationInfo.FLAG_MULTIARCH) != 0) flags.append(flags.length() == 0 ? "" : "|").append("FLAG_MULTIARCH"); if ((mPackageInfo.applicationInfo.flags & ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) flags.append(flags.length() == 0 ? "" : "|").append("FLAG_HARDWARE_ACCELERATED"); if (flags.length() != 0) { ListItem flagsItem = ListItem.newSelectableRegularItem(getString(R.string.sdk_flags), flags.toString()); flagsItem.setMonospace(true); mListItems.add(flagsItem); } if (mIsExternalApk) return; mListItems.add(ListItem.newRegularItem(getString(R.string.date_installed), getTime(mPackageInfo.firstInstallTime))); mListItems.add(ListItem.newRegularItem(getString(R.string.date_updated), getTime(mPackageInfo.lastUpdateTime))); if (!mPackageName.equals(mApplicationInfo.processName)) { mListItems.add(ListItem.newSelectableRegularItem(getString(R.string.process_name), mApplicationInfo.processName)); } if (appInfo.installerApp != null) { ListItem installerItem = ListItem.newSelectableRegularItem( getString(R.string.installer_app), appInfo.installerApp, v -> displayInstallerDialog(Objects.requireNonNull(appInfo.installSource))); installerItem.setActionIcon(R.drawable.ic_information_circle); installerItem.setActionContentDescription(R.string.more_info); mListItems.add(installerItem); } mListItems.add(ListItem.newSelectableRegularItem(getString(R.string.user_id), String.format(Locale.getDefault(), "%d", mApplicationInfo.uid))); if (mPackageInfo.sharedUserId != null) mListItems.add(ListItem.newSelectableRegularItem(getString(R.string.shared_user_id), mPackageInfo.sharedUserId)); if (appInfo.primaryCpuAbi != null) { mListItems.add(ListItem.newSelectableRegularItem(getString(R.string.primary_abi), appInfo.primaryCpuAbi)); } if (appInfo.zygotePreloadName != null) { mListItems.add(ListItem.newSelectableRegularItem(getString(R.string.zygote_preload_name), appInfo.zygotePreloadName)); } if (!mIsExternalApk) { mListItems.add(ListItem.newRegularItem(getString(R.string.hidden_api_enforcement_policy), getHiddenApiEnforcementPolicy(appInfo.hiddenApiEnforcementPolicy))); } if (appInfo.seInfo != null) { mListItems.add(ListItem.newSelectableRegularItem(getString(R.string.selinux), appInfo.seInfo)); } // Main activity if (appInfo.mainActivity != null) { final ComponentName launchComponentName = appInfo.mainActivity.getComponent(); if (launchComponentName != null) { final String mainActivity = launchComponentName.getClassName(); ListItem listItem = ListItem.newSelectableRegularItem(getString(R.string.main_activity), mainActivity, view -> startActivity(appInfo.mainActivity)); listItem.setActionContentDescription(R.string.open); mListItems.add(listItem); } } } } @NonNull private String getHiddenApiEnforcementPolicy(int policy) { switch (policy) { case HIDDEN_API_ENFORCEMENT_DEFAULT: return getString(R.string.hidden_api_enf_default_policy); default: case HIDDEN_API_ENFORCEMENT_DISABLED: return getString(R.string.hidden_api_enf_policy_none); case HIDDEN_API_ENFORCEMENT_JUST_WARN: return getString(R.string.hidden_api_enf_policy_warn); case HIDDEN_API_ENFORCEMENT_ENABLED: return getString(R.string.hidden_api_enf_policy_dark_grey_and_black); case HIDDEN_API_ENFORCEMENT_BLACK: return getString(R.string.hidden_api_enf_policy_black); } } private void setDataUsage(@NonNull AppInfoViewModel.AppInfo appInfo) { AppUsageStatsManager.DataUsage dataUsage = appInfo.dataUsage; if (dataUsage == null) { // No permission return; } // Hide data usage if: // 1. OS is Android 6.0 onwards, AND // 2. The user is not the current user, AND // 3. Remote UID is not system UID if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && mUserId != UserHandleHidden.myUserId() && !SelfPermissions.isSystem()) { return; } synchronized (mListItems) { if (isDetached()) return; mListItems.add(ListItem.newGroupStart(getString(R.string.data_usage_msg))); mListItems.add(ListItem.newInlineItem(getString(R.string.data_transmitted), getReadableSize(dataUsage.getTx()))); mListItems.add(ListItem.newInlineItem(getString(R.string.data_received), getReadableSize(dataUsage.getRx()))); } } @MainThread @GuardedBy("mListItems") private void setupVerticalView(AppInfoViewModel.AppInfo appInfo) { if (mListFuture != null) mListFuture.cancel(true); mListFuture = ThreadUtils.postOnBackgroundThread(() -> { synchronized (mListItems) { mListItems.clear(); if (!mIsExternalApk) { setPathsAndDirectories(appInfo); setDataUsage(appInfo); // Storage and Cache if (FeatureController.isUsageAccessEnabled()) { setStorageAndCache(appInfo); } } setMoreInfo(appInfo); ThreadUtils.postOnMainThread(() -> { if (isDetached()) return; ++mLoadedItemCount; if (mLoadedItemCount >= 4) { showProgressIndicator(false); } if (isDetached()) return; mAdapter.setAdapterList(mListItems); }); } }); } @Nullable private Path[] getSharedPrefs(@Nullable String sourceDir) { if (sourceDir == null) return null; try { Path sharedPath = Paths.get(sourceDir).findFile("shared_prefs"); return sharedPath.listFiles(); } catch (FileNotFoundException e) { return null; } } @Nullable private Path[] getDatabases(@Nullable String sourceDir) { if (sourceDir == null) return null; try { Path sharedPath = Paths.get(sourceDir).findFile("databases"); return sharedPath.listFiles((dir, name) -> !(name.endsWith("-journal") || name.endsWith("-wal") || name.endsWith("-shm"))); } catch (FileNotFoundException e) { return null; } } @GuardedBy("mListItems") private void setStorageAndCache(AppInfoViewModel.AppInfo appInfo) { if (FeatureController.isUsageAccessEnabled()) { // Grant optional READ_PHONE_STATE permission if (AppUsageStatsManager.requireReadPhoneStatePermission()) { ThreadUtils.postOnMainThread(() -> mRequestPerm.launch(Manifest.permission.READ_PHONE_STATE, granted -> { if (granted) { mAppInfoModel.loadAppInfo(mPackageInfo, mIsExternalApk); } })); } } if (!SelfPermissions.checkUsageStatsPermission()) { ThreadUtils.postOnMainThread(() -> new MaterialAlertDialogBuilder(mActivity) .setTitle(R.string.grant_usage_access) .setMessage(R.string.grant_usage_acess_message) .setPositiveButton(R.string.go, (dialog, which) -> { try { mActivityLauncher.launch(new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS), result -> { if (SelfPermissions.checkUsageStatsPermission()) { FeatureController.getInstance().modifyState(FeatureController .FEAT_USAGE_ACCESS, true); // Reload app info mAppInfoModel.loadAppInfo(mPackageInfo, mIsExternalApk); } }); } catch (SecurityException ignore) { } }) .setNegativeButton(R.string.cancel, null) .setNeutralButton(R.string.never_ask, (dialog, which) -> FeatureController.getInstance().modifyState( FeatureController.FEAT_USAGE_ACCESS, false)) .setCancelable(false) .show()); return; } PackageSizeInfo sizeInfo = appInfo.sizeInfo; if (sizeInfo == null) return; synchronized (mListItems) { mListItems.add(ListItem.newGroupStart(getString(R.string.storage_and_cache))); mListItems.add(ListItem.newInlineItem(getString(R.string.app_size), getReadableSize(sizeInfo.codeSize))); mListItems.add(ListItem.newInlineItem(getString(R.string.data_size), getReadableSize(sizeInfo.dataSize))); mListItems.add(ListItem.newInlineItem(getString(R.string.cache_size), getReadableSize(sizeInfo.cacheSize))); if (sizeInfo.obbSize != 0) { mListItems.add(ListItem.newInlineItem(getString(R.string.obb_size), getReadableSize(sizeInfo.obbSize))); } if (sizeInfo.mediaSize != 0) { mListItems.add(ListItem.newInlineItem(getString(R.string.media_size), getReadableSize(sizeInfo.mediaSize))); } mListItems.add(ListItem.newInlineItem(getString(R.string.total_size), getReadableSize(sizeInfo.getTotalSize()))); } } @MainThread private void freeze(boolean freeze) { if (mMainModel == null) return; if (freeze) { mMainModel.loadFreezeType(); } else { // Unfreeze ThreadUtils.postOnBackgroundThread(this::doUnfreeze); } } private void showFreezeDialog(int freezeType, boolean isCustom) { View view = View.inflate(mActivity, R.layout.item_checkbox, null); MaterialCheckBox checkBox = view.findViewById(R.id.checkbox); checkBox.setText(R.string.remember_option_for_this_app); checkBox.setChecked(isCustom); FreezeUnfreeze.getFreezeDialog(mActivity, freezeType) .setIcon(R.drawable.ic_snowflake) .setTitle(R.string.freeze) .setView(view) .setPositiveButton(R.string.freeze, (dialog, which, selectedItem) -> { if (selectedItem == null) { return; } ThreadUtils.postOnBackgroundThread(() -> doFreeze(selectedItem, checkBox.isChecked())); }) .setNegativeButton(R.string.cancel, null) .show(); } @WorkerThread private void doFreeze(@FreezeUtils.FreezeMethod int freezeType, boolean remember) { try { if (remember) { FreezeUtils.storeFreezeMethod(mPackageName, freezeType); } else { FreezeUtils.deleteFreezeMethod(mPackageName); } FreezeUtils.freeze(mPackageName, mUserId, freezeType); } catch (Throwable th) { Log.e(TAG, th); ThreadUtils.postOnMainThread(() -> displayLongToast(R.string.failed_to_freeze, mAppLabel)); } } @WorkerThread private void doUnfreeze() { try { FreezeUtils.unfreeze(mPackageName, mUserId); } catch (Throwable th) { Log.e(TAG, th); ThreadUtils.postOnMainThread(() -> displayLongToast(R.string.failed_to_unfreeze, mAppLabel)); } } private void createFreezeShortcut(boolean isFrozen) { if (mMainModel == null) return; List allFlags = new ArrayList<>(3); for (int i = 0; i < 3; ++i) { allFlags.add(1 << i); } new SearchableMultiChoiceDialogBuilder<>(mActivity, allFlags, R.array.freeze_unfreeze_flags) .setTitle(R.string.freeze_unfreeze) .setPositiveButton(R.string.create_shortcut, (dialog, which, selections) -> { int flags = 0; for (int flag : selections) { flags |= flag; } Bitmap icon = getBitmapFromDrawable(mIconView.getDrawable()); FreezeUnfreezeShortcutInfo shortcutInfo = new FreezeUnfreezeShortcutInfo(mPackageName, mUserId, flags); shortcutInfo.setName(mAppLabel); shortcutInfo.setIcon(isFrozen ? getDimmedBitmap(icon) : icon); CreateShortcutDialogFragment dialog1 = CreateShortcutDialogFragment.getInstance(shortcutInfo); dialog1.show(getChildFragmentManager(), CreateShortcutDialogFragment.TAG); }) .show(); } private void displayInstallerDialog(@NonNull InstallSourceInfoCompat installSource) { List installerInfoList = new ArrayList<>(3); List packageNames = new ArrayList<>(3); if (installSource.getInstallingPackageLabel() != null) { CharSequence info = new SpannableStringBuilder(getSmallerText(getString(R.string.installer))) .append("\n") .append(getTitleText(requireContext(), installSource.getInstallingPackageLabel())) .append("\n") .append(installSource.getInstallingPackageName()); installerInfoList.add(info); packageNames.add(installSource.getInstallingPackageName()); } if (installSource.getInitiatingPackageLabel() != null) { CharSequence info = new SpannableStringBuilder(getSmallerText(getString(R.string.actual_installer))) .append("\n") .append(getTitleText(requireContext(), installSource.getInitiatingPackageLabel())) .append("\n") .append(installSource.getInitiatingPackageName()); installerInfoList.add(info); packageNames.add(installSource.getInitiatingPackageName()); } if (installSource.getOriginatingPackageLabel() != null) { CharSequence info = new SpannableStringBuilder(getSmallerText(getString(R.string.apk_source))) .append("\n") .append(getTitleText(requireContext(), installSource.getOriginatingPackageLabel())) .append("\n") .append(installSource.getOriginatingPackageName()); installerInfoList.add(info); packageNames.add(installSource.getOriginatingPackageName()); } new SearchableItemsDialogBuilder<>(requireContext(), installerInfoList) .setTitle(R.string.installer) .setOnItemClickListener((dialog, which, item) -> { String packageName = packageNames.get(which); Intent intent = AppDetailsActivity.getIntent(requireContext(), packageName, mUserId); startActivity(intent); }) .setNegativeButton(R.string.close, null) .show(); } /** * Get Unix time to formatted time. * * @param time Unix time * @return Formatted time */ @NonNull private String getTime(long time) { return DateUtils.formatLongDateTime(requireContext(), time); } /** * Format sizes (bytes to B, KB, MB etc.). * * @param size Size in Bytes * @return Formatted size */ private String getReadableSize(long size) { return Formatter.formatFileSize(mActivity, size); } private void showProgressIndicator(boolean show) { if (mProgressIndicator == null) return; if (show) mProgressIndicator.show(); else mProgressIndicator.hide(); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/details/info/AppInfoRecyclerAdapter.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.details.info; import android.content.Context; import android.graphics.Typeface; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.appcompat.widget.LinearLayoutCompat; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.button.MaterialButton; import com.google.android.material.divider.MaterialDivider; import java.util.ArrayList; import java.util.List; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.util.AdapterUtils; import static io.github.muntashirakon.AppManager.details.info.ListItem.LIST_ITEM_GROUP_BEGIN; import static io.github.muntashirakon.AppManager.details.info.ListItem.LIST_ITEM_INLINE; import static io.github.muntashirakon.AppManager.details.info.ListItem.LIST_ITEM_REGULAR; import static io.github.muntashirakon.AppManager.details.info.ListItem.LIST_ITEM_REGULAR_ACTION; class AppInfoRecyclerAdapter extends RecyclerView.Adapter { private final Context mContext; private final List mAdapterList; AppInfoRecyclerAdapter(Context context) { mContext = context; mAdapterList = new ArrayList<>(); } void setAdapterList(@NonNull List list) { AdapterUtils.notifyDataSetChanged(this, mAdapterList, list); } @Override @ListItem.ListItemType public int getItemViewType(int position) { return mAdapterList.get(position).type; } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, @ListItem.ListItemType int viewType) { final View view; switch (viewType) { case LIST_ITEM_GROUP_BEGIN: view = LayoutInflater.from(parent.getContext()).inflate(io.github.muntashirakon.ui.R.layout.m3_preference_category, parent, false); break; default: case LIST_ITEM_REGULAR: view = LayoutInflater.from(parent.getContext()).inflate(io.github.muntashirakon.ui.R.layout.m3_preference, parent, false); break; case LIST_ITEM_REGULAR_ACTION: { view = LayoutInflater.from(parent.getContext()).inflate(io.github.muntashirakon.ui.R.layout.m3_preference, parent, false); View action = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_right_standalone_action, parent, false); LinearLayoutCompat layoutCompat = view.findViewById(android.R.id.widget_frame); layoutCompat.addView(action); break; } case LIST_ITEM_INLINE: { view = LayoutInflater.from(parent.getContext()).inflate(io.github.muntashirakon.ui.R.layout.m3_preference, parent, false); View action = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_right_summary, parent, false); LinearLayoutCompat layoutCompat = view.findViewById(android.R.id.widget_frame); layoutCompat.addView(action); break; } } return new ViewHolder(view, viewType); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { ListItem listItem = mAdapterList.get(position); // Set title holder.title.setText(listItem.getTitle()); if (listItem.type == LIST_ITEM_GROUP_BEGIN) { return; } // Set common properties holder.subtitle.setText(listItem.getSubtitle()); holder.subtitle.setTextIsSelectable(listItem.isSelectable()); holder.subtitle.setTypeface(listItem.isMonospace() ? Typeface.MONOSPACE : Typeface.DEFAULT); if (listItem.type == LIST_ITEM_INLINE) { return; } if (listItem.type == LIST_ITEM_REGULAR_ACTION) { holder.actionDivider.setVisibility(listItem.getOnActionClickListener() != null ? View.VISIBLE : View.GONE); if (listItem.getActionIconRes() != 0) { holder.actionIcon.setIconResource(listItem.getActionIconRes()); } if (listItem.getActionContentDescription() != null) { holder.actionIcon.setContentDescription(listItem.getActionContentDescription()); } else if (listItem.getActionContentDescriptionRes() != 0) { holder.actionIcon.setContentDescription(mContext.getString(listItem.getActionContentDescriptionRes())); } if (listItem.getOnActionClickListener() != null) { holder.actionIcon.setVisibility(View.VISIBLE); holder.actionIcon.setOnClickListener(listItem.getOnActionClickListener()); } else holder.actionIcon.setVisibility(View.GONE); } } @Override public int getItemCount() { return mAdapterList.size(); } static class ViewHolder extends RecyclerView.ViewHolder { TextView title; TextView subtitle; MaterialButton actionIcon; MaterialDivider actionDivider; public ViewHolder(@NonNull View itemView, @ListItem.ListItemType int viewType) { super(itemView); itemView.findViewById(R.id.icon_frame).setVisibility(View.GONE); switch (viewType) { case LIST_ITEM_GROUP_BEGIN: { title = itemView.findViewById(android.R.id.title); itemView.findViewById(android.R.id.summary).setVisibility(View.GONE); break; } case LIST_ITEM_REGULAR: case LIST_ITEM_REGULAR_ACTION: title = itemView.findViewById(android.R.id.title); subtitle = itemView.findViewById(android.R.id.summary); actionDivider = itemView.findViewById(R.id.divider); actionIcon = itemView.findViewById(android.R.id.button1); break; default: break; case LIST_ITEM_INLINE: title = itemView.findViewById(android.R.id.title); subtitle = itemView.findViewById(android.R.id.text1); itemView.findViewById(android.R.id.summary).setVisibility(View.GONE); break; } } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/details/info/AppInfoViewModel.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.details.info; import android.Manifest; import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.Application; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.verify.domain.DomainVerificationUserState; import android.os.Build; import android.os.UserHandleHidden; import android.text.TextUtils; import androidx.annotation.AnyThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import androidx.core.util.Pair; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.zip.ZipFile; import io.github.muntashirakon.AppManager.StaticDataset; import io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat; import io.github.muntashirakon.AppManager.apk.installer.PackageInstallerService; import io.github.muntashirakon.AppManager.backup.BackupUtils; import io.github.muntashirakon.AppManager.compat.ActivityManagerCompat; import io.github.muntashirakon.AppManager.compat.ApplicationInfoCompat; import io.github.muntashirakon.AppManager.compat.DeviceIdleManagerCompat; import io.github.muntashirakon.AppManager.compat.DomainVerificationManagerCompat; import io.github.muntashirakon.AppManager.compat.InstallSourceInfoCompat; import io.github.muntashirakon.AppManager.compat.ManifestCompat; import io.github.muntashirakon.AppManager.compat.NetworkPolicyManagerCompat; import io.github.muntashirakon.AppManager.compat.PackageInfoCompat2; import io.github.muntashirakon.AppManager.compat.PackageManagerCompat; import io.github.muntashirakon.AppManager.compat.SensorServiceCompat; import io.github.muntashirakon.AppManager.db.entity.Backup; import io.github.muntashirakon.AppManager.debloat.DebloatObject; import io.github.muntashirakon.AppManager.details.AppDetailsViewModel; import io.github.muntashirakon.AppManager.magisk.MagiskDenyList; import io.github.muntashirakon.AppManager.magisk.MagiskHide; import io.github.muntashirakon.AppManager.magisk.MagiskProcess; import io.github.muntashirakon.AppManager.magisk.MagiskUtils; import io.github.muntashirakon.AppManager.misc.OsEnvironment; import io.github.muntashirakon.AppManager.misc.XposedModuleInfo; import io.github.muntashirakon.AppManager.rules.RuleType; import io.github.muntashirakon.AppManager.rules.compontents.ComponentUtils; import io.github.muntashirakon.AppManager.rules.struct.ComponentRule; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.settings.FeatureController; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.ssaid.SsaidSettings; import io.github.muntashirakon.AppManager.types.PackageSizeInfo; import io.github.muntashirakon.AppManager.uri.UriManager; import io.github.muntashirakon.AppManager.usage.AppUsageStatsManager; import io.github.muntashirakon.AppManager.usage.TimeInterval; import io.github.muntashirakon.AppManager.usage.UsageUtils; import io.github.muntashirakon.AppManager.utils.ArrayUtils; import io.github.muntashirakon.AppManager.utils.ExUtils; import io.github.muntashirakon.AppManager.utils.KeyStoreUtils; import io.github.muntashirakon.AppManager.utils.PackageUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; public class AppInfoViewModel extends AndroidViewModel { private final MutableLiveData mAppLabel = new MutableLiveData<>(); private final MutableLiveData mTagCloud = new MutableLiveData<>(); private final MutableLiveData mAppInfo = new MutableLiveData<>(); private final MutableLiveData> mInstallExistingResult = new MutableLiveData<>(); private final ExecutorService mExecutor = Executors.newFixedThreadPool(4); private Future mTagCloudFuture; private Future mAppInfoFuture; @Nullable private AppDetailsViewModel mMainModel; public AppInfoViewModel(@NonNull Application application) { super(application); } @Override protected void onCleared() { if (mTagCloudFuture != null) { mTagCloudFuture.cancel(true); } if (mAppInfoFuture != null) { mAppInfoFuture.cancel(true); } super.onCleared(); } public void setMainModel(@NonNull AppDetailsViewModel mainModel) { mMainModel = mainModel; } public LiveData getAppLabel() { return mAppLabel; } public LiveData getTagCloud() { return mTagCloud; } public LiveData getAppInfo() { return mAppInfo; } public LiveData> getInstallExistingResult() { return mInstallExistingResult; } @AnyThread public void loadAppLabel(@NonNull ApplicationInfo applicationInfo) { ThreadUtils.postOnBackgroundThread(() -> { CharSequence appLabel = applicationInfo.loadLabel(getApplication().getPackageManager()); mAppLabel.postValue(appLabel); }); } @AnyThread public void loadTagCloud(@NonNull PackageInfo packageInfo, boolean isExternalApk) { if (mTagCloudFuture != null) { mTagCloudFuture.cancel(true); } mTagCloudFuture = ThreadUtils.postOnBackgroundThread(() -> loadTagCloudInternal(packageInfo, isExternalApk)); } @WorkerThread private void loadTagCloudInternal(@NonNull PackageInfo packageInfo, boolean isExternalApk) { if (mMainModel == null) return; String packageName = packageInfo.packageName; int userId = mMainModel.getUserId(); ApplicationInfo applicationInfo = packageInfo.applicationInfo; TagCloud tagCloud = new TagCloud(); try { Map trackerComponents = ComponentUtils.getTrackerComponentsForPackage(packageInfo); tagCloud.trackerComponents = new ArrayList<>(trackerComponents.size()); for (String component : trackerComponents.keySet()) { ComponentRule componentRule = mMainModel.getComponentRule(component); if (componentRule == null) { componentRule = new ComponentRule(packageName, component, trackerComponents.get(component), Prefs.Blocking.getDefaultBlockingMethod()); } tagCloud.trackerComponents.add(componentRule); tagCloud.areAllTrackersBlocked &= componentRule.isBlocked(); } if (ThreadUtils.isInterrupted()) { return; } tagCloud.isSystemApp = ApplicationInfoCompat.isSystemApp(applicationInfo); tagCloud.isUpdatedSystemApp = (applicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0; String codePath = PackageUtils.getHiddenCodePathOrDefault(packageName, applicationInfo.publicSourceDir); tagCloud.isSystemlessPath = !isExternalApk && MagiskUtils.isSystemlessPath(codePath); if (!isExternalApk && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { DomainVerificationUserState userState = DomainVerificationManagerCompat .getDomainVerificationUserState(packageName, userId); if (userState != null) { tagCloud.canOpenLinks = userState.isLinkHandlingAllowed(); if (!userState.getHostToStateMap().isEmpty()) { tagCloud.hostsToOpen = userState.getHostToStateMap(); } } } tagCloud.splitCount = mMainModel.getSplitCount(); tagCloud.isDebuggable = (applicationInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0; tagCloud.isTestOnly = ApplicationInfoCompat.isTestOnly(applicationInfo); tagCloud.hasCode = (applicationInfo.flags & ApplicationInfo.FLAG_HAS_CODE) != 0; tagCloud.isOverlay = PackageInfoCompat2.getOverlayTarget(packageInfo) != null; tagCloud.hasRequestedLargeHeap = (applicationInfo.flags & ApplicationInfo.FLAG_LARGE_HEAP) != 0; if (ThreadUtils.isInterrupted()) { return; } tagCloud.isRunning = false; for (ActivityManager.RunningAppProcessInfo info : ActivityManagerCompat.getRunningAppProcesses()) { if (ArrayUtils.contains(info.pkgList, packageName)) { tagCloud.isRunning = true; break; } } tagCloud.runningServices = ActivityManagerCompat.getRunningServices(packageName, userId); tagCloud.isForceStopped = ApplicationInfoCompat.isStopped(applicationInfo); tagCloud.isAppEnabled = applicationInfo.enabled; tagCloud.isAppSuspended = ApplicationInfoCompat.isSuspended(applicationInfo); tagCloud.isAppHidden = ApplicationInfoCompat.isHidden(applicationInfo); if (ThreadUtils.isInterrupted()) { return; } tagCloud.magiskHiddenProcesses = MagiskHide.getProcesses(packageInfo); boolean magiskHideEnabled = false; for (MagiskProcess magiskProcess : tagCloud.magiskHiddenProcesses) { magiskHideEnabled |= magiskProcess.isEnabled(); for (ActivityManager.RunningServiceInfo info : tagCloud.runningServices) { if (info.process.startsWith(magiskProcess.name)) { magiskProcess.setRunning(true); } } } tagCloud.isMagiskHideEnabled = !isExternalApk && magiskHideEnabled; tagCloud.magiskDeniedProcesses = MagiskDenyList.getProcesses(packageInfo); boolean magiskDenyListEnabled = false; for (MagiskProcess magiskProcess : tagCloud.magiskDeniedProcesses) { magiskDenyListEnabled |= magiskProcess.isEnabled(); for (ActivityManager.RunningServiceInfo info : tagCloud.runningServices) { if (info.process.startsWith(magiskProcess.name)) { magiskProcess.setRunning(true); } } } tagCloud.isMagiskDenyListEnabled = !isExternalApk && magiskDenyListEnabled; if (ThreadUtils.isInterrupted()) { return; } List debloatObjects = StaticDataset.getDebloatObjects(); for (DebloatObject debloatObject : debloatObjects) { if (packageName.equals(debloatObject.packageName)) { tagCloud.bloatwareRemovalType = debloatObject.getRemoval(); break; } } if (ThreadUtils.isInterrupted()) { return; } if (!isExternalApk && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.MANAGE_SENSORS)) { tagCloud.sensorsEnabled = SensorServiceCompat.isSensorEnabled(packageName, userId); } else tagCloud.sensorsEnabled = true; try (ZipFile zipFile = new ZipFile(applicationInfo.publicSourceDir)) { Boolean isXposedModule = XposedModuleInfo.isXposedModule(applicationInfo, zipFile); if (!Boolean.FALSE.equals(isXposedModule)) { tagCloud.xposedModuleInfo = new XposedModuleInfo(applicationInfo, isXposedModule == null ? null : zipFile); } } catch (Throwable th) { th.printStackTrace(); } tagCloud.canWriteAndExecute = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && applicationInfo.targetSdkVersion < Build.VERSION_CODES.Q; tagCloud.hasKeyStoreItems = KeyStoreUtils.hasKeyStore(applicationInfo.uid); tagCloud.hasMasterKeyInKeyStore = KeyStoreUtils.hasMasterKey(applicationInfo.uid); tagCloud.usesPlayAppSigning = PackageUtils.usesPlayAppSigning(applicationInfo); if (ThreadUtils.isInterrupted()) { return; } tagCloud.backups = BackupUtils.getBackupMetadataFromDbNoLockValidate(packageName); if (!isExternalApk) { tagCloud.isBatteryOptimized = DeviceIdleManagerCompat.isBatteryOptimizedApp(packageName); } else { tagCloud.isBatteryOptimized = true; } if (!isExternalApk && SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.MANAGE_NETWORK_POLICY)) { tagCloud.netPolicies = NetworkPolicyManagerCompat.getUidPolicy(applicationInfo.uid); } else { tagCloud.netPolicies = 0; } if (!isExternalApk && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { try { tagCloud.ssaid = new SsaidSettings(userId) .getSsaid(packageName, applicationInfo.uid); if (TextUtils.isEmpty(tagCloud.ssaid)) tagCloud.ssaid = null; } catch (IOException ignore) { } } if (!isExternalApk) { if (ThreadUtils.isInterrupted()) { return; } List uriGrants = new UriManager().getGrantedUris(packageName); if (uriGrants != null) { Iterator uriGrantIterator = uriGrants.listIterator(); UriManager.UriGrant uriGrant; while (uriGrantIterator.hasNext()) { uriGrant = uriGrantIterator.next(); if (uriGrant.targetUserId != userId) { uriGrantIterator.remove(); } } tagCloud.uriGrants = uriGrants; } } if (ApplicationInfoCompat.isStaticSharedLibrary(applicationInfo)) { if (ThreadUtils.isInterrupted()) { return; } List staticSharedLibraryNames = new ArrayList<>(); // Check for packages by the same packagename List appList; try { appList = PackageManagerCompat.getInstalledApplications(PackageManagerCompat.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES, userId); for (ApplicationInfo info : appList) { if (info.packageName.equals(packageName)) { staticSharedLibraryNames.add(info.processName); } } } catch (Throwable ignore) { staticSharedLibraryNames.add(applicationInfo.processName); } tagCloud.staticSharedLibraryNames = staticSharedLibraryNames.toArray(new String[0]); } if (ThreadUtils.isInterrupted()) { return; } mTagCloud.postValue(tagCloud); } catch (Throwable th) { // Unknown behaviour ThreadUtils.postOnMainThread(() -> { // Throw Runtime exception in main thread to crash the app throw new RuntimeException(th); }); } } @AnyThread public void loadAppInfo(@NonNull PackageInfo packageInfo, boolean isExternalApk) { if (mAppInfoFuture != null) { mAppInfoFuture.cancel(true); } mAppInfoFuture = ThreadUtils.postOnBackgroundThread(() -> loadAppInfoInternal(packageInfo, isExternalApk)); } @WorkerThread private void loadAppInfoInternal(@NonNull PackageInfo packageInfo, boolean isExternalApk) { String packageName = packageInfo.packageName; ApplicationInfo applicationInfo = packageInfo.applicationInfo; int userId = UserHandleHidden.getUserId(applicationInfo.uid); PackageManager pm = getApplication().getPackageManager(); AppInfo appInfo = new AppInfo(); try { if (!isExternalApk) { // Set source dir appInfo.sourceDir = new File(applicationInfo.publicSourceDir).getParent(); // Set data dirs appInfo.dataDir = applicationInfo.dataDir; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { appInfo.dataDeDir = applicationInfo.deviceProtectedDataDir; } // Set directories appInfo.extDataDirs = new ArrayList<>(); OsEnvironment.UserEnvironment ue = OsEnvironment.getUserEnvironment(userId); Path[] externalDataDirs = ue.buildExternalStorageAppDataDirs(packageName); for (Path externalDataDir : externalDataDirs) { Path accessiblePath = Paths.getAccessiblePath(externalDataDir); if (accessiblePath.exists()) { appInfo.extDataDirs.add(Objects.requireNonNull(accessiblePath.getFilePath())); } } // Set JNI dir if (Paths.exists(applicationInfo.nativeLibraryDir)) { appInfo.jniDir = applicationInfo.nativeLibraryDir; } boolean hasUsageAccess = FeatureController.isUsageAccessEnabled() && SelfPermissions.checkUsageStatsPermission(); if (hasUsageAccess) { // Net statistics AppUsageStatsManager.DataUsage dataUsage; TimeInterval interval = UsageUtils.getLastWeek(); dataUsage = AppUsageStatsManager.getDataUsageForPackage(applicationInfo.uid, interval); if (dataUsage.getTotal() == 0 && !ArrayUtils.contains( packageInfo.requestedPermissions, Manifest.permission.INTERNET)) { appInfo.dataUsage = null; } else appInfo.dataUsage = dataUsage; // Set sizes appInfo.sizeInfo = PackageUtils.getPackageSizeInfo(getApplication(), packageName, userId, Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? applicationInfo.storageUuid : null); } // Set installer app InstallSourceInfoCompat installSourceInfo = ExUtils.exceptionAsNull(() -> PackageManagerCompat.getInstallSourceInfo(packageName, userId)); if (installSourceInfo != null) { if (installSourceInfo.getInstallingPackageName() != null) { CharSequence label = PackageUtils.getPackageLabel(pm, installSourceInfo.getInstallingPackageName(), userId); appInfo.installerApp = label; installSourceInfo.setInstallingPackageLabel(label); } if (installSourceInfo.getInitiatingPackageName() != null) { CharSequence label = PackageUtils.getPackageLabel(pm, installSourceInfo.getInitiatingPackageName(), userId); if (appInfo.installerApp == null) { appInfo.installerApp = label; } installSourceInfo.setInitiatingPackageLabel(label); } if (installSourceInfo.getOriginatingPackageName() != null) { installSourceInfo.setOriginatingPackageLabel(PackageUtils.getPackageLabel(pm, installSourceInfo.getOriginatingPackageName(), userId)); } appInfo.installSource = installSourceInfo; } // Set main activity appInfo.mainActivity = PackageManagerCompat.getLaunchIntentForPackage(packageName, userId); // SELinux appInfo.seInfo = ApplicationInfoCompat.getSeInfo(applicationInfo); // Primary ABI appInfo.primaryCpuAbi = ApplicationInfoCompat.getPrimaryCpuAbi(applicationInfo); // zygotePreloadName appInfo.zygotePreloadName = ApplicationInfoCompat.getZygotePreloadName(applicationInfo); // hiddenApiEnforcementPolicy appInfo.hiddenApiEnforcementPolicy = ApplicationInfoCompat.getHiddenApiEnforcementPolicy(applicationInfo); } mAppInfo.postValue(appInfo); } catch (Throwable th) { // Unknown behaviour ThreadUtils.postOnMainThread(() -> { // Throw Runtime exception in main thread to crash the app throw new RuntimeException(th); }); } } public void installExisting(@NonNull String packageName, @UserIdInt int userId) { mExecutor.submit(() -> { PackageInstallerCompat installer = PackageInstallerCompat.getNewInstance(); installer.setOnInstallListener(new PackageInstallerCompat.OnInstallListener() { @Override public void onStartInstall(int sessionId, String packageName) { } @Override public void onFinishedInstall(int sessionId, String packageName, int result, @Nullable String blockingPackage, @Nullable String statusMessage) { StringBuilder sb = new StringBuilder(); sb.append(PackageInstallerService.getStringFromStatus(getApplication(), result, getAppLabel().getValue(), blockingPackage)); if (statusMessage != null) { sb.append("\n\n").append(statusMessage); } mInstallExistingResult.postValue(new Pair<>(result, sb)); } }); installer.installExisting(packageName, userId); }); } public static class TagCloud { public List trackerComponents; public boolean areAllTrackersBlocked = true; public boolean isSystemApp; public boolean isSystemlessPath; public boolean isUpdatedSystemApp; public boolean canOpenLinks; /** * Hosts that can be opened by the app (Android 12+). State is one of {@link DomainVerificationUserState#DOMAIN_STATE_NONE}, * {@link DomainVerificationUserState#DOMAIN_STATE_SELECTED}, {@link DomainVerificationUserState#DOMAIN_STATE_VERIFIED}. */ public Map hostsToOpen; public int splitCount; public boolean isDebuggable; public boolean isTestOnly; public boolean hasCode; public boolean isOverlay; public boolean hasRequestedLargeHeap; public boolean isRunning; public List runningServices; public List magiskHiddenProcesses; public List magiskDeniedProcesses; public boolean isForceStopped; public boolean isAppEnabled; public boolean isAppHidden; public boolean isAppSuspended; public boolean isMagiskHideEnabled; public boolean isMagiskDenyListEnabled; @DebloatObject.Removal public int bloatwareRemovalType; public boolean sensorsEnabled; @Nullable public XposedModuleInfo xposedModuleInfo; public boolean canWriteAndExecute; public boolean hasKeyStoreItems; public boolean hasMasterKeyInKeyStore; public boolean usesPlayAppSigning; public List backups; public boolean isBatteryOptimized; public int netPolicies; @Nullable public String ssaid; @Nullable public List uriGrants; @Nullable public String[] staticSharedLibraryNames; } public static class AppInfo { // Paths & dirs @Nullable public String sourceDir; @Nullable public String dataDir; @Nullable public String dataDeDir; public List extDataDirs = Collections.emptyList(); @Nullable public String jniDir; // Data usage @Nullable public AppUsageStatsManager.DataUsage dataUsage; @Nullable public PackageSizeInfo sizeInfo; // More info @Nullable public InstallSourceInfoCompat installSource; @Nullable public CharSequence installerApp; @Nullable public Intent mainActivity; @Nullable public String seInfo; @Nullable public String primaryCpuAbi; @Nullable public String zygotePreloadName; public int hiddenApiEnforcementPolicy; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/details/info/ListItem.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.details.info; import android.view.View; import androidx.annotation.DrawableRes; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import io.github.muntashirakon.AppManager.R; class ListItem { @IntDef(value = { LIST_ITEM_GROUP_BEGIN, LIST_ITEM_REGULAR, LIST_ITEM_REGULAR_ACTION, LIST_ITEM_INLINE }) @interface ListItemType { } static final int LIST_ITEM_GROUP_BEGIN = 0; // Group header static final int LIST_ITEM_REGULAR = 1; static final int LIST_ITEM_REGULAR_ACTION = 2; static final int LIST_ITEM_INLINE = 3; @ListItemType public final int type; @Nullable private CharSequence mTitle; @Nullable private CharSequence mSubtitle; @DrawableRes private int mActionIconRes; @StringRes private int mActionContentDescriptionRes; @Nullable private CharSequence mActionContentDescription; @Nullable private View.OnClickListener mOnActionClickListener; private boolean mIsSelectable; private boolean mIsMonospace; @NonNull public static ListItem newGroupStart(@Nullable CharSequence header) { ListItem listItem = new ListItem(LIST_ITEM_GROUP_BEGIN); listItem.mTitle = header; return listItem; } @NonNull public static ListItem newInlineItem(@Nullable CharSequence title, @Nullable CharSequence subtitle) { ListItem listItem = new ListItem(LIST_ITEM_INLINE); listItem.mTitle = title; listItem.mSubtitle = subtitle; return listItem; } @NonNull public static ListItem newRegularItem(@Nullable CharSequence title, @Nullable CharSequence subtitle) { ListItem listItem = new ListItem(LIST_ITEM_REGULAR); listItem.mTitle = title; listItem.mSubtitle = subtitle; return listItem; } @NonNull public static ListItem newSelectableRegularItem(@Nullable CharSequence title, @Nullable CharSequence subtitle) { ListItem listItem = new ListItem(LIST_ITEM_REGULAR); listItem.mIsSelectable = true; listItem.mTitle = title; listItem.mSubtitle = subtitle; return listItem; } @NonNull public static ListItem newSelectableRegularItem(@Nullable CharSequence title, @Nullable CharSequence subtitle, @Nullable View.OnClickListener actionListener) { ListItem listItem = new ListItem(LIST_ITEM_REGULAR_ACTION); listItem.mIsSelectable = true; listItem.mTitle = title; listItem.mSubtitle = subtitle; listItem.mActionIconRes = R.drawable.ic_open_in_new; listItem.mOnActionClickListener = actionListener; return listItem; } public ListItem(int listType) { this.type = listType; } @Nullable public CharSequence getTitle() { return mTitle; } public void setTitle(@Nullable CharSequence title) { this.mTitle = title; } @Nullable public CharSequence getSubtitle() { return mSubtitle; } public void setSubtitle(@Nullable CharSequence subtitle) { this.mSubtitle = subtitle; } @DrawableRes public int getActionIconRes() { return mActionIconRes; } public void setActionIcon(@DrawableRes int actionIcon) { this.mActionIconRes = actionIcon; } @Nullable public View.OnClickListener getOnActionClickListener() { return mOnActionClickListener; } public void setOnActionClickListener(@Nullable View.OnClickListener onActionClickListener) { this.mOnActionClickListener = onActionClickListener; } @StringRes public int getActionContentDescriptionRes() { return mActionContentDescriptionRes; } @Nullable public CharSequence getActionContentDescription() { return mActionContentDescription; } public void setActionContentDescription(@StringRes int contentDescriptionRes) { this.mActionContentDescriptionRes = contentDescriptionRes; } public void setActionContentDescription(@Nullable CharSequence contentDescription) { this.mActionContentDescription = contentDescription; } public boolean isMonospace() { return mIsMonospace; } public void setMonospace(boolean monospace) { mIsMonospace = monospace; } public boolean isSelectable() { return mIsSelectable; } public void setSelectable(boolean selectable) { mIsSelectable = selectable; } @NonNull @Override public String toString() { return "ListItem{" + "type=" + type + ", title='" + mTitle + '\'' + ", subtitle='" + mSubtitle + '\'' + '}'; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/details/info/TagItem.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.details.info; import android.content.Context; import android.content.res.ColorStateList; import android.graphics.Color; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.core.graphics.ColorUtils; import com.google.android.material.chip.Chip; import io.github.muntashirakon.AppManager.R; class TagItem { @StringRes private int mTextRes; @Nullable private CharSequence mText; @ColorInt private int mColor; private boolean mColorSet = false; private View.OnClickListener mOnClickListener; public TagItem setTextRes(@StringRes int textRes) { mTextRes = textRes; return this; } public TagItem setText(@Nullable CharSequence text) { mText = text; return this; } public TagItem setColor(@ColorInt int color) { mColor = color; mColorSet = true; return this; } public TagItem setOnClickListener(View.OnClickListener clickListener) { mOnClickListener = clickListener; return this; } public Chip toChip(@NonNull Context context, @NonNull ViewGroup parent) { Chip chip = (Chip) LayoutInflater.from(context).inflate(R.layout.item_chip, parent, false); if (mTextRes != 0) { chip.setText(mTextRes); } else chip.setText(mText); if (mColorSet) { chip.setChipBackgroundColor(ColorStateList.valueOf(mColor)); double luminance = ColorUtils.calculateLuminance(mColor); chip.setTextColor(luminance < 0.5 ? Color.WHITE : Color.BLACK); } if (mOnClickListener != null) { chip.setOnClickListener(mOnClickListener); } return chip; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/details/manifest/ManifestViewerActivity.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.details.manifest; import android.annotation.SuppressLint; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.view.MenuItem; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.UiThread; import androidx.lifecycle.ViewModelProvider; import com.google.android.material.progressindicator.LinearProgressIndicator; import io.github.muntashirakon.AppManager.BaseActivity; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.apk.ApkSource; import io.github.muntashirakon.AppManager.editor.CodeEditorFragment; import io.github.muntashirakon.AppManager.intercept.IntentCompat; import io.github.muntashirakon.AppManager.utils.UIUtils; public class ManifestViewerActivity extends BaseActivity { public static final String EXTRA_PACKAGE_NAME = "pkg"; private ManifestViewerViewModel mModel; @SuppressLint("WrongConstant") @Override protected void onAuthenticated(Bundle savedInstanceState) { setContentView(R.layout.activity_code_editor); setSupportActionBar(findViewById(R.id.toolbar)); mModel = new ViewModelProvider(this).get(ManifestViewerViewModel.class); LinearProgressIndicator progressIndicator = findViewById(R.id.progress_linear); progressIndicator.setVisibilityAfterHide(View.GONE); final Intent intent = getIntent(); final Uri packageUri = IntentCompat.getDataUri(intent); String packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME); if (packageUri == null && packageName == null) { showErrorAndFinish(); return; } final ApkSource apkSource = packageUri != null ? ApkSource.getApkSource(packageUri, intent.getType()) : null; mModel.getManifestLiveData().observe(this, manifest -> { CodeEditorFragment.Options options = new CodeEditorFragment.Options.Builder() .setTitle(getString(R.string.manifest_viewer)) .setSubtitle("AndroidManifest.xml") .setReadOnly(true) .setUri(manifest) .setJavaSmaliToggle(false) .setEnableSharing(true) .build(); CodeEditorFragment fragment = new CodeEditorFragment(); Bundle args = new Bundle(); args.putParcelable(CodeEditorFragment.ARG_OPTIONS, options); fragment.setArguments(args); getSupportFragmentManager() .beginTransaction() .replace(R.id.container, fragment) .commit(); }); mModel.loadApkFile(apkSource, packageName); } @UiThread private void showErrorAndFinish() { UIUtils.displayShortToast(R.string.error); finish(); } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int id = item.getItemId(); if (id == android.R.id.home) { finish(); return true; } return super.onOptionsItemSelected(item); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/details/manifest/ManifestViewerViewModel.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.details.manifest; import android.app.Application; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.net.Uri; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import java.io.File; import java.io.PrintStream; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.concurrent.Future; import io.github.muntashirakon.AppManager.apk.ApkFile; import io.github.muntashirakon.AppManager.apk.ApkSource; import io.github.muntashirakon.AppManager.apk.parser.AndroidBinXmlDecoder; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.self.filecache.FileCache; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.io.IoUtils; public class ManifestViewerViewModel extends AndroidViewModel { public static final String TAG = ManifestViewerViewModel.class.getSimpleName(); private ApkFile mApkFile; @Nullable private Future mManifestLoaderResult; private final FileCache mFileCache = new FileCache(); private final MutableLiveData mManifestLiveData = new MutableLiveData<>(); public ManifestViewerViewModel(@NonNull Application application) { super(application); } @Override protected void onCleared() { if (mManifestLoaderResult != null) { mManifestLoaderResult.cancel(true); } IoUtils.closeQuietly(mApkFile); IoUtils.closeQuietly(mFileCache); super.onCleared(); } public LiveData getManifestLiveData() { return mManifestLiveData; } public void loadApkFile(@Nullable ApkSource apkSource, @Nullable String packageName) { mManifestLoaderResult = ThreadUtils.postOnBackgroundThread(() -> { final PackageManager pm = getApplication().getPackageManager(); ApkSource realApkSource; if (apkSource != null) { realApkSource = apkSource; } else { try { ApplicationInfo applicationInfo = pm.getApplicationInfo(packageName, 0); realApkSource = ApkSource.getApkSource(applicationInfo); } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "Error: ", e); return; } } try { mApkFile = realApkSource.resolve(); } catch (ApkFile.ApkFileException e) { Log.e(TAG, "Error: ", e); return; } if (ThreadUtils.isInterrupted()) { return; } if (mApkFile != null) { ByteBuffer byteBuffer = mApkFile.getBaseEntry().manifest; // Reset properties byteBuffer.position(0); byteBuffer.order(ByteOrder.LITTLE_ENDIAN); try { File cachedFile = mFileCache.createCachedFile("xml"); try (PrintStream ps = new PrintStream(cachedFile)) { AndroidBinXmlDecoder.decode(byteBuffer, ps); } mManifestLiveData.postValue(Uri.fromFile(cachedFile)); } catch (Throwable e) { Log.e(TAG, "Could not parse APK", e); } } }); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/details/struct/AppDetailsActivityItem.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.details.struct; import android.content.pm.ActivityInfo; import androidx.annotation.NonNull; public class AppDetailsActivityItem extends AppDetailsComponentItem { public boolean canLaunchAssist; public AppDetailsActivityItem(@NonNull ActivityInfo componentInfo) { super(componentInfo); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/details/struct/AppDetailsAppOpItem.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.details.struct; import android.app.AppOpsManager; import android.content.pm.PackageInfo; import android.content.pm.PermissionInfo; import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresPermission; import androidx.annotation.WorkerThread; import androidx.core.content.pm.PermissionInfoCompat; import java.util.List; import java.util.Objects; import io.github.muntashirakon.AppManager.compat.AppOpsManagerCompat; import io.github.muntashirakon.AppManager.compat.ManifestCompat; import io.github.muntashirakon.AppManager.permission.DevelopmentPermission; import io.github.muntashirakon.AppManager.permission.PermUtils; import io.github.muntashirakon.AppManager.permission.Permission; import io.github.muntashirakon.AppManager.permission.PermissionException; import io.github.muntashirakon.AppManager.permission.ReadOnlyPermission; import io.github.muntashirakon.AppManager.permission.RuntimePermission; public class AppDetailsAppOpItem extends AppDetailsItem { @Nullable public final Permission permission; @Nullable public final PermissionInfo permissionInfo; public final boolean isDangerous; public final boolean hasModifiablePermission; /** * Whether the permission is part of the app. */ public final boolean appContainsPermission; @Nullable private AppOpsManagerCompat.OpEntry mOpEntry; public AppDetailsAppOpItem(@NonNull AppOpsManagerCompat.OpEntry opEntry) { this(opEntry.getOp()); name = opEntry.getName(); mOpEntry = opEntry; } public AppDetailsAppOpItem(int op) { super(op); name = AppOpsManagerCompat.opToName(op); mOpEntry = null; permissionInfo = null; permission = null; isDangerous = false; hasModifiablePermission = false; appContainsPermission = false; } public AppDetailsAppOpItem(@NonNull AppOpsManagerCompat.OpEntry opEntry, @NonNull PermissionInfo permissionInfo, boolean isGranted, int permissionFlags, boolean appContainsPermission) { super(opEntry.getOp()); name = opEntry.getName(); mOpEntry = opEntry; this.permissionInfo = permissionInfo; this.appContainsPermission = appContainsPermission; isDangerous = PermissionInfoCompat.getProtection(permissionInfo) == PermissionInfo.PROTECTION_DANGEROUS; int protectionFlags = PermissionInfoCompat.getProtectionFlags(permissionInfo); if (isDangerous && PermUtils.systemSupportsRuntimePermissions()) { permission = new RuntimePermission(permissionInfo.name, isGranted, opEntry.getOp(), isAllowed(), permissionFlags); } else if ((protectionFlags & PermissionInfo.PROTECTION_FLAG_DEVELOPMENT) != 0) { permission = new DevelopmentPermission(permissionInfo.name, isGranted, opEntry.getOp(), isAllowed(), permissionFlags); } else { permission = new ReadOnlyPermission(permissionInfo.name, isGranted, opEntry.getOp(), isAllowed(), permissionFlags); } hasModifiablePermission = PermUtils.isModifiable(permission); } public AppDetailsAppOpItem(int op, @NonNull PermissionInfo permissionInfo, boolean isGranted, int permissionFlags, boolean appContainsPermission) { super(op); name = AppOpsManagerCompat.opToName(op); mOpEntry = null; this.permissionInfo = permissionInfo; this.appContainsPermission = appContainsPermission; isDangerous = PermissionInfoCompat.getProtection(permissionInfo) == PermissionInfo.PROTECTION_DANGEROUS; int protectionFlags = PermissionInfoCompat.getProtectionFlags(permissionInfo); if (isDangerous && PermUtils.systemSupportsRuntimePermissions()) { permission = new RuntimePermission(permissionInfo.name, isGranted, op, isAllowed(), permissionFlags); } else if ((protectionFlags & PermissionInfo.PROTECTION_FLAG_DEVELOPMENT) != 0) { permission = new DevelopmentPermission(permissionInfo.name, isGranted, op, isAllowed(), permissionFlags); } else { permission = new ReadOnlyPermission(permissionInfo.name, isGranted, op, isAllowed(), permissionFlags); } hasModifiablePermission = PermUtils.isModifiable(permission); } public int getOp() { return item; } @AppOpsManagerCompat.Mode public int getMode() { if (mOpEntry != null) { return mOpEntry.getMode(); } return AppOpsManagerCompat.opToDefaultMode(getOp()); } public long getDuration() { if (mOpEntry != null) { return mOpEntry.getDuration(); } return 0L; } public long getTime() { if (mOpEntry != null) { return mOpEntry.getTime(); } return 0L; } public long getRejectTime() { if (mOpEntry != null) { return mOpEntry.getRejectTime(); } return 0L; } public boolean isRunning() { return mOpEntry != null && mOpEntry.isRunning(); } public boolean isAllowed() { boolean isAllowed = false; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { isAllowed = getMode() == AppOpsManager.MODE_FOREGROUND; } isAllowed |= getMode() == AppOpsManager.MODE_ALLOWED; // Special case for default if (getMode() == AppOpsManager.MODE_DEFAULT) { isAllowed |= (permission != null && permission.isGranted()); } return isAllowed; } /** * Allow the app op. * *

This also automatically grants the permission associated with the app op. */ @RequiresPermission(allOf = { "android.permission.MANAGE_APP_OPS_MODES", ManifestCompat.permission.GRANT_RUNTIME_PERMISSIONS, }) @WorkerThread public void allowAppOp(@NonNull PackageInfo packageInfo, @NonNull AppOpsManagerCompat appOpsManager) throws PermissionException { if (hasModifiablePermission && permission != null) { PermUtils.grantPermission(packageInfo, permission, appOpsManager, true, true); } else { PermUtils.allowAppOp(appOpsManager, getOp(), packageInfo.packageName, packageInfo.applicationInfo.uid); } invalidate(appOpsManager, packageInfo); } /** * Disallow the app op. * *

This also revokes the permission associated with the app op. */ @RequiresPermission(allOf = { "android.permission.MANAGE_APP_OPS_MODES", ManifestCompat.permission.REVOKE_RUNTIME_PERMISSIONS, }) @WorkerThread public void disallowAppOp(@NonNull PackageInfo packageInfo, @NonNull AppOpsManagerCompat appOpsManager) throws PermissionException { if (hasModifiablePermission && permission != null) { PermUtils.revokePermission(packageInfo, permission, appOpsManager, true); } else { PermUtils.disallowAppOp(appOpsManager, getOp(), packageInfo.packageName, packageInfo.applicationInfo.uid); } invalidate(appOpsManager, packageInfo); } /** * Set mode for app op. * *

This also grants/revoke the permission associated with the app op. */ @RequiresPermission(allOf = { "android.permission.MANAGE_APP_OPS_MODES", ManifestCompat.permission.GRANT_RUNTIME_PERMISSIONS, ManifestCompat.permission.REVOKE_RUNTIME_PERMISSIONS, }) @WorkerThread public void setAppOp(@NonNull PackageInfo packageInfo, @NonNull AppOpsManagerCompat appOpsManager, @AppOpsManagerCompat.Mode int mode) throws PermissionException { if (hasModifiablePermission && permission != null) { boolean isAllowed = false; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { isAllowed = getMode() == AppOpsManager.MODE_FOREGROUND; } isAllowed |= getMode() == AppOpsManager.MODE_ALLOWED; if (isAllowed) { PermUtils.grantPermission(packageInfo, permission, appOpsManager, true, true); } else { PermUtils.revokePermission(packageInfo, permission, appOpsManager, true); } } PermUtils.setAppOpMode(appOpsManager, getOp(), packageInfo.packageName, packageInfo.applicationInfo.uid, mode); invalidate(appOpsManager, packageInfo); } @RequiresPermission("android.permission.MANAGE_APP_OPS_MODES") public void invalidate(@NonNull AppOpsManagerCompat appOpsManager, @NonNull PackageInfo packageInfo) throws PermissionException { try { List opEntryList = appOpsManager.getOpsForPackage(packageInfo.applicationInfo.uid, packageInfo.packageName, new int[]{getOp()}).get(0).getOps(); mOpEntry = !opEntryList.isEmpty() ? opEntryList.get(0) : null; } catch (Exception e) { throw new PermissionException(e); } } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof AppDetailsAppOpItem)) return false; if (!super.equals(o)) return false; AppDetailsAppOpItem that = (AppDetailsAppOpItem) o; return Objects.equals(item, that.item); } @Override public int hashCode() { return Objects.hash(item); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/details/struct/AppDetailsComponentItem.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.details.struct; import android.content.pm.ComponentInfo; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import io.github.muntashirakon.AppManager.rules.struct.ComponentRule; /** * Stores individual app details component item */ public class AppDetailsComponentItem extends AppDetailsItem { public CharSequence label; public boolean canLaunch; private boolean mIsTracker; @Nullable private ComponentRule mRule; private boolean mIsDisabled; public AppDetailsComponentItem(@NonNull ComponentInfo componentInfo) { super(componentInfo); name = componentInfo.name; mIsDisabled = !componentInfo.isEnabled(); } public boolean isTracker() { return mIsTracker; } public void setTracker(boolean tracker) { mIsTracker = tracker; } public boolean isBlocked() { if (mRule == null) { return false; } return mRule.isBlocked() && (mRule.isIfw() || isDisabled()); } @Nullable public ComponentRule getRule() { return mRule; } public void setRule(@Nullable ComponentRule rule) { mRule = rule; } public boolean isDisabled() { return mIsDisabled; } public void setDisabled(boolean disabled) { mIsDisabled = disabled; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/details/struct/AppDetailsDefinedPermissionItem.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.details.struct; import android.content.pm.PermissionInfo; import androidx.annotation.NonNull; public class AppDetailsDefinedPermissionItem extends AppDetailsItem { public final boolean isExternal; public AppDetailsDefinedPermissionItem(@NonNull PermissionInfo permissionInfo, boolean isExternal) { super(permissionInfo); name = permissionInfo.name; this.isExternal = isExternal; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/details/struct/AppDetailsFeatureItem.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.details.struct; import android.content.pm.FeatureInfo; import androidx.annotation.NonNull; public class AppDetailsFeatureItem extends AppDetailsItem { public static final String OPEN_GL_ES = "OpenGL ES"; public final boolean required; public final boolean available; public AppDetailsFeatureItem(@NonNull FeatureInfo featureInfo, boolean available) { super(featureInfo); // Currently, feature only has a single flag, which specifies whether the feature is required. this.required = (featureInfo.flags & FeatureInfo.FLAG_REQUIRED) != 0; this.available = available; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/details/struct/AppDetailsItem.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.details.struct; import androidx.annotation.NonNull; import java.util.Objects; /** * Stores individual app details item */ public class AppDetailsItem { @NonNull public T item; @NonNull public String name = ""; public AppDetailsItem(@NonNull T object) { item = object; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof AppDetailsItem)) return false; AppDetailsItem that = (AppDetailsItem) o; return name.equals(that.name); } @Override public int hashCode() { return Objects.hash(name); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/details/struct/AppDetailsLibraryItem.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.details.struct; import java.io.File; public class AppDetailsLibraryItem extends AppDetailsItem { public long size; public String type; public File path; public AppDetailsLibraryItem(T item) { super(item); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/details/struct/AppDetailsOverlayItem.java ================================================ package io.github.muntashirakon.AppManager.details.struct; import android.annotation.UserIdInt; import android.content.om.IOverlayManager; import android.content.om.OverlayInfo; import android.content.om.OverlayInfoHidden; import android.os.Build; import android.os.RemoteException; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import dev.rikka.tools.refine.Refine; @RequiresApi(Build.VERSION_CODES.O) public class AppDetailsOverlayItem extends AppDetailsItem { @SuppressWarnings("NewApi") // Required due to sdk lying about the real api version requirement for overlay info public AppDetailsOverlayItem(@NonNull OverlayInfo overlayInfo) { super(Refine.unsafeCast(overlayInfo)); if (overlayInfo.getOverlayName() != null) { name = overlayInfo.getOverlayName(); } else { name = overlayInfo.getOverlayIdentifier().toString(); } } public String getPackageName() { return item.packageName; } @Nullable public String getCategory() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { return item.category; } return null; } public boolean isEnabled() { return item.isEnabled(); } @SuppressWarnings("deprecation") public boolean isMutable() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { return item.isMutable; } if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) { return getState() != OverlayInfoHidden.STATE_ENABLED_IMMUTABLE; } return true; } public String getReadableState() { return stateToString(item.state); } public int getState() { return item.state; } @RequiresApi(Build.VERSION_CODES.P) public int getPriority() { return item.priority; } public boolean setEnabled(@NonNull IOverlayManager mgr, boolean enabled) throws RemoteException { return mgr.setEnabled(getPackageName(), enabled, item.userId); } public boolean setPriority(@NonNull IOverlayManager mgr, String newParentPackageName) throws RemoteException { return mgr.setPriority(getPackageName(), newParentPackageName, item.userId); } public boolean setHighestPriority(@NonNull IOverlayManager mgr) throws RemoteException { return mgr.setHighestPriority(item.packageName, item.userId); } public boolean setLowestPriority(@NonNull IOverlayManager mgr) throws RemoteException { return mgr.setLowestPriority(item.packageName, item.userId); } public static String stateToString(@OverlayInfoHidden.State int state) { return OverlayInfoHidden.stateToString(state); } @Nullable public String getOverlayName() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { return item.overlayName; } return null; } public String getBaseCodePath() { return item.baseCodePath; } @UserIdInt public int getUserId() { return item.userId; } public boolean isFabricated() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { return item.isFabricated; } return false; } @NonNull @Override public String toString() { return "AppDetailsOverlayItem: { " + item + " }"; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/details/struct/AppDetailsPermissionItem.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.details.struct; import android.content.pm.PackageInfo; import android.content.pm.PermissionInfo; import android.os.RemoteException; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import androidx.core.content.pm.PermissionInfoCompat; import io.github.muntashirakon.AppManager.compat.AppOpsManagerCompat; import io.github.muntashirakon.AppManager.permission.PermUtils; import io.github.muntashirakon.AppManager.permission.Permission; import io.github.muntashirakon.AppManager.permission.PermissionException; /** * Stores individual app details item */ public class AppDetailsPermissionItem extends AppDetailsItem { @NonNull public final Permission permission; public final boolean isDangerous; // AKA Runtime public final boolean modifiable; public final int flags; public final int protectionFlags; @Nullable public final PermUtils.SettingItem settingItem; public AppDetailsPermissionItem(@NonNull PermissionInfo permissionInfo, @NonNull Permission permission, int flags) { super(permissionInfo); this.permission = permission; this.isDangerous = PermissionInfoCompat.getProtection(permissionInfo) == PermissionInfo.PROTECTION_DANGEROUS; this.protectionFlags = PermissionInfoCompat.getProtectionFlags(permissionInfo); this.modifiable = PermUtils.isModifiable(permission); this.flags = flags; this.settingItem = PermUtils.permissionNameToSettingItem.get(permissionInfo.name); } public boolean isGranted() { if (!permission.isReadOnly()) { return permission.isGrantedIncludingAppOp(); } if (permission.affectsAppOp()) { return permission.isAppOpAllowed(); } return permission.isGranted(); } /** * Grant the permission. * *

This also automatically grants app op if it has app op. */ @WorkerThread public void grantPermission(@NonNull PackageInfo packageInfo, @NonNull AppOpsManagerCompat appOpsManager) throws RemoteException, PermissionException { PermUtils.grantPermission(packageInfo, permission, appOpsManager, true, true); } /** * Revoke the permission. * *

This also disallows the app op for the permission if it has app op. */ @WorkerThread public void revokePermission(@NonNull PackageInfo packageInfo, AppOpsManagerCompat appOpsManager) throws RemoteException, PermissionException { PermUtils.revokePermission(packageInfo, permission, appOpsManager, true); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/details/struct/AppDetailsServiceItem.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.details.struct; import android.app.ActivityManager; import android.content.pm.ServiceInfo; import androidx.annotation.NonNull; import androidx.annotation.Nullable; public class AppDetailsServiceItem extends AppDetailsComponentItem { @Nullable private ActivityManager.RunningServiceInfo mRunningServiceInfo; public AppDetailsServiceItem(@NonNull ServiceInfo serviceInfo) { super(serviceInfo); } public void setRunningServiceInfo(@Nullable ActivityManager.RunningServiceInfo runningServiceInfo) { mRunningServiceInfo = runningServiceInfo; } @Nullable public ActivityManager.RunningServiceInfo getRunningServiceInfo() { return mRunningServiceInfo; } public boolean isRunning() { return mRunningServiceInfo != null; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/dex/DexClasses.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.dex; import androidx.annotation.IntRange; import androidx.annotation.NonNull; import com.android.tools.smali.baksmali.Adaptors.ClassDefinition; import com.android.tools.smali.baksmali.BaksmaliOptions; import com.android.tools.smali.baksmali.formatter.BaksmaliFormatter; import com.android.tools.smali.baksmali.formatter.BaksmaliWriter; import com.android.tools.smali.dexlib2.Opcodes; import com.android.tools.smali.dexlib2.analysis.InlineMethodResolver; import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile; import com.android.tools.smali.dexlib2.dexbacked.DexBackedOdexFile; import com.android.tools.smali.dexlib2.iface.ClassDef; import com.android.tools.smali.dexlib2.iface.MultiDexContainer; import java.io.BufferedInputStream; import java.io.Closeable; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.StringWriter; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Objects; import io.github.muntashirakon.AppManager.BuildConfig; // FIXME: 8/2/22 Add support for lower SDKs by fixing Smali/Baksmali public class DexClasses implements Closeable { private final HashMap mClassNameClassDefMap = new HashMap<>(); private final HashMap> mBaseClassNestedClassMap = new HashMap<>(); // TODO: 18/10/21 Load frameworks.jar and add its dex files as options.classPath private final BaksmaliOptions mOptions; private final Opcodes mOpcodes; public DexClasses(@NonNull File apkFile, @IntRange(from = -1) int apiLevel) throws IOException { mOpcodes = apiLevel < 0 ? Opcodes.getDefault() : Opcodes.forApi(apiLevel); mOptions = new BaksmaliOptions(); // options mOptions.deodex = false; mOptions.implicitReferences = false; mOptions.parameterRegisters = true; mOptions.localsDirective = true; mOptions.sequentialLabels = true; mOptions.debugInfo = BuildConfig.DEBUG; mOptions.codeOffsets = false; mOptions.accessorComments = false; mOptions.registerInfo = 0; mOptions.inlineResolver = null; BaksmaliFormatter formatter = new BaksmaliFormatter(); MultiDexContainer container = DexUtils.loadApk(apkFile, apiLevel); List dexEntryNames = container.getDexEntryNames(); for (String dexEntryName : dexEntryNames) { MultiDexContainer.DexEntry dexEntry = Objects.requireNonNull(container.getEntry(dexEntryName)); DexBackedDexFile dexFile = dexEntry.getDexFile(); // Store list of classes for (ClassDef classDef : dexFile.getClasses()) { String name = formatter.getType(classDef.getType()); if (name.endsWith(";")) name = name.substring(0, name.length() - 1); if (name.startsWith("L")) { name = name.substring(1).replace('/', '.'); } mClassNameClassDefMap.put(name, classDef); String baseClass = DexUtils.getClassNameWithoutInnerClasses(name); List classes = mBaseClassNestedClassMap.get(baseClass); if (classes == null) { classes = new ArrayList<>(); mBaseClassNestedClassMap.put(baseClass, classes); } classes.add(name); } if (dexFile.supportsOptimizedOpcodes()) { throw new IOException("ODEX isn't supported."); } if (dexFile instanceof DexBackedOdexFile) { mOptions.inlineResolver = InlineMethodResolver.createInlineMethodResolver( ((DexBackedOdexFile) dexFile).getOdexVersion()); } } } public DexClasses(@NonNull InputStream inputStream, @IntRange(from = -1) int apiLevel) throws IOException { mOpcodes = apiLevel < 0 ? Opcodes.getDefault() : Opcodes.forApi(apiLevel); mOptions = new BaksmaliOptions(); // options mOptions.deodex = false; mOptions.implicitReferences = false; mOptions.parameterRegisters = true; mOptions.localsDirective = true; mOptions.sequentialLabels = true; mOptions.debugInfo = BuildConfig.DEBUG; mOptions.codeOffsets = false; mOptions.accessorComments = false; mOptions.registerInfo = 0; mOptions.inlineResolver = null; BaksmaliFormatter formatter = new BaksmaliFormatter(); InputStream is = new BufferedInputStream(inputStream); DexBackedDexFile dexFile = DexUtils.loadDexContainer(is, apiLevel); // Store list of classes for (ClassDef classDef : dexFile.getClasses()) { String name = formatter.getType(classDef.getType()); if (name.endsWith(";")) name = name.substring(0, name.length() - 1); if (name.startsWith("L")) { name = name.substring(1).replace('/', '.'); } mClassNameClassDefMap.put(name, classDef); String baseClass = DexUtils.getClassNameWithoutInnerClasses(name); List classes = mBaseClassNestedClassMap.get(baseClass); if (classes == null) { classes = new ArrayList<>(); mBaseClassNestedClassMap.put(baseClass, classes); } classes.add(name); } if (dexFile.supportsOptimizedOpcodes()) { throw new IOException("ODEX isn't supported."); } if (dexFile instanceof DexBackedOdexFile) { mOptions.inlineResolver = InlineMethodResolver.createInlineMethodResolver( ((DexBackedOdexFile) dexFile).getOdexVersion()); } } @NonNull public List getClassNames() { return new ArrayList<>(mClassNameClassDefMap.keySet()); } @NonNull public List getBaseClassNames() { return new ArrayList<>(mBaseClassNestedClassMap.keySet()); } @NonNull public ClassDef getClassDef(@NonNull String className) throws ClassNotFoundException { ClassDef classDef = mClassNameClassDefMap.get(className); if (classDef == null) throw new ClassNotFoundException(className + " could not be found."); return classDef; } @NonNull public String getJavaCode(@NonNull String className) throws ClassNotFoundException { try { String baseClass = DexUtils.getClassNameWithoutInnerClasses(className); List classes = mBaseClassNestedClassMap.get(baseClass); if (classes == null || classes.isEmpty() || !classes.contains(className)) { throw new ClassNotFoundException(); } List classDefs = new ArrayList<>(classes.size()); for (String cls : classes) { classDefs.add(getClassDef(cls)); } return DexUtils.toJavaCode(classDefs, mOpcodes); } catch (IOException e) { throw new ClassNotFoundException(e.getMessage(), e); } } @NonNull public String getClassContents(@NonNull String className) throws ClassNotFoundException { return getClassContents(getClassDef(className)); } @NonNull public String getClassContents(@NonNull ClassDef classdef) throws ClassNotFoundException { StringWriter stringWriter = new StringWriter(); try (BaksmaliWriter baksmaliWriter = new BaksmaliWriter(stringWriter)) { ClassDefinition classDefinition = new ClassDefinition(mOptions, classdef); classDefinition.writeTo(baksmaliWriter); return stringWriter.toString(); } catch (IOException e) { throw new ClassNotFoundException(e.getMessage(), e); } } @Override public void close() throws IOException { } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/dex/DexUtils.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.dex; import androidx.annotation.AnyThread; import androidx.annotation.NonNull; import com.android.tools.smali.dexlib2.DexFileFactory; import com.android.tools.smali.dexlib2.Opcodes; import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile; import com.android.tools.smali.dexlib2.dexbacked.DexBackedOdexFile; import com.android.tools.smali.dexlib2.iface.ClassDef; import com.android.tools.smali.dexlib2.iface.MultiDexContainer; import com.android.tools.smali.dexlib2.writer.builder.DexBuilder; import com.android.tools.smali.dexlib2.writer.io.DexDataStore; import com.android.tools.smali.dexlib2.writer.io.FileDataStore; import com.android.tools.smali.dexlib2.writer.pool.DexPool; import com.android.tools.smali.smali.smaliFlexLexer; import com.android.tools.smali.smali.smaliParser; import com.android.tools.smali.smali.smaliTreeWalker; import org.antlr.runtime.CommonTokenStream; import org.antlr.runtime.RecognitionException; import org.antlr.runtime.tree.CommonTree; import org.antlr.runtime.tree.CommonTreeNodeStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.StringReader; import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.io.Path; import jadx.api.JadxArgs; import jadx.api.JadxDecompiler; import jadx.api.JavaClass; import jadx.core.utils.files.FileUtils; public final class DexUtils { @NonNull public static String getClassNameWithoutInnerClasses(@NonNull String className) { int idxOfDollar = findFirstInnerClassIndex(className); return idxOfDollar >= 0 ? className.substring(0, idxOfDollar) : className; } public static int findFirstInnerClassIndex(@NonNull String className) { // Find first $ but without matching any . // This is better than String#indexOf(char) because it stops searching as soon as it finds a . int validDollarIndex = -1; for (int i = className.length() - 1; i >= 0; --i) { int ch = className.charAt(i); if (ch == '.') { // Found a ., no need to look any further return validDollarIndex; } if (ch == '$') { // Found a valid index validDollarIndex = i; // But there can be many, so look again } } return validDollarIndex; } @AnyThread public static boolean isDex(@NonNull Path path) throws IOException { int header; try (InputStream is = path.openInputStream()) { byte[] headerBytes = new byte[4]; is.read(headerBytes); header = new BigInteger(headerBytes).intValue(); } return header == 0x6465780A; } public static MultiDexContainer loadApk(File apkFile, int apiLevel) throws IOException { return DexFileFactory.loadDexContainer(apkFile, apiLevel < 0 ? Opcodes.getDefault() : Opcodes.forApi(apiLevel)); } public static void storeDex(@NonNull List classDefList, @NonNull DexDataStore dataStore, int apiLevel) throws IOException { Opcodes opcodes = apiLevel < 0 ? Opcodes.getDefault() : Opcodes.forApi(apiLevel); DexPool dexPool = new DexPool(opcodes); for (ClassDef classDef : classDefList) { dexPool.internClass(classDef); } dexPool.writeTo(dataStore); } @NonNull public static ClassDef toClassDef(@NonNull File smaliFile, int apiLevel) throws IOException, RecognitionException { try (InputStreamReader sr = new InputStreamReader(new FileInputStream(smaliFile), StandardCharsets.UTF_8)) { return toClassDef(sr, apiLevel); } } @NonNull public static ClassDef toClassDef(@NonNull String smaliContents, int apiLevel) throws IOException, RecognitionException { try (StringReader sr = new StringReader(smaliContents)) { return toClassDef(sr, apiLevel); } } @NonNull public static ClassDef toClassDef(@NonNull Reader smaliReader, int apiLevel) throws IOException, RecognitionException { Opcodes opcodes = apiLevel < 0 ? Opcodes.getDefault() : Opcodes.forApi(apiLevel); smaliFlexLexer lexer = new smaliFlexLexer(smaliReader, opcodes.api); CommonTokenStream tokens = new CommonTokenStream(lexer); smaliParser parser = new smaliParser(tokens); parser.setVerboseErrors(false); parser.setAllowOdex(false); parser.setApiLevel(opcodes.api); smaliParser.smali_file_return result = parser.smali_file(); if (parser.getNumberOfSyntaxErrors() > 0 || lexer.getNumberOfSyntaxErrors() > 0) { throw new IOException((parser.getNumberOfSyntaxErrors() + lexer.getNumberOfSyntaxErrors()) + " syntax errors during parsing and/or lexing."); } CommonTree t = result.getTree(); CommonTreeNodeStream treeStream = new CommonTreeNodeStream(t); treeStream.setTokenStream(tokens); DexBuilder dexBuilder = new DexBuilder(opcodes); smaliTreeWalker dexGen = new smaliTreeWalker(treeStream); dexGen.setApiLevel(opcodes.api); dexGen.setVerboseErrors(false); dexGen.setDexBuilder(dexBuilder); ClassDef classDef = dexGen.smali_file(); if (dexGen.getNumberOfSyntaxErrors() > 0) { throw new IOException(dexGen.getNumberOfSyntaxErrors() + " syntax errors during dex creation"); } if (classDef == null) { throw new IOException("Could not generate class from smali."); } return classDef; } @NonNull public static String toJavaCode(@NonNull List classDefs, @NonNull Opcodes opcodes) throws IOException { File tmp = FileUtils.createTempFile(".dex"); try { DexPool pool = new DexPool(opcodes); for (ClassDef classDef : classDefs) { pool.internClass(classDef); } pool.writeTo(new FileDataStore(tmp)); return toJavaCode(tmp); } finally { tmp.delete(); } } @NonNull public static String toJavaCode(@NonNull ClassDef classDef, @NonNull Opcodes opcodes) throws IOException { File tmp = FileUtils.createTempFile(".dex"); try { DexPool pool = new DexPool(opcodes); pool.internClass(classDef); pool.writeTo(new FileDataStore(tmp)); return toJavaCode(tmp); } finally { tmp.delete(); } } @NonNull public static String toJavaCode(@NonNull List smaliContents, int api) throws IOException { Opcodes opcodes = api < 0 ? Opcodes.getDefault() : Opcodes.forApi(api); try { List classDefs = new ArrayList<>(smaliContents.size()); for (String smaliContent : smaliContents) { classDefs.add(toClassDef(smaliContent, api)); } return toJavaCode(classDefs, opcodes); } catch (RecognitionException e) { throw new IOException(e); } } @NonNull public static String toJavaCode(@NonNull String smaliContent, int api) throws IOException { Opcodes opcodes = api < 0 ? Opcodes.getDefault() : Opcodes.forApi(api); try { return toJavaCode(toClassDef(smaliContent, api), opcodes); } catch (RecognitionException e) { throw new IOException(e); } } @NonNull public static String toJavaCode(@NonNull File dexFile) { JadxArgs args = new JadxArgs(); args.setInputFile(dexFile); args.setSkipResources(true); args.setShowInconsistentCode(true); args.setDebugInfo(BuildConfig.DEBUG); try (JadxDecompiler decompiler = new JadxDecompiler(args)) { decompiler.load(); JavaClass javaClass = decompiler.getClasses().iterator().next(); javaClass.decompile(); return javaClass.getCode(); } } @NonNull public static DexBackedDexFile loadDexContainer(@NonNull InputStream inputStream, int api) throws IOException { Opcodes opcodes = api < 0 ? Opcodes.getDefault() : Opcodes.forApi(api); try { return DexBackedDexFile.fromInputStream(opcodes, inputStream); } catch (DexBackedDexFile.NotADexFile ex) { // just eat it } try { return DexBackedOdexFile.fromInputStream(opcodes, inputStream); } catch (DexBackedOdexFile.NotAnOdexFile ex) { // just eat it } throw new DexFileFactory.UnsupportedFileTypeException("InputStream is not a dex, odex file."); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/editor/CodeEditorActivity.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.editor; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.view.MenuItem; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import com.google.android.material.progressindicator.LinearProgressIndicator; import io.github.muntashirakon.AppManager.BaseActivity; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.intercept.IntentCompat; import io.github.muntashirakon.io.Paths; public class CodeEditorActivity extends BaseActivity { public static final String ALIAS_EDITOR = "io.github.muntashirakon.AppManager.editor.EditorActivity"; private static final String EXTRA_READ_ONLY = "read_only"; public static Intent getIntent(@NonNull Context context, @NonNull Uri uri, @Nullable String title, @Nullable String subtitle, boolean readOnly) { return new Intent(context, CodeEditorActivity.class) .setData(uri) .putExtra(EXTRA_READ_ONLY, readOnly) .putExtra(Intent.EXTRA_TITLE, title) .putExtra(Intent.EXTRA_SUBJECT, subtitle); } public static Intent getIntent(@NonNull Context context, @NonNull Uri uri, @Nullable String title, @Nullable String subtitle) { return new Intent(context, CodeEditorActivity.class) .setData(uri) .putExtra(Intent.EXTRA_TITLE, title) .putExtra(Intent.EXTRA_SUBJECT, subtitle); } @Override protected void onAuthenticated(@Nullable Bundle savedInstanceState) { setContentView(R.layout.activity_code_editor); setSupportActionBar(findViewById(R.id.toolbar)); LinearProgressIndicator progressIndicator = findViewById(R.id.progress_linear); progressIndicator.setVisibilityAfterHide(View.GONE); String title = getIntent().getStringExtra(Intent.EXTRA_TITLE); if (title == null) { title = getString(R.string.title_code_editor); } String subtitle = getIntent().getStringExtra(Intent.EXTRA_SUBJECT); Uri fileUri = IntentCompat.getDataUri(getIntent()); boolean readOnly = getIntent().getBooleanExtra(EXTRA_READ_ONLY, false); if (subtitle == null) { if (fileUri != null) { subtitle = Paths.trimPathExtension(fileUri.getLastPathSegment()); } else { subtitle = "Untitled.txt"; } } if (fileUri == null) { progressIndicator.hide(); } CodeEditorFragment.Options options = new CodeEditorFragment.Options.Builder() .setUri(fileUri) .setTitle(title) .setSubtitle(subtitle) .setEnableSharing(false) .setJavaSmaliToggle(false) .setReadOnly(readOnly) .build(); CodeEditorFragment fragment = new CodeEditorFragment(); Bundle args = new Bundle(); args.putParcelable(CodeEditorFragment.ARG_OPTIONS, options); fragment.setArguments(args); getSupportFragmentManager() .beginTransaction() .replace(R.id.container, fragment) .commit(); ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setTitle(title); actionBar.setSubtitle(subtitle); } } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == android.R.id.home) { getOnBackPressedDispatcher().onBackPressed(); return true; } return super.onOptionsItemSelected(item); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/editor/CodeEditorFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.editor; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.graphics.Typeface; import android.net.Uri; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.HorizontalScrollView; import android.widget.TextView; import androidx.activity.OnBackPressedCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.MainThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.LinearLayoutCompat; import androidx.appcompat.widget.PopupMenu; import androidx.core.os.BundleCompat; import androidx.core.os.ParcelCompat; import androidx.core.view.MenuProvider; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.ViewModelProvider; import androidx.transition.Transition; import androidx.transition.TransitionManager; import com.google.android.material.button.MaterialButton; import com.google.android.material.color.MaterialColors; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.elevation.SurfaceColors; import com.google.android.material.progressindicator.LinearProgressIndicator; import com.google.android.material.textfield.TextInputEditText; import com.google.android.material.textfield.TextInputLayout; import com.google.android.material.transition.MaterialSharedAxis; import java.util.Objects; import java.util.regex.PatternSyntaxException; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.app.AndroidFragment; import io.github.muntashirakon.AppManager.fm.FmProvider; import io.github.muntashirakon.AppManager.intercept.IntentCompat; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; import io.github.muntashirakon.util.UiUtils; import io.github.rosemoe.sora.event.ContentChangeEvent; import io.github.rosemoe.sora.event.PublishSearchResultEvent; import io.github.rosemoe.sora.event.SelectionChangeEvent; import io.github.rosemoe.sora.lang.EmptyLanguage; import io.github.rosemoe.sora.lang.Language; import io.github.rosemoe.sora.langs.textmate.TextMateColorScheme; import io.github.rosemoe.sora.text.Content; import io.github.rosemoe.sora.text.Cursor; import io.github.rosemoe.sora.text.LineSeparator; import io.github.rosemoe.sora.widget.CodeEditor; import io.github.rosemoe.sora.widget.DirectAccessProps; import io.github.rosemoe.sora.widget.EditorSearcher.SearchOptions; import io.github.rosemoe.sora.widget.SymbolInputView; import io.github.rosemoe.sora.widget.schemes.EditorColorScheme; public class CodeEditorFragment extends AndroidFragment implements MenuProvider { public static final String ARG_OPTIONS = "options"; public static class Options implements Parcelable { @Nullable public final Uri uri; @Nullable public final String title; @Nullable public final String subtitle; public final boolean readOnly; public final boolean javaSmaliToggle; public final boolean enableSharing; private Options(@Nullable Uri uri, @Nullable String title, @Nullable String subtitle, boolean readOnly, boolean javaSmaliToggle, boolean enableSharing) { this.uri = uri; this.title = title; this.subtitle = subtitle; this.readOnly = readOnly; this.javaSmaliToggle = javaSmaliToggle; this.enableSharing = enableSharing; } protected Options(@NonNull Parcel in) { uri = ParcelCompat.readParcelable(in, Uri.class.getClassLoader(), Uri.class); title = in.readString(); subtitle = in.readString(); readOnly = in.readByte() != 0; javaSmaliToggle = in.readByte() != 0; enableSharing = in.readByte() != 0; } public static final Creator CREATOR = new Creator() { @Override public Options createFromParcel(Parcel in) { return new Options(in); } @Override public Options[] newArray(int size) { return new Options[size]; } }; @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeParcelable(uri, flags); dest.writeString(title); dest.writeString(subtitle); dest.writeByte((byte) (readOnly ? 1 : 0)); dest.writeByte((byte) (javaSmaliToggle ? 1 : 0)); dest.writeByte((byte) (enableSharing ? 1 : 0)); } public static class Builder { @Nullable private Uri uri; @Nullable private String title; @Nullable private String subtitle; private boolean readOnly = false; private boolean javaSmaliToggle = false; private boolean enableSharing = true; public Builder() { } public Builder(@NonNull Options options) { uri = options.uri; title = options.title; subtitle = options.subtitle; readOnly = options.readOnly; javaSmaliToggle = options.javaSmaliToggle; enableSharing = options.enableSharing; } public Builder setUri(@Nullable Uri uri) { this.uri = uri; return this; } public Builder setTitle(@Nullable String title) { this.title = title; return this; } public Builder setSubtitle(@Nullable String subtitle) { this.subtitle = subtitle; return this; } public Builder setReadOnly(boolean readOnly) { this.readOnly = readOnly; return this; } public Builder setJavaSmaliToggle(boolean javaSmaliToggle) { this.javaSmaliToggle = javaSmaliToggle; return this; } public Builder setEnableSharing(boolean enableSharing) { this.enableSharing = enableSharing; return this; } public Options build() { return new Options(uri, title, subtitle, readOnly, javaSmaliToggle, enableSharing); } } } private EditorColorScheme mColorScheme; private CodeEditor mEditor; private SymbolInputView mSymbolInputView; private TextView mPositionButton; private MaterialButton mLockButton; private LinearLayoutCompat mSearchWidget; private TextInputEditText mSearchView; private TextInputEditText mReplaceView; private TextInputLayout mReplaceViewContainer; private MaterialButton mReplaceButton; private MaterialButton mReplaceAllButton; private TextView mSearchResultCount; private Options mOptions; private SearchOptions mSearchOptions = new SearchOptions(false, false); private MenuItem mSaveMenu; private MenuItem mUndoMenu; private MenuItem mRedoMenu; private MenuItem mJavaSmaliToggleMenu; private MenuItem mShareMenu; private CodeEditorViewModel mViewModel; private boolean mTextModified = false; private final ActivityResultLauncher mSaveOpenedFile = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { try { if (result.getResultCode() != Activity.RESULT_OK) { return; } Intent data = result.getData(); Uri uri = IntentCompat.getDataUri(data); if (uri == null) return; int takeFlags = data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); saveFile(mEditor.getText(), uri); if (takeFlags != 0) { // Make this URI the current URI mOptions = new Options.Builder(mOptions) .setUri(uri) .setSubtitle(Paths.get(uri).getName()) .build(); mViewModel.setOptions(mOptions); } } finally { showProgressIndicator(false); unlockEditor(); } }); private final OnBackPressedCallback mExitSearchBackPressedCallback = new OnBackPressedCallback(false) { @Override public void handleOnBackPressed() { if (mSearchWidget != null && mSearchWidget.getVisibility() == View.VISIBLE) { hideSearchWidget(); return; } setEnabled(false); requireActivity().getOnBackPressedDispatcher().onBackPressed(); } }; private final OnBackPressedCallback mTextModifiedBackPressedCallback = new OnBackPressedCallback(false) { @Override public void handleOnBackPressed() { if (mTextModified) { new MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.exit_confirmation) .setMessage(R.string.file_modified_are_you_sure) .setPositiveButton(R.string.no, null) .setNegativeButton(R.string.yes, (dialog, which) -> { setEnabled(false); requireActivity().getOnBackPressedDispatcher().onBackPressed(); }) .setNeutralButton(R.string.save_and_exit, (dialog, which) -> { saveFile(); setEnabled(false); requireActivity().getOnBackPressedDispatcher().onBackPressed(); }) .show(); return; } setEnabled(false); requireActivity().getOnBackPressedDispatcher().onBackPressed(); } }; @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_code_editor, container, false); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { mViewModel = new ViewModelProvider(this).get(CodeEditorViewModel.class); mOptions = Objects.requireNonNull(BundleCompat.getParcelable(requireArguments(), ARG_OPTIONS, Options.class)); mViewModel.setOptions(mOptions); mColorScheme = EditorThemes.getColorScheme(requireContext()); mEditor = view.findViewById(R.id.editor); mEditor.setColorScheme(mColorScheme); mEditor.setTypefaceText(Typeface.MONOSPACE); mEditor.setTextSize(14); mEditor.setLineSpacing(2f, 1.1f); mEditor.subscribeEvent(ContentChangeEvent.class, (event, unsubscribe) -> { if (!mTextModified && event.getAction() != ContentChangeEvent.ACTION_SET_NEW_TEXT) { mTextModified = true; mTextModifiedBackPressedCallback.setEnabled(true); getActionBar().ifPresent(actionBar -> actionBar.setSubtitle("* " + mOptions.subtitle)); } mEditor.postDelayed(this::updateLiveButtons, 50); }); mEditor.subscribeEvent(SelectionChangeEvent.class, (event, unsubscribe) -> getFragmentActivity() .ifPresent(activity -> updatePositionText())); mEditor.subscribeEvent(PublishSearchResultEvent.class, (event, unsubscribe) -> getFragmentActivity() .ifPresent(activity -> { updatePositionText(); updateSearchResult(); })); DirectAccessProps props = mEditor.getProps(); props.useICULibToSelectWords = false; props.symbolPairAutoCompletion = false; props.deleteMultiSpaces = -1; props.deleteEmptyLineFast = false; mSymbolInputView = view.findViewById(R.id.symbol_input); mSymbolInputView.addSymbols( new String[]{"⇥", "{", "}", "(", ")", ",", ".", ";", "\"", "?", "+", "-", "*", "/"}, new String[]{"\t", "{", "}", "(", ")", ",", ".", ";", "\"", "?", "+", "-", "*", "/"}); mSymbolInputView.setTextColor(MaterialColors.getColor(mSymbolInputView, com.google.android.material.R.attr.colorOnSurface)); mSymbolInputView.setBackground(null); ((HorizontalScrollView) mSymbolInputView.getParent()).setBackgroundColor(SurfaceColors.SURFACE_2.getColor(requireContext())); mSymbolInputView.bindEditor(mEditor); if (mOptions.readOnly) { mSymbolInputView.setVisibility(View.GONE); } // Setup search widget mSearchWidget = view.findViewById(R.id.search_container); mSearchView = view.findViewById(R.id.search_bar); mSearchView.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } @Override public void afterTextChanged(Editable s) { if (TextUtils.isEmpty(s)) { mEditor.getSearcher().stopSearch(); } else { try { mEditor.getSearcher().search(s.toString(), mSearchOptions); } catch (PatternSyntaxException ignore) { } } } }); TextInputLayout searchViewContainer = view.findViewById(R.id.search_bar_container); searchViewContainer.setEndIconOnClickListener(v -> { PopupMenu popupMenu = new PopupMenu(v.getContext(), v); Menu menu = popupMenu.getMenu(); menu.add(R.string.search_option_match_case) .setCheckable(true) .setChecked(!mSearchOptions.ignoreCase) .setOnMenuItemClickListener(item -> { boolean ignoreCase = item.isChecked(); item.setChecked(ignoreCase); mSearchOptions = new SearchOptions(mSearchOptions.type, ignoreCase); search(mSearchView.getText()); return true; }); menu.add(R.string.search_option_regex) .setCheckable(true) .setChecked(mSearchOptions.type == SearchOptions.TYPE_REGULAR_EXPRESSION) .setOnMenuItemClickListener(item -> { boolean regex = !item.isChecked(); item.setChecked(regex); int type = regex ? SearchOptions.TYPE_REGULAR_EXPRESSION : SearchOptions.TYPE_NORMAL; mSearchOptions = new SearchOptions(type, mSearchOptions.ignoreCase); search(mSearchView.getText()); return true; }); menu.add(R.string.search_option_whole_word) .setCheckable(true) .setChecked(mSearchOptions.type == SearchOptions.TYPE_WHOLE_WORD) .setOnMenuItemClickListener(item -> { boolean wholeWord = !item.isChecked(); item.setChecked(wholeWord); int type = wholeWord ? SearchOptions.TYPE_WHOLE_WORD : SearchOptions.TYPE_NORMAL; mSearchOptions = new SearchOptions(type, mSearchOptions.ignoreCase); search(mSearchView.getText()); return true; }); popupMenu.show(); }); mSearchResultCount = view.findViewById(R.id.search_result_count); view.findViewById(R.id.previous_button).setOnClickListener(v -> { if (!mEditor.getSearcher().hasQuery()) { return; } mEditor.getSearcher().gotoPrevious(); }); view.findViewById(R.id.next_button).setOnClickListener(v -> { if (!mEditor.getSearcher().hasQuery()) { return; } mEditor.getSearcher().gotoNext(); }); mReplaceView = view.findViewById(R.id.replace_bar); mReplaceViewContainer = view.findViewById(R.id.replace_bar_container); mReplaceButton = view.findViewById(R.id.replace_button); mReplaceAllButton = view.findViewById(R.id.replace_all_button); mReplaceButton.setOnClickListener(v -> { if (!mEditor.getSearcher().hasQuery()) { return; } CharSequence query = mReplaceView.getText(); if (!TextUtils.isEmpty(query)) { mEditor.getSearcher().replaceThis(query.toString()); } }); mReplaceAllButton.setOnClickListener(v -> { if (!mEditor.getSearcher().hasQuery()) { return; } CharSequence query = mReplaceView.getText(); if (!TextUtils.isEmpty(query)) { mEditor.getSearcher().replaceAll(query.toString()); } }); // Setup status bar mLockButton = view.findViewById(R.id.lock); mLockButton.setOnClickListener(v -> { // Toggle lock if (mEditor.isEditable()) { lockEditor(); } else { unlockEditor(); } }); TextView languageButton = view.findViewById(R.id.language); languageButton.setOnClickListener(v -> { // TODO: 13/9/22 Display all the supported languages }); // TODO: 13/9/22 Enable setting custom tab size if possible (e.g. Makefile requires tab) TextView indentSizeButton = view.findViewById(R.id.tab_size); TextView lineSeparatorButton = view.findViewById(R.id.line_separator); lineSeparatorButton.setOnClickListener(v -> { PopupMenu popupMenu = new PopupMenu(requireContext(), v); Menu menu = popupMenu.getMenu(); menu.add(R.string.line_separator).setEnabled(false); if (!mEditor.getLineSeparator().equals(LineSeparator.CRLF)) { menu.add("CRLF - Windows (\\r\\n)").setOnMenuItemClickListener(menuItem -> { mEditor.setLineSeparator(LineSeparator.CRLF); // TODO: 18/9/22 Update line separator for existing texts lineSeparatorButton.setText(mEditor.getLineSeparator().name()); return true; }); } if (!mEditor.getLineSeparator().equals(LineSeparator.CR)) { menu.add("CR - Classic Mac OS (\\r)").setOnMenuItemClickListener(menuItem -> { mEditor.setLineSeparator(LineSeparator.CR); lineSeparatorButton.setText(mEditor.getLineSeparator().name()); return true; }); } if (!mEditor.getLineSeparator().equals(LineSeparator.LF)) { menu.add("LF - Unix & Mac OS (\\n)").setOnMenuItemClickListener(menuItem -> { mEditor.setLineSeparator(LineSeparator.LF); lineSeparatorButton.setText(mEditor.getLineSeparator().name()); return true; }); } popupMenu.show(); }); mPositionButton = view.findViewById(R.id.position); mPositionButton.setOnClickListener(v -> { // TODO: 13/9/22 Enable going to custom places }); requireActivity().addMenuProvider(this, getViewLifecycleOwner(), Lifecycle.State.RESUMED); // Update live buttons at the start updateLiveButtons(); updateStartupMenu(); UiUtils.applyWindowInsetsAsPaddingNoTop(view.findViewById(R.id.editor_container)); mViewModel.getContentLiveData().observe(getViewLifecycleOwner(), content -> { showProgressIndicator(false); if (content == null) { UIUtils.displayLongToast(R.string.failed); return; } mEditor.setEditorLanguage(getLanguage(mViewModel.getLanguage())); if (mViewModel.isReadOnly()) { mLockButton.setIconResource(R.drawable.ic_lock); mLockButton.setEnabled(false); mEditor.setEditable(false); } else { mLockButton.setEnabled(true); } languageButton.setText(mViewModel.getLanguage()); languageButton.setEnabled(!mViewModel.isReadOnly()); indentSizeButton.setEnabled(!mViewModel.isReadOnly()); // TODO: 13/9/22 Use localization CharSequence tabSize = mEditor.getTabWidth() + " " + (mEditor.getEditorLanguage().useTab() ? "tabs" : "spaces"); indentSizeButton.setText(tabSize); lineSeparatorButton.setEnabled(!mViewModel.isReadOnly()); mEditor.setText(content); lineSeparatorButton.setText(mEditor.getLineSeparator().name()); updatePositionText(); }); mViewModel.getSaveFileLiveData().observe(getViewLifecycleOwner(), successful -> { if (successful) { UIUtils.displayShortToast(R.string.saved_successfully); mTextModified = false; mTextModifiedBackPressedCallback.setEnabled(false); getActionBar().ifPresent(actionBar -> actionBar.setSubtitle(mOptions.subtitle)); } else { UIUtils.displayLongToast(R.string.saving_failed); } }); mViewModel.getJavaFileLiveData().observe(getViewLifecycleOwner(), uri -> { CodeEditorFragment.Options options = new CodeEditorFragment.Options.Builder() .setUri(uri) .setTitle(mOptions.title) .setSubtitle(mOptions.subtitle) .setEnableSharing(true) .setJavaSmaliToggle(false) .setReadOnly(true) .build(); CodeEditorFragment fragment = new CodeEditorFragment(); Bundle args = new Bundle(); args.putParcelable(CodeEditorFragment.ARG_OPTIONS, options); fragment.setArguments(args); getFragmentActivity().ifPresent(activity -> activity .getSupportFragmentManager() .beginTransaction() .replace(((ViewGroup) requireView().getParent()).getId(), fragment) .addToBackStack(null) .commit()); }); mViewModel.loadFileContentIfAvailable(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); // Handle back press: The order MUST be kept same requireActivity().getOnBackPressedDispatcher().addCallback(this, mTextModifiedBackPressedCallback); requireActivity().getOnBackPressedDispatcher().addCallback(this, mExitSearchBackPressedCallback); } @Override public void onResume() { super.onResume(); getActionBar().ifPresent(actionBar -> { actionBar.setTitle(mOptions.title); actionBar.setSubtitle((mTextModified ? "* " : "") + mOptions.subtitle); }); } @Override public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { inflater.inflate(R.menu.activity_code_editor_actions, menu); mSaveMenu = menu.findItem(R.id.action_save); mUndoMenu = menu.findItem(R.id.action_undo); mRedoMenu = menu.findItem(R.id.action_redo); mJavaSmaliToggleMenu = menu.findItem(R.id.action_java_smali_toggle); mShareMenu = menu.findItem(R.id.action_share); updateStartupMenu(); } @Override public boolean onMenuItemSelected(@NonNull MenuItem item) { int id = item.getItemId(); if (id == R.id.action_undo) { if (mEditor != null && mEditor.canUndo()) { mEditor.undo(); return true; } } else if (id == R.id.action_redo) { if (mEditor != null && mEditor.canRedo()) { mEditor.redo(); return true; } } else if (id == R.id.action_wrap) { if (mEditor != null) { mEditor.setWordwrap(!mEditor.isWordwrap()); return true; } } else if (id == R.id.action_save) { saveFile(); return true; } else if (id == R.id.action_save_as) { launchIntentSaver(); return true; } else if (id == R.id.action_share) { Path filePath = mViewModel.getSourceFile(); if (filePath != null) { Intent intent = new Intent(Intent.ACTION_SEND) .setType(filePath.getType()) .putExtra(Intent.EXTRA_STREAM, FmProvider.getContentUri(filePath)) .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(Intent.createChooser(intent, getString(R.string.share))); } return true; } else if (id == R.id.action_java_smali_toggle) { mViewModel.generateJava(mEditor.getText()); return true; } else if (id == R.id.action_search) { if (mSearchWidget != null) { // FIXME: 21/4/23 Ideally, search widget should have cross button to close it. if (mSearchWidget.getVisibility() == View.VISIBLE) { hideSearchWidget(); } else showSearchWidget(); return true; } } return false; } private void showProgressIndicator(boolean show) { LinearProgressIndicator progressIndicator = requireActivity().findViewById(R.id.progress_linear); if (progressIndicator != null) { if (show) { progressIndicator.show(); } else { progressIndicator.hide(); } } } private void updateLiveButtons() { boolean readOnly = mViewModel.isReadOnly(); if (mSaveMenu != null) { mSaveMenu.setEnabled(mTextModified && !readOnly); } if (mUndoMenu != null) { mUndoMenu.setEnabled(mEditor != null && mEditor.canUndo() && !readOnly); } if (mRedoMenu != null) { mRedoMenu.setEnabled(mEditor != null && mEditor.canRedo() && !readOnly); } if (mReplaceViewContainer != null) { mReplaceViewContainer.setVisibility(readOnly ? View.GONE : View.VISIBLE); } if (mReplaceButton != null) { mReplaceButton.setVisibility(readOnly ? View.GONE : View.VISIBLE); } if (mReplaceAllButton != null) { mReplaceAllButton.setVisibility(readOnly ? View.GONE : View.VISIBLE); } } private void updateStartupMenu() { if (mViewModel == null) return; if (mJavaSmaliToggleMenu != null) { mJavaSmaliToggleMenu.setVisible(mViewModel.canGenerateJava()); mJavaSmaliToggleMenu.setEnabled(mViewModel.canGenerateJava()); } if (mShareMenu != null) { mShareMenu.setEnabled(mViewModel.isBackedByAFile()); } } @MainThread private void updatePositionText() { Cursor cursor = mEditor.getCursor(); StringBuilder text = new StringBuilder() .append(1 + cursor.getLeftLine()) .append(":") .append(cursor.getLeftColumn()); if (cursor.isSelected()) { text.append(" (") .append(cursor.getRight() - cursor.getLeft()) .append(" chars)"); } mPositionButton.setText(text); } @MainThread private void updateSearchResult() { int count = mEditor.getSearcher().hasQuery() ? mEditor.getSearcher().getMatchedPositionCount() : 0; mSearchResultCount.setText(getResources().getQuantityString(R.plurals.search_results, count, count)); } private void saveFile() { if (!mViewModel.isBackedByAFile()) { launchIntentSaver(); } else if (mViewModel.canWrite()) { saveFile(mEditor.getText(), null); } else { new MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.read_only_file) .setMessage(R.string.read_only_file_warning) .setPositiveButton(R.string.yes, (dialog, which) -> launchIntentSaver()) .setNegativeButton(R.string.no, null) .show(); } } private void saveFile(Content content, @Nullable Uri uri) { if (mViewModel == null) return; mViewModel.saveFile(content, uri == null ? null : Paths.get(uri)); } @NonNull public Language getLanguage(@Nullable String language) { if (language == null || !(mColorScheme instanceof TextMateColorScheme)) { return new EmptyLanguage(); } return Languages.getLanguage(requireContext(), language, ((TextMateColorScheme) mColorScheme).getThemeSource()); } public void showSearchWidget() { if (mSearchWidget != null) { mExitSearchBackPressedCallback.setEnabled(true); Transition sharedAxis = new MaterialSharedAxis(MaterialSharedAxis.Y, true); TransitionManager.beginDelayedTransition(mSearchWidget, sharedAxis); mSearchWidget.setVisibility(View.VISIBLE); mSearchView.requestFocus(); } } public void hideSearchWidget() { if (mSearchWidget != null) { Transition sharedAxis = new MaterialSharedAxis(MaterialSharedAxis.Y, false); TransitionManager.beginDelayedTransition(mSearchWidget, sharedAxis); mSearchWidget.setVisibility(View.GONE); mEditor.getSearcher().stopSearch(); mExitSearchBackPressedCallback.setEnabled(false); } } private void search(@Nullable CharSequence s) { if (TextUtils.isEmpty(s)) { mEditor.getSearcher().stopSearch(); } else { try { mEditor.getSearcher().search(s.toString(), mSearchOptions); } catch (PatternSyntaxException ignore) { } } } private void lockEditor() { if (mViewModel.isReadOnly()) { return; } if (mEditor.isEditable()) { mEditor.setEditable(false); mSymbolInputView.setVisibility(View.GONE); mLockButton.setIconResource(R.drawable.ic_lock); } } private void unlockEditor() { if (mViewModel.isReadOnly()) { return; } if (!mEditor.isEditable()) { mEditor.setEditable(true); mSymbolInputView.setVisibility(View.VISIBLE); mLockButton.setIconResource(R.drawable.ic_unlock); } } private void launchIntentSaver() { showProgressIndicator(true); lockEditor(); mSaveOpenedFile.launch(getSaveIntent()); } private Intent getSaveIntent() { return new Intent(Intent.ACTION_CREATE_DOCUMENT) .setType("*/*") .putExtra(Intent.EXTRA_TITLE, mViewModel.getFilename()); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/editor/CodeEditorViewModel.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.editor; import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; import static org.xmlpull.v1.XmlPullParser.END_TAG; import static org.xmlpull.v1.XmlPullParser.IGNORABLE_WHITESPACE; import static org.xmlpull.v1.XmlPullParser.START_TAG; import static org.xmlpull.v1.XmlPullParser.TEXT; import android.app.Application; import android.net.Uri; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import org.jetbrains.annotations.Contract; import org.xmlpull.v1.XmlPullParserException; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintStream; import java.io.Reader; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Future; import io.github.muntashirakon.AppManager.apk.parser.AndroidBinXmlDecoder; import io.github.muntashirakon.AppManager.apk.parser.AndroidBinXmlEncoder; import io.github.muntashirakon.AppManager.dex.DexUtils; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.self.filecache.FileCache; import io.github.muntashirakon.AppManager.utils.ExUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.compat.xml.TypedXmlPullParser; import io.github.muntashirakon.compat.xml.TypedXmlSerializer; import io.github.muntashirakon.compat.xml.Xml; import io.github.muntashirakon.io.CharSequenceInputStream; import io.github.muntashirakon.io.IoUtils; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; import io.github.muntashirakon.lifecycle.SingleLiveEvent; import io.github.rosemoe.sora.text.Content; import io.github.rosemoe.sora.text.ContentIO; public class CodeEditorViewModel extends AndroidViewModel { public static final String TAG = CodeEditorViewModel.class.getSimpleName(); // TODO: 12/9/22 Another option is to store them as assets/resources private static final Map EXT_TO_LANGUAGE_MAP = new HashMap() {{ // We skip the default ones put("cmd", "sh"); put("htm", "xml"); put("html", "xml"); put("kt", "kotlin"); put("prop", "properties"); put("tokens", "properties"); put("xhtml", "xml"); }}; @IntDef({XML_TYPE_NONE, XML_TYPE_AXML, XML_TYPE_ABX}) @Retention(RetentionPolicy.SOURCE) private @interface XmlType { } public static final int XML_TYPE_NONE = 0; public static final int XML_TYPE_AXML = 1; public static final int XML_TYPE_ABX = 2; @Nullable private String mLanguage; private boolean mCanGenerateJava; @XmlType private int mXmlType = XML_TYPE_NONE; @Nullable private Path mSourceFile; private CodeEditorFragment.Options mOptions; @Nullable private Future mContentLoaderResult; @Nullable private Future mJavaConverterResult; private final FileCache mFileCache = new FileCache(); private final MutableLiveData mContentLiveData = new MutableLiveData<>(); // Only for smali private final SingleLiveEvent mJavaFileLiveData = new SingleLiveEvent<>(); private final MutableLiveData mSaveFileLiveData = new MutableLiveData<>(); public CodeEditorViewModel(@NonNull Application application) { super(application); } @Override protected void onCleared() { if (mContentLoaderResult != null) { mContentLoaderResult.cancel(true); } if (mJavaConverterResult != null) { mJavaConverterResult.cancel(true); } IoUtils.closeQuietly(mFileCache); super.onCleared(); } public LiveData getContentLiveData() { return mContentLiveData; } public LiveData getJavaFileLiveData() { return mJavaFileLiveData; } public LiveData getSaveFileLiveData() { return mSaveFileLiveData; } public void setOptions(@NonNull CodeEditorFragment.Options options) { mOptions = options; mSourceFile = options.uri != null ? Paths.get(options.uri) : null; String extension = mSourceFile != null ? mSourceFile.getExtension() : null; mLanguage = getLanguageFromExt(extension); mCanGenerateJava = options.javaSmaliToggle || "smali".equals(mLanguage); } @Nullable public Path getSourceFile() { return mSourceFile; } public void loadFileContentIfAvailable() { if (mSourceFile == null) return; if (mContentLoaderResult != null) { mContentLoaderResult.cancel(true); } mContentLoaderResult = ThreadUtils.postOnBackgroundThread(() -> { Content content = null; if ("xml".equals(mLanguage)) { byte[] bytes = mSourceFile.getContentAsBinary(); ByteBuffer buffer = ByteBuffer.wrap(bytes); try { if (AndroidBinXmlDecoder.isBinaryXml(buffer)) { content = new Content(AndroidBinXmlDecoder.decode(bytes)); mXmlType = XML_TYPE_AXML; } else if (Xml.isBinaryXml(buffer)) { // FIXME: 19/5/23 Unfortunately, converting ABX to XML is lossy. Find a way to fix this. // Until then, the feature is disabled. // content = getXmlFromAbx(bytes); // xmlType = XML_TYPE_ABX; } } catch (IOException e) { Log.e(TAG, "Unable to convert XML bytes to plain text.", e); } } if (content == null) { try (InputStream is = mSourceFile.openInputStream()) { content = ContentIO.createFrom(is); mXmlType = XML_TYPE_NONE; }catch (IOException e) { Log.e(TAG, "Could not read file %s", e, mSourceFile); } } mContentLiveData.postValue(content); }); } public void saveFile(@NonNull Content content, @Nullable Path alternativeFile) { ThreadUtils.postOnBackgroundThread(() -> { if (mSourceFile == null && alternativeFile == null) { mSaveFileLiveData.postValue(false); return; } // Important: Alternative file gets the top priority Path savingPath = alternativeFile != null ? alternativeFile : mSourceFile; try (OutputStream os = savingPath.openOutputStream()) { switch (mXmlType) { case XML_TYPE_AXML: { // TODO: Use serializer from the latest update byte[] realContent = AndroidBinXmlEncoder.encodeString(content.toString()); os.write(realContent); break; } case XML_TYPE_ABX: { try (InputStream is = new CharSequenceInputStream(content, StandardCharsets.UTF_8)) { copyAbxFromXml(is, os); } break; } default: case XML_TYPE_NONE: ContentIO.writeTo(content, os, false); } mSaveFileLiveData.postValue(true); } catch (IOException e) { Log.e(TAG, "Could not write to file %s", e, savingPath); mSaveFileLiveData.postValue(false); } }); } public boolean isReadOnly() { return mOptions == null || mOptions.readOnly; } public boolean canWrite() { return !isReadOnly() && mSourceFile != null && mSourceFile.canWrite(); } public boolean isBackedByAFile() { return mSourceFile != null; } @NonNull public String getFilename() { if (mSourceFile == null) { return "untitled.txt"; } return mSourceFile.getName(); } public boolean canGenerateJava() { return mCanGenerateJava; } @Nullable public String getLanguage() { return mLanguage; } public void generateJava(Content smaliContent) { if (!mCanGenerateJava) { return; } if (mJavaConverterResult != null) { mJavaConverterResult.cancel(true); } mJavaConverterResult = ThreadUtils.postOnBackgroundThread(() -> { List smaliContents; if (mSourceFile != null) { Path parent = mSourceFile.getParent(); String baseName = DexUtils.getClassNameWithoutInnerClasses(Paths.trimPathExtension(mSourceFile.getName())); String baseSmali = baseName + ".smali"; String baseStartWith = baseName + "$"; Path[] paths = parent != null ? parent.listFiles((dir, name) -> name.equals(baseSmali) || name.startsWith(baseStartWith)) : new Path[0]; smaliContents = new ArrayList<>(paths.length + 1); smaliContents.add(smaliContent.toString()); for (Path path : paths) { if (path.equals(mSourceFile)) { // We already have this file continue; } String content = path.getContentAsString(null); if (content != null) { smaliContents.add(content); } else { mJavaFileLiveData.postValue(null); return; } } } else { smaliContents = Collections.singletonList(smaliContent.toString()); } if (ThreadUtils.isInterrupted()) { return; } try { File cachedFile = mFileCache.createCachedFile("java"); try (PrintStream ps = new PrintStream(cachedFile)) { ps.print(DexUtils.toJavaCode(smaliContents, -1)); } mJavaFileLiveData.postValue(Uri.fromFile(cachedFile)); } catch (Throwable e) { e.printStackTrace(); mJavaFileLiveData.postValue(null); } }); } @Contract("!null -> !null") @Nullable private static String getLanguageFromExt(@Nullable String ext) { String lang = EXT_TO_LANGUAGE_MAP.get(ext); if (lang != null) return lang; return ext; } private static String getXmlFromAbx(@NonNull byte[] data) throws IOException { try (InputStream is = new BufferedInputStream(new ByteArrayInputStream(data)); ByteArrayOutputStream os = new ByteArrayOutputStream()) { TypedXmlPullParser parser = Xml.newBinaryPullParser(); parser.setInput(is, StandardCharsets.UTF_8.name()); TypedXmlSerializer serializer = Xml.newFastSerializer(); serializer.setOutput(os, StandardCharsets.UTF_8.name()); copyXml(parser, serializer); return os.toString(); } catch (XmlPullParserException e) { return ExUtils.rethrowAsIOException(e); } } private static void copyAbxFromXml(@NonNull InputStream in, @NonNull OutputStream out) throws IOException { try (Reader is = new InputStreamReader(in)) { TypedXmlPullParser parser = Xml.newFastPullParser(); parser.setInput(is); TypedXmlSerializer serializer = Xml.newBinarySerializer(); serializer.setOutput(out, StandardCharsets.UTF_8.name()); copyXml(parser, serializer); } catch (XmlPullParserException e) { ExUtils.rethrowAsIOException(e); } } public static void copyXml(@NonNull TypedXmlPullParser parser, @NonNull TypedXmlSerializer serializer) throws IOException, XmlPullParserException { serializer.startDocument(null, null); int event; do { event = parser.nextToken(); switch (event) { case START_TAG: serializer.startTag(null, parser.getName()); for (int i = 0; i < parser.getAttributeCount(); i++) { String attributeName = parser.getAttributeName(i); serializer.attribute(null, attributeName, parser.getAttributeValue(i)); } break; case END_TAG: serializer.endTag(null, parser.getName()); break; case TEXT: serializer.text(parser.getText()); break; case IGNORABLE_WHITESPACE: serializer.ignorableWhitespace(parser.getText()); break; case END_DOCUMENT: serializer.endDocument(); break; default: throw new UnsupportedOperationException(); } } while (event != END_DOCUMENT); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/editor/CodeEditorWidget.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.editor; import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.inputmethod.BaseInputConnection; import android.widget.Toast; import java.lang.reflect.Field; import io.github.muntashirakon.AppManager.utils.ClipboardUtils; import io.github.muntashirakon.AppManager.utils.Utils; import io.github.rosemoe.sora.text.Cursor; import io.github.rosemoe.sora.text.TextRange; import io.github.rosemoe.sora.widget.CodeEditor; import io.github.rosemoe.sora.widget.DirectAccessProps; public class CodeEditorWidget extends CodeEditor { public static final String TAG = CodeEditorWidget.class.getSimpleName(); public CodeEditorWidget(Context context) { super(context); } public CodeEditorWidget(Context context, AttributeSet attrs) { super(context, attrs); } public CodeEditorWidget(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public CodeEditorWidget(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } public void pasteText() { try { CharSequence data = ClipboardUtils.readClipboard(getContext()); BaseInputConnection inputConnection = getInputConnection(); TextRange lastInsertion = getLastInsertion(); if (data != null && inputConnection != null) { String text = data.toString(); inputConnection.commitText(text, 1); if (getProps().formatPastedText) { formatCodeAsync(lastInsertion.getStart(), lastInsertion.getEnd()); } notifyIMEExternalCursorChange(); } } catch (Exception e) { Log.w(TAG, e); Toast.makeText(getContext(), e.toString(), Toast.LENGTH_SHORT).show(); } } public void copyText() { copyText(true); } public void copyText(boolean shouldCopyLine) { Cursor cursor = getCursor(); if (cursor.isSelected()) { String clip = getText().substring(cursor.getLeft(), cursor.getRight()); Utils.copyToClipboard(getContext(), "text", clip); } else if (shouldCopyLine) { copyLine(); } } private void copyLine() { final Cursor cursor = getCursor(); if (cursor.isSelected()) { copyText(); return; } final int line = cursor.left().line; setSelectionRegion(line, 0, line, getText().getColumnCount(line)); copyText(false); } public BaseInputConnection getInputConnection() { try { // Get the Class object of the superclass Class superClass = this.getClass().getSuperclass(); // Get the private field from the superclass Field field = superClass.getDeclaredField("inputConnection"); // Make the field accessible field.setAccessible(true); // Read the value of the private field for this object return (BaseInputConnection) field.get(this); } catch (Exception e) { e.printStackTrace(); return null; } } public TextRange getLastInsertion() { try { // Get the Class object of the superclass Class superClass = this.getClass().getSuperclass(); // Get the private field from the superclass Field field = superClass.getDeclaredField("lastInsertion"); // Make the field accessible field.setAccessible(true); // Read the value of the private field for this object return (TextRange) field.get(this); } catch (Exception e) { e.printStackTrace(); return null; } } public DirectAccessProps getProps() { try { // Get the Class object of the superclass Class superClass = this.getClass().getSuperclass(); // Get the private field from the superclass Field field = superClass.getDeclaredField("props"); // Make the field accessible field.setAccessible(true); // Read the value of the private field for this object return (DirectAccessProps) field.get(this); } catch (Exception e) { e.printStackTrace(); return null; } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/editor/EditorThemes.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.editor; import android.content.Context; import android.graphics.Color; import androidx.annotation.NonNull; import androidx.core.graphics.ColorUtils; import com.google.android.material.color.MaterialColors; import com.google.android.material.elevation.SurfaceColors; import org.eclipse.tm4e.core.registry.IThemeSource; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.util.UiUtils; import io.github.rosemoe.sora.lang.styling.color.EditorColor; import io.github.rosemoe.sora.langs.textmate.TextMateColorScheme; import io.github.rosemoe.sora.langs.textmate.registry.ThemeRegistry; import io.github.rosemoe.sora.langs.textmate.registry.model.ThemeModel; import io.github.rosemoe.sora.widget.CodeEditor; import io.github.rosemoe.sora.widget.schemes.EditorColorScheme; public final class EditorThemes { public static final String TAG = EditorThemes.class.getSimpleName(); @NonNull public static EditorColorScheme getColorScheme(@NonNull Context context) { return UiUtils.isDarkMode(context) ? getDarkScheme(context) : getLightScheme(context); } @NonNull private static EditorColorScheme getLightScheme(@NonNull Context context) { EditorColorScheme scheme; try { scheme = new TextMateColorSchemeFixed(IThemeSource.fromInputStream( context.getAssets().open("editor_themes/light.tmTheme"), "light.tmTheme", null) ); } catch (Exception e) { Log.e(TAG, "Could not create light scheme for TM language", e); scheme = new LightScheme(); fixColor(context, scheme); } return scheme; } @NonNull private static EditorColorScheme getDarkScheme(@NonNull Context context) { EditorColorScheme scheme; try { scheme = new TextMateColorSchemeFixed( IThemeSource.fromInputStream(context.getAssets().open("editor_themes/dark.tmTheme.json"), "dark.tmTheme.json", null) ); } catch (Exception e) { Log.e(TAG, "Could not create dark scheme for TM language", e); scheme = new DarkScheme(); fixColor(context, scheme); } return scheme; } private static void fixColor(@NonNull Context context, @NonNull EditorColorScheme scheme) { scheme.setColor(EditorColorScheme.WHOLE_BACKGROUND, SurfaceColors.SURFACE_0.getColor(context)); scheme.setColor(EditorColorScheme.LINE_NUMBER_BACKGROUND, SurfaceColors.SURFACE_2.getColor(context)); scheme.setColor(EditorColorScheme.COMPLETION_WND_BACKGROUND, SurfaceColors.SURFACE_1.getColor(context)); scheme.setColor(EditorColorScheme.HIGHLIGHTED_DELIMITERS_FOREGROUND, Color.RED); int thumbColor = MaterialColors.getColor(context, androidx.appcompat.R.attr.colorControlActivated, EditorColor.class.getSimpleName()); scheme.setColor(EditorColorScheme.SCROLL_BAR_THUMB, thumbColor); scheme.setColor(EditorColorScheme.SCROLL_BAR_THUMB_PRESSED, thumbColor); int trackColor = ColorUtils.setAlphaComponent(MaterialColors.getColor(context, androidx.appcompat.R.attr.colorControlNormal, EditorColor.class.getSimpleName()), 0x39); scheme.setColor(EditorColorScheme.SCROLL_BAR_TRACK, trackColor); } private static class TextMateColorSchemeFixed extends TextMateColorScheme { public TextMateColorSchemeFixed(IThemeSource themeSource) throws Exception { super(ThemeRegistry.getInstance(), new ThemeModel(themeSource)); } @Override public void attachEditor(CodeEditor editor) { super.attachEditor(editor); fixColor(editor.getContext(), this); } } // Copyright 2022 Raival private static class LightScheme extends EditorColorScheme { @Override public void applyDefault() { super.applyDefault(); setColor(ANNOTATION, -0x9b9b9c); setColor(FUNCTION_NAME, -0x1000000); setColor(IDENTIFIER_NAME, -0x1000000); setColor(IDENTIFIER_VAR, -0x479cc2); setColor(LITERAL, -0xd5ff01); setColor(OPERATOR, -0xc60000); setColor(COMMENT, -0xc080a1); setColor(KEYWORD, -0x80ff8c); setColor(WHOLE_BACKGROUND, -0x1); setColor(TEXT_NORMAL, -0x1000000); setColor(LINE_NUMBER_BACKGROUND, -0x1); setColor(LINE_NUMBER, -0x878788); setColor(SELECTED_TEXT_BACKGROUND, -0xcc6601); setColor(MATCHED_TEXT_BACKGROUND, -0x2b2b2c); setColor(CURRENT_LINE, -0x170d02); setColor(SELECTION_INSERT, -0xfc1415); setColor(SELECTION_HANDLE, -0xfc1415); setColor(BLOCK_LINE, -0x272728); setColor(BLOCK_LINE_CURRENT, 0); setColor(TEXT_SELECTED, -0x1); } } // Copyright 2022 Raival private static class DarkScheme extends EditorColorScheme { @Override public void applyDefault() { super.applyDefault(); setColor(ANNOTATION, -0x444ad7); setColor(FUNCTION_NAME, -0x332f27); setColor(IDENTIFIER_NAME, -0x332f27); setColor(IDENTIFIER_VAR, -0x678956); setColor(LITERAL, -0x9578a7); setColor(OPERATOR, -0x332f27); setColor(COMMENT, -0x7f7f80); setColor(KEYWORD, -0x3387ce); setColor(WHOLE_BACKGROUND, -0xd4d4d5); setColor(TEXT_NORMAL, -0x332f27); setColor(LINE_NUMBER_BACKGROUND, -0xcecccb); setColor(LINE_NUMBER, -0x9f9c9a); setColor(LINE_DIVIDER, -0x9f9c9a); setColor(SCROLL_BAR_THUMB, -0x59595a); setColor(SCROLL_BAR_THUMB_PRESSED, -0xa9a9aa); setColor(SELECTED_TEXT_BACKGROUND, -0xc98948); setColor(MATCHED_TEXT_BACKGROUND, -0xcda6c3); setColor(CURRENT_LINE, -0xcdcdce); setColor(SELECTION_INSERT, -0x332f27); setColor(SELECTION_HANDLE, -0x332f27); setColor(BLOCK_LINE, -0xa8a8a9); setColor(BLOCK_LINE_CURRENT, -0x22a8a8a9); setColor(NON_PRINTABLE_CHAR, -0x222223); setColor(TEXT_SELECTED, -0x332f27); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/editor/Languages.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.editor; import android.content.Context; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.eclipse.tm4e.core.registry.IGrammarSource; import org.eclipse.tm4e.core.registry.IThemeSource; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; import java.nio.charset.StandardCharsets; import io.github.muntashirakon.AppManager.logs.Log; import io.github.rosemoe.sora.lang.EmptyLanguage; import io.github.rosemoe.sora.lang.Language; import io.github.rosemoe.sora.langs.textmate.TextMateLanguage; public final class Languages { @NonNull public static Language getLanguage(@NonNull Context context, @NonNull String language, @Nullable IThemeSource themeSource) { try { IGrammarSource grammarSource = IGrammarSource.fromInputStream(context.getAssets().open("languages/" + language + "/tmLanguage.json"), "tmLanguage.json", StandardCharsets.UTF_8); Reader languageConfiguration = new InputStreamReader(context.getAssets().open("languages/" + language + "/language-configuration.json")); if (themeSource == null) { throw new FileNotFoundException("Invalid theme source"); } return TextMateLanguage.create(grammarSource, languageConfiguration, themeSource); } catch (IOException e) { Log.w("CodeEditor", "Could not load resources for language %s", e, language); return new EmptyLanguage(); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/filters/AbsExpressionEvaluator.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.filters; import androidx.annotation.NonNull; import androidx.annotation.Nullable; public abstract class AbsExpressionEvaluator { protected CharSequence lastError; @Nullable public CharSequence getLastError() { return lastError; } protected abstract boolean evalId(@NonNull String id); public boolean evaluate(@NonNull String expr) { lastError = null; // Process parentheses first while (expr.contains("(")) { int start = expr.lastIndexOf('('); int end = expr.indexOf(')', start); if (end == -1) { lastError = "Expected ')'."; return false; } // Get expression without parenthesis String subExpr = expr.substring(start + 1, end); boolean subResult = evalOrExpr(subExpr); expr = expr.substring(0, start) + subResult + expr.substring(end + 1); } // Evaluate the final expression without parentheses return evalOrExpr(expr); } private boolean evalOrExpr(@NonNull String expr) { String[] orParts = expr.split(" \\| "); for (String part : orParts) { if (evalAndExpr(part)) { // No need to evaluate any further return true; } } // None of the parts returned true return false; } private boolean evalAndExpr(@NonNull String expr) { String[] andParts = expr.split(" & "); for (String andPart : andParts) { andPart = andPart.trim(); if (andPart.equals("true")) { continue; } if (andPart.equals("false") || !evalId(andPart)) { // No need to evaluate any further return false; } } return true; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/filters/EditFilterOptionFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.filters; import static io.github.muntashirakon.AppManager.filters.options.FilterOption.TYPE_DURATION_MILLIS; import static io.github.muntashirakon.AppManager.filters.options.FilterOption.TYPE_INT; import static io.github.muntashirakon.AppManager.filters.options.FilterOption.TYPE_INT_FLAGS; import static io.github.muntashirakon.AppManager.filters.options.FilterOption.TYPE_LONG; import static io.github.muntashirakon.AppManager.filters.options.FilterOption.TYPE_NONE; import static io.github.muntashirakon.AppManager.filters.options.FilterOption.TYPE_REGEX; import static io.github.muntashirakon.AppManager.filters.options.FilterOption.TYPE_SIZE_BYTES; import static io.github.muntashirakon.AppManager.filters.options.FilterOption.TYPE_STR_MULTIPLE; import static io.github.muntashirakon.AppManager.filters.options.FilterOption.TYPE_STR_SINGLE; import static io.github.muntashirakon.AppManager.filters.options.FilterOption.TYPE_TIME_MILLIS; import android.annotation.SuppressLint; import android.app.Dialog; import android.os.Bundle; import android.text.Editable; import android.text.InputType; import android.text.TextUtils; import android.text.TextWatcher; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.CheckedTextView; import androidx.annotation.LayoutRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.os.BundleCompat; import androidx.core.widget.TextViewCompat; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.FragmentActivity; import androidx.recyclerview.widget.LinearLayoutManager; import com.google.android.material.color.MaterialColors; import com.google.android.material.datepicker.MaterialDatePicker; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.resources.MaterialAttributes; import com.google.android.material.textfield.TextInputEditText; import com.google.android.material.textfield.TextInputLayout; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.filters.options.FilterOption; import io.github.muntashirakon.AppManager.filters.options.FilterOptions; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.DateUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.adapters.SelectedArrayAdapter; import io.github.muntashirakon.util.AdapterUtils; import io.github.muntashirakon.view.TextInputLayoutCompat; import io.github.muntashirakon.widget.MaterialSpinner; import io.github.muntashirakon.widget.RecyclerView; import mobi.upod.timedurationpicker.TimeDurationPickerDialog; public class EditFilterOptionFragment extends DialogFragment { public static final String TAG = EditFilterOptionFragment.class.getSimpleName(); public static final String ARG_OPTION = "opt"; public static final String ARG_POSITION = "pos"; public interface OnClickDialogButtonInterface { void onDeleteItem(int position, int id); void onUpdateItem(int position, @NonNull FilterOption item); void onAddItem(@NonNull FilterOption item); } private MaterialSpinner mKeySpinner; private TextInputLayout mGenericTextInputLayout; private TextInputEditText mGenericEditText; private TextInputLayout mDateTextInputLayout; private TextInputEditText mDateEditText; private RecyclerView mFlagsRecyclerView; @Nullable private FilterOption mFilterOption; @Nullable private FilterOption mCurrentFilterOption; @Nullable private String mCurrentKey; @FilterOption.KeyType private int mCurrentKeyType; @Nullable private ArrayAdapter mKeyAdapter; private FilterOptionFlagsAdapter mFilterOptionFlagsAdapter; private OnClickDialogButtonInterface mOnClickDialogButtonInterface; private int mPosition; private long mDate; private final TextWatcher mGenericEditTextWatcher = new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } @Override public void afterTextChanged(Editable s) { if (mCurrentKeyType != TYPE_NONE && TextUtils.isEmpty(s)) { mGenericTextInputLayout.setErrorEnabled(true); mGenericTextInputLayout.setError(getString(R.string.value_cannot_be_empty)); return; } if (mCurrentKeyType == TYPE_REGEX) { try { Pattern.compile(s.toString()); } catch (PatternSyntaxException e) { mGenericTextInputLayout.setErrorEnabled(true); mGenericTextInputLayout.setError(getString(R.string.invalid_regex)); return; } } else if (mCurrentKeyType == TYPE_DURATION_MILLIS) { try { mDate = Long.parseLong(s.toString()); mDateEditText.setText(DateUtils.getFormattedDuration(ContextUtils.getContext(), mDate)); } catch (NumberFormatException ignore) { } } else if (mCurrentKeyType == TYPE_TIME_MILLIS) { try { mDate = Long.parseLong(s.toString()); mDateEditText.setText(DateUtils.formatDate(ContextUtils.getContext(), mDate)); } catch (NumberFormatException ignore) { } } else if (mCurrentKeyType == TYPE_INT_FLAGS) { try { mFilterOptionFlagsAdapter.setFlag(Integer.parseInt(s.toString())); } catch (NumberFormatException ignore) { } } mGenericTextInputLayout.setErrorEnabled(false); } }; public void setOnClickDialogButtonInterface(OnClickDialogButtonInterface onClickDialogButtonInterface) { mOnClickDialogButtonInterface = onClickDialogButtonInterface; } @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { FragmentActivity activity = requireActivity(); Bundle args = requireArguments(); mFilterOption = BundleCompat.getParcelable(args, ARG_OPTION, FilterOption.class); mPosition = args.getInt(ARG_POSITION, -1); boolean editMode = mFilterOption != null; View view = View.inflate(activity, R.layout.dialog_edit_filter_option, null); MaterialSpinner filterSpinner = view.findViewById(R.id.filter_selector_spinner); ArrayAdapter filters = SelectedArrayAdapter.createFromResource(activity, R.array.finder_filters, io.github.muntashirakon.ui.R.layout.auto_complete_dropdown_item); filterSpinner.setAdapter(filters); mKeySpinner = view.findViewById(R.id.type_selector_spinner); mKeyAdapter = new SelectedArrayAdapter<>(requireActivity(), io.github.muntashirakon.ui.R.layout.auto_complete_dropdown_item); mKeySpinner.setAdapter(mKeyAdapter); mGenericEditText = view.findViewById(R.id.input_string); mGenericEditText.addTextChangedListener(mGenericEditTextWatcher); mGenericTextInputLayout = TextInputLayoutCompat.fromTextInputEditText(mGenericEditText); mDateEditText = view.findViewById(android.R.id.input); mDateEditText.setKeyListener(null); mDateTextInputLayout = TextInputLayoutCompat.fromTextInputEditText(mDateEditText); mFlagsRecyclerView = view.findViewById(R.id.recycler_view); mFlagsRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); @SuppressLint({"RestrictedApi", "PrivateResource"}) int layoutId = MaterialAttributes.resolveInteger(requireContext(), androidx.appcompat.R.attr.multiChoiceItemLayout, com.google.android.material.R.layout.mtrl_alert_select_dialog_multichoice); mFilterOptionFlagsAdapter = new FilterOptionFlagsAdapter(layoutId, v -> mGenericEditText.setText(String.valueOf(mFilterOptionFlagsAdapter.getFlag()))); mFlagsRecyclerView.setAdapter(mFilterOptionFlagsAdapter); if (mFilterOption != null) { mCurrentFilterOption = mFilterOption; filterSpinner.setSelection(filters.getPosition(mCurrentFilterOption.type)); updateUiForFilter(mCurrentFilterOption); } else { filterSpinner.setSelection(-1); } // Setup listeners filterSpinner.setOnItemClickListener((parent, v, position, id) -> { mCurrentFilterOption = FilterOptions.create(filters.getItem(position).toString()); updateUiForFilter(mCurrentFilterOption); }); mKeySpinner.setOnItemClickListener((parent, view1, position, id) -> { if (mKeyAdapter == null || mCurrentFilterOption == null) { return; } mCurrentKey = mKeyAdapter.getItem(position); int lastKeyType = mCurrentKeyType; mCurrentKeyType = Objects.requireNonNull(mCurrentFilterOption.getKeysWithType().get(mCurrentKey)); // Reset value if the data is not of the same type if (lastKeyType != mCurrentKeyType) { mGenericEditText.setText(""); } updateUiForType(mCurrentKeyType); }); Objects.requireNonNull(mOnClickDialogButtonInterface); MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(activity); builder.setView(view) .setPositiveButton(editMode ? R.string.update : R.string.add, (dialog, which) -> { if (mCurrentFilterOption == null) { UIUtils.displayLongToast(R.string.key_name_cannot_be_null); return; } Editable editable = mGenericEditText.getText(); try { Objects.requireNonNull(mCurrentKey); mCurrentFilterOption.setKeyValue(mCurrentKey, TextUtils.isEmpty(editable) ? null : editable.toString()); } catch (Exception e) { e.printStackTrace(); UIUtils.displayLongToast(R.string.error_evaluating_input); return; } if (editMode) { mOnClickDialogButtonInterface.onUpdateItem(mPosition, mCurrentFilterOption); } else { mOnClickDialogButtonInterface.onAddItem(mCurrentFilterOption); } }) .setNegativeButton(R.string.cancel, (dialog, which) -> { if (getDialog() != null) getDialog().cancel(); }); if (editMode) { builder.setNeutralButton(R.string.delete, (dialog, which) -> mOnClickDialogButtonInterface.onDeleteItem(mPosition, mFilterOption.id)); } return builder.create(); } private void updateUiForFilter(@NonNull FilterOption filterOption) { if (mKeyAdapter == null) { return; } // List all keys mKeyAdapter.clear(); mKeyAdapter.addAll(filterOption.getKeysWithType().keySet()); // Set default or previously set key as the current key mCurrentKey = filterOption.getKey(); mCurrentKeyType = filterOption.getKeyType(); mKeySpinner.setSelection(mKeyAdapter.getPosition(mCurrentKey)); // Update the text field mGenericEditText.setText(filterOption.getValue()); updateUiForType(mCurrentKeyType); } private void updateUiForType(@FilterOption.KeyType int type) { // Update visibility if (type == TYPE_NONE || type == TYPE_INT_FLAGS) { mGenericTextInputLayout.setVisibility(View.GONE); mDateTextInputLayout.setVisibility(View.GONE); } else if (type == TYPE_DURATION_MILLIS || type == TYPE_TIME_MILLIS) { mGenericTextInputLayout.setVisibility(View.GONE); mDateTextInputLayout.setVisibility(View.VISIBLE); } else { mGenericTextInputLayout.setVisibility(View.VISIBLE); mDateTextInputLayout.setVisibility(View.GONE); } if (type == TYPE_INT_FLAGS) { mFlagsRecyclerView.setVisibility(View.VISIBLE); } else mFlagsRecyclerView.setVisibility(View.GONE); // Update single-line mGenericEditText.setSingleLine(type != TYPE_STR_MULTIPLE); // Update hint, input-type switch (type) { case TYPE_NONE: break; case TYPE_INT_FLAGS: mGenericEditText.setInputType(InputType.TYPE_CLASS_NUMBER); Objects.requireNonNull(mCurrentFilterOption); mFilterOptionFlagsAdapter.setFlagMap(mCurrentFilterOption.getFlags(Objects.requireNonNull(mCurrentKey))); break; case TYPE_DURATION_MILLIS: mDateTextInputLayout.setHint(R.string.duration); mGenericEditText.setInputType(InputType.TYPE_CLASS_NUMBER); mDateEditText.setOnClickListener(v -> openDurationPicker()); break; case TYPE_INT: mGenericTextInputLayout.setHint(R.string.integer_value); mGenericEditText.setInputType(InputType.TYPE_CLASS_NUMBER); break; case TYPE_LONG: mGenericTextInputLayout.setHint(R.string.long_integer_value); mGenericEditText.setInputType(InputType.TYPE_CLASS_NUMBER); break; case TYPE_REGEX: mGenericTextInputLayout.setHint(R.string.search_option_regex); mGenericEditText.setInputType(InputType.TYPE_CLASS_TEXT); break; case TYPE_SIZE_BYTES: mGenericTextInputLayout.setHint(R.string.size_in_bytes); mGenericEditText.setInputType(InputType.TYPE_CLASS_NUMBER); break; case TYPE_STR_MULTIPLE: mGenericTextInputLayout.setHint(R.string.string_value); // newlines mGenericEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE); break; case TYPE_STR_SINGLE: mGenericTextInputLayout.setHint(R.string.string_value); mGenericEditText.setInputType(InputType.TYPE_CLASS_TEXT); break; case TYPE_TIME_MILLIS: mDateTextInputLayout.setHint(R.string.date); mGenericEditText.setInputType(InputType.TYPE_CLASS_NUMBER); mDateEditText.setOnClickListener(v -> openDatePicker()); break; } } public void openDatePicker() { MaterialDatePicker datePicker = MaterialDatePicker.Builder.datePicker() .setTitleText(R.string.date) .setSelection(mDate <= 0 ? MaterialDatePicker.todayInUtcMilliseconds() : mDate) .build(); datePicker.addOnPositiveButtonClickListener(selection -> mGenericEditText.setText(String.valueOf(selection))); datePicker.show(getChildFragmentManager(), "DatePicker"); } public void openDurationPicker() { new TimeDurationPickerDialog(requireContext(), (picker, duration) -> mGenericEditText.setText(String.valueOf(duration)), mDate) .show(); } private static class FilterOptionFlagsAdapter extends RecyclerView.Adapter { @LayoutRes private final int mLayoutId; private final View.OnClickListener mItemClickListener; private final List mFlags = Collections.synchronizedList(new ArrayList<>()); private Map mFlagMap; private int mFlag; public FilterOptionFlagsAdapter(@LayoutRes int layoutId, View.OnClickListener itemClickListener) { mLayoutId = layoutId; mFlagMap = Collections.emptyMap(); mItemClickListener = itemClickListener; } public void setFlagMap(@NonNull Map flagMap) { mFlagMap = flagMap; AdapterUtils.notifyDataSetChanged(this, mFlags, new ArrayList<>(flagMap.keySet())); } public void setFlag(int flag) { mFlag = flag; notifyItemRangeChanged(0, mFlags.size(), AdapterUtils.STUB); } public int getFlag() { return mFlag; } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View v = LayoutInflater.from(parent.getContext()).inflate(mLayoutId, parent, false); return new ViewHolder(v); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { int flag = mFlags.get(position); CharSequence flagName = mFlagMap.get(flag); holder.item.setText(flagName); holder.item.setChecked((mFlag & flag) != 0); holder.item.setOnClickListener(v -> { if ((mFlag & flag) != 0) { // Already selected, deselect mFlag &= ~flag; holder.item.setChecked(false); } else { // Not yet selected, select mFlag |= flag; holder.item.setChecked(true); } mItemClickListener.onClick(v); }); } @Override public int getItemCount() { return mFlags.size(); } static class ViewHolder extends RecyclerView.ViewHolder { CheckedTextView item; @SuppressLint("RestrictedApi") public ViewHolder(@NonNull View itemView) { super(itemView); item = itemView.findViewById(android.R.id.text1); int textAppearanceBodyLarge = MaterialAttributes.resolveInteger(item.getContext(), com.google.android.material.R.attr.textAppearanceBodyLarge, 0); TextViewCompat.setTextAppearance(item, textAppearanceBodyLarge); item.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16); item.setTextColor(MaterialColors.getColor(item.getContext(), com.google.android.material.R.attr.colorOnSurfaceVariant, -1)); } } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/filters/EditFiltersDialogFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.filters; import android.app.Dialog; import android.graphics.Color; import android.os.Bundle; import android.text.Editable; import android.text.Spanned; import android.text.TextUtils; import android.text.TextWatcher; import android.text.style.ForegroundColorSpan; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.FragmentActivity; import androidx.recyclerview.widget.LinearLayoutManager; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.textfield.TextInputEditText; import com.google.android.material.textfield.TextInputLayout; import java.util.HashMap; import java.util.Map; import java.util.Objects; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.filters.options.FilterOption; import io.github.muntashirakon.dialog.DialogTitleBuilder; import io.github.muntashirakon.view.TextInputLayoutCompat; import io.github.muntashirakon.widget.RecyclerView; public class EditFiltersDialogFragment extends DialogFragment implements EditFilterOptionFragment.OnClickDialogButtonInterface { public static final String TAG = EditFiltersDialogFragment.class.getSimpleName(); public interface OnSaveDialogButtonInterface { @NonNull FilterItem getFilterItem(); void onItemAltered(@NonNull FilterItem item); } private static final Map HIGHLIGHT_MAP = new HashMap() {{ put("&", Color.RED); put("|", Color.RED); put("(", Color.RED); put(")", Color.RED); put("true", Color.BLUE); put("false", Color.BLUE); }}; private static class ExprTester extends AbsExpressionEvaluator { private final FilterItem mFilterItem; public ExprTester(FilterItem filterItem) { mFilterItem = filterItem; } @Override protected boolean evalId(@NonNull String id) { if (TextUtils.isEmpty(id)) { return false; } // Extract ID int idx = id.lastIndexOf('_'); int intId; if (idx >= 0 && id.length() > (idx + 1)) { String part2 = id.substring(idx + 1); if (TextUtils.isDigitsOnly(part2)) { intId = Integer.parseInt(part2); } else intId = 0; } else intId = 0; FilterOption option = mFilterItem.getFilterOptionForId(intId); if (option == null) { lastError = "Invalid ID '" + id + "'"; } return option != null; } } private FinderFilterAdapter mFinderFilterAdapter; private TextInputLayout mFinderFilterEditorLayout; private TextInputEditText mFinderFilterEditor; private FilterItem mFilterItem; private OnSaveDialogButtonInterface mOnSaveDialogButtonInterface; private boolean mFilterEditorModified = false; private ExprTester mExprTester; private final TextWatcher mFinderFilterEditorWatcher = new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } @Override public void afterTextChanged(Editable s) { updateEditorColors(s); mFilterEditorModified = true; } }; public void setOnSaveDialogButtonInterface(OnSaveDialogButtonInterface onSaveDialogButtonInterface) { mOnSaveDialogButtonInterface = onSaveDialogButtonInterface; } @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { FragmentActivity activity = requireActivity(); mFilterItem = Objects.requireNonNull(mOnSaveDialogButtonInterface).getFilterItem(); mFinderFilterAdapter = new FinderFilterAdapter(mFilterItem); View view = View.inflate(activity, R.layout.dialog_edit_filter_item, null); RecyclerView recyclerView = view.findViewById(android.R.id.list); recyclerView.setLayoutManager(new LinearLayoutManager(activity)); recyclerView.setAdapter(mFinderFilterAdapter); mFinderFilterEditor = view.findViewById(R.id.editor); mFinderFilterEditor.setText(mFilterItem.getExpr()); mFinderFilterEditor.addTextChangedListener(mFinderFilterEditorWatcher); mFinderFilterEditorLayout = TextInputLayoutCompat.fromTextInputEditText(mFinderFilterEditor); DialogTitleBuilder builder = new DialogTitleBuilder(activity) .setTitle(R.string.filters) .setEndIcon(R.drawable.ic_add, v -> { EditFilterOptionFragment dialogFragment = new EditFilterOptionFragment(); Bundle args = new Bundle(); dialogFragment.setArguments(args); dialogFragment.setOnClickDialogButtonInterface(this); dialogFragment.show(getChildFragmentManager(), EditFilterOptionFragment.TAG); }) .setEndIconContentDescription(R.string.add_filter_ellipsis); mFinderFilterAdapter.setOnItemClickListener(new FinderFilterAdapter.OnClickListener() { @Override public void onEdit(View view, int position, FilterOption filterOption) { displayEditor(position, filterOption); } @Override public void onRemove(View view, int position, FilterOption filterOption) { onDeleteItem(position, filterOption.id); } }); return new MaterialAlertDialogBuilder(activity) .setCustomTitle(builder.build()) .setView(view) .setNegativeButton(R.string.cancel, null) .setPositiveButton(R.string.apply, (dialog, which) -> { if (mFilterEditorModified && mFinderFilterEditorLayout.getError() == null) { mFilterItem.setExpr(mFinderFilterEditor.getText().toString()); } mOnSaveDialogButtonInterface.onItemAltered(mFilterItem); }) .show(); } private void displayEditor(int position, @NonNull FilterOption filterOption) { EditFilterOptionFragment dialogFragment = new EditFilterOptionFragment(); Bundle args = new Bundle(); args.putParcelable(EditFilterOptionFragment.ARG_OPTION, filterOption); args.putInt(EditFilterOptionFragment.ARG_POSITION, position); dialogFragment.setArguments(args); dialogFragment.setOnClickDialogButtonInterface(this); dialogFragment.show(getChildFragmentManager(), EditFilterOptionFragment.TAG); } @Override public void onAddItem(@NonNull FilterOption item) { mFinderFilterAdapter.add(item); mFinderFilterEditor.removeTextChangedListener(mFinderFilterEditorWatcher); mFinderFilterEditor.setText(mFilterItem.getExpr()); updateEditorColors(mFinderFilterEditor.getText()); mFinderFilterEditor.addTextChangedListener(mFinderFilterEditorWatcher); } @Override public void onUpdateItem(int position, @NonNull FilterOption item) { mFinderFilterAdapter.update(position, item); mFinderFilterEditor.removeTextChangedListener(mFinderFilterEditorWatcher); mFinderFilterEditor.setText(mFilterItem.getExpr()); updateEditorColors(mFinderFilterEditor.getText()); mFinderFilterEditor.addTextChangedListener(mFinderFilterEditorWatcher); } @Override public void onDeleteItem(int position, int id) { mFinderFilterAdapter.remove(position, id); mFinderFilterEditor.removeTextChangedListener(mFinderFilterEditorWatcher); mFinderFilterEditor.setText(mFilterItem.getExpr()); updateEditorColors(mFinderFilterEditor.getText()); mFinderFilterEditor.addTextChangedListener(mFinderFilterEditorWatcher); } private void updateEditorColors(@Nullable Editable s) { if (mExprTester == null) { mExprTester = new ExprTester(mFilterItem); } if (s == null) { return; } String text = s.toString(); for (Map.Entry entry : HIGHLIGHT_MAP.entrySet()) { String keyword = entry.getKey(); int color = entry.getValue(); int index = text.indexOf(keyword); while (index >= 0) { s.setSpan(new ForegroundColorSpan(color), index, index + keyword.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); index = text.indexOf(keyword, index + keyword.length()); } } if (!mExprTester.evaluate(s.toString())) { CharSequence error = mExprTester.getLastError(); mFinderFilterEditorLayout.setError(error); } else { mFinderFilterEditorLayout.setError(null); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/filters/FilterItem.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.filters; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.collection.ArrayMap; import androidx.core.os.ParcelCompat; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.List; import java.util.Objects; import io.github.muntashirakon.AppManager.filters.options.DataUsageOption; import io.github.muntashirakon.AppManager.filters.options.FilterOption; import io.github.muntashirakon.AppManager.filters.options.RunningAppsOption; import io.github.muntashirakon.AppManager.filters.options.ScreenTimeOption; import io.github.muntashirakon.AppManager.filters.options.TimesOpenedOption; import io.github.muntashirakon.AppManager.history.IJsonSerializer; import io.github.muntashirakon.AppManager.history.JsonDeserializer; import io.github.muntashirakon.util.ParcelUtils; public class FilterItem implements IJsonSerializer, Parcelable { private static class ExprEvaluator extends AbsExpressionEvaluator { private final ArrayMap mFilterOptions; @Nullable private IFilterableAppInfo mInfo; @Nullable private FilterOption.TestResult mResult; public ExprEvaluator(ArrayMap filterOptions) { mFilterOptions = filterOptions; } public void setInfo(@Nullable IFilterableAppInfo info) { mInfo = info; mResult = new FilterOption.TestResult(); } @Nullable public FilterOption.TestResult getResult() { return mResult; } @Override protected boolean evalId(@NonNull String id) { if (mResult == null) { mResult = new FilterOption.TestResult(); } // Extract ID int idx = id.lastIndexOf('_'); int intId; if (idx >= 0 && id.length() > (idx + 1)) { intId = Integer.parseInt(id.substring(idx + 1)); } else intId = 0; FilterOption option = mFilterOptions.get(intId); if (option == null || mInfo == null) { return false; } return option.test(mInfo, mResult).isMatched(); } } @NonNull private String mName; private final ArrayMap mFilterOptions; private String mExpr = ""; private boolean mCustomExpr = false; // Assign this id to the next filter option (starts with 1) private int mNextId = 1; // Counters for special cases private int mTimesUsageInfoUsed = 0; private int mTimesRunningOptionUsed = 0; public FilterItem() { this("Untitled"); } private FilterItem(@NonNull String name) { mName = name; mFilterOptions = new ArrayMap<>(); } @NonNull public String getName() { return mName; } public void setName(@NonNull String name) { mName = name; } @NonNull public String getExpr() { return mExpr; } public void setExpr(@NonNull String expr) { mExpr = expr; mCustomExpr = true; } public int addFilterOption(@NonNull FilterOption filterOption) { filterOption.id = getNextId(); if (!mCustomExpr) { String id = filterOption.getFullId(); // Add this to expr if (TextUtils.isEmpty(mExpr)) { mExpr = id; } else mExpr += " & " + id; } incrementUsage(filterOption, true); if (mFilterOptions.put(filterOption.id, filterOption) == null) { return mFilterOptions.indexOfKey(filterOption.id); } return -1; } public void updateFilterOptionAt(int i, @NonNull FilterOption filterOption) { FilterOption oldFilterOption = mFilterOptions.valueAt(i); if (oldFilterOption == null) { throw new IllegalArgumentException("Invalid index " + i); } filterOption.id = oldFilterOption.id; mFilterOptions.setValueAt(i, filterOption); if (!mCustomExpr) { String idStr = oldFilterOption.getFullId(); // Default expression is just all the filters &'ed together String[] ops = mExpr.split(" & "); StringBuilder sb = new StringBuilder(); for (String op : ops) { if (sb.length() > 0) { sb.append(" & "); } if (idStr.equals(op)) { sb.append(filterOption.getFullId()); } else sb.append(op); } mExpr = sb.toString(); } incrementUsage(oldFilterOption, false); incrementUsage(filterOption, true); } public boolean removeFilterOptionAt(int i) { FilterOption filterOption = mFilterOptions.removeAt(i); if (filterOption == null) { return false; } mNextId = filterOption.id; if (!mCustomExpr) { String idStr = filterOption.getFullId(); // Default expression is just all the filters &'ed together String[] ops = mExpr.split(" & "); StringBuilder sb = new StringBuilder(); for (String op : ops) { if (!idStr.equals(op)) { if (sb.length() > 0) { sb.append(" & "); } sb.append(op); } } mExpr = sb.toString(); } incrementUsage(filterOption, false); return true; } public int getSize() { return mFilterOptions.size(); } public FilterOption getFilterOptionAt(int i) { return mFilterOptions.valueAt(i); } @Nullable public FilterOption getFilterOptionForId(int id) { return mFilterOptions.get(id); } public int getTimesUsageInfoUsed() { return mTimesUsageInfoUsed; } public int getTimesRunningOptionUsed() { return mTimesRunningOptionUsed; } public List> getFilteredList(@NonNull List allFilterableAppInfo) { List> filteredFilterableAppInfo = new ArrayList<>(); ExprEvaluator evaluator = new ExprEvaluator(mFilterOptions); String expr = TextUtils.isEmpty(mExpr) ? "true" : mExpr; for (T info : allFilterableAppInfo) { evaluator.setInfo(info); boolean eval = evaluator.evaluate(expr); FilterOption.TestResult result = Objects.requireNonNull(evaluator.getResult()); if (eval) { filteredFilterableAppInfo.add(new FilteredItemInfo<>(info, result)); } } return filteredFilterableAppInfo; } private void incrementUsage(FilterOption filterOption, boolean increment) { boolean requireCountUpdate; if (filterOption instanceof DataUsageOption) { requireCountUpdate = true; } else if (filterOption instanceof TimesOpenedOption) { requireCountUpdate = true; } else if (filterOption instanceof ScreenTimeOption) { requireCountUpdate = true; } else requireCountUpdate = false; if (requireCountUpdate) { if (increment) ++mTimesUsageInfoUsed; else --mTimesUsageInfoUsed; return; } if (filterOption instanceof RunningAppsOption) { if (increment) ++mTimesRunningOptionUsed; else --mTimesRunningOptionUsed; } } public FilterItem(@NonNull Parcel in) { mName = Objects.requireNonNull(in.readString()); mExpr = Objects.requireNonNull(in.readString()); mCustomExpr = ParcelCompat.readBoolean(in); mFilterOptions = ParcelUtils.readArrayMap(in, Integer.class.getClassLoader(), FilterOption.class.getClassLoader()); } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeString(mName); dest.writeString(mExpr); ParcelCompat.writeBoolean(dest, mCustomExpr); ParcelUtils.writeMap(mFilterOptions, dest); } @Override public int describeContents() { return 0; } public static final Creator CREATOR = new Creator() { @Override @NonNull public FilterItem createFromParcel(@NonNull Parcel in) { return new FilterItem(in); } @Override @NonNull public FilterItem[] newArray(int size) { return new FilterItem[size]; } }; @NonNull @Override public JSONObject serializeToJson() throws JSONException { JSONObject object = new JSONObject(); JSONArray array = new JSONArray(); for (FilterOption filterOption : mFilterOptions.values()) { array.put(filterOption.toJson()); } object.put("name", mName); object.put("expr", mExpr); object.put("custom_expr", mCustomExpr); object.put("options", array); return object; } public FilterItem(@NonNull JSONObject object) throws JSONException { mName = object.getString("name"); mExpr = object.getString("expr"); mCustomExpr = object.getBoolean("custom_expr"); mFilterOptions = new ArrayMap<>(); JSONArray array = object.getJSONArray("options"); for (int i = 0; i < array.length(); ++i) { FilterOption option = FilterOption.fromJson(array.getJSONObject(i)); mFilterOptions.put(option.id, option); } } public static final JsonDeserializer.Creator DESERIALIZER = FilterItem::new; private int getNextId() { // Find next ID while (mFilterOptions.containsKey(mNextId)) { ++mNextId; } return mNextId; } public static class FilteredItemInfo { public final T info; public final FilterOption.TestResult result; FilteredItemInfo(T info, FilterOption.TestResult result) { this.info = info; this.result = result; } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/filters/FilterableAppInfo.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.filters; import android.app.ActivityManager; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.ComponentInfo; import android.content.pm.FeatureInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PermissionInfo; import android.content.pm.ProviderInfo; import android.content.pm.ServiceInfo; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.RemoteException; import android.os.UserHandleHidden; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.core.content.pm.PackageInfoCompat; import java.io.IOException; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import aosp.libcore.util.EmptyArray; import io.github.muntashirakon.AppManager.StaticDataset; import io.github.muntashirakon.AppManager.apk.signing.SignerInfo; import io.github.muntashirakon.AppManager.backup.BackupUtils; import io.github.muntashirakon.AppManager.compat.ActivityManagerCompat; import io.github.muntashirakon.AppManager.compat.AppOpsManagerCompat; import io.github.muntashirakon.AppManager.compat.ApplicationInfoCompat; import io.github.muntashirakon.AppManager.compat.DeviceIdleManagerCompat; import io.github.muntashirakon.AppManager.compat.InstallSourceInfoCompat; import io.github.muntashirakon.AppManager.compat.ManifestCompat; import io.github.muntashirakon.AppManager.compat.PackageManagerCompat; import io.github.muntashirakon.AppManager.compat.SensorServiceCompat; import io.github.muntashirakon.AppManager.db.entity.Backup; import io.github.muntashirakon.AppManager.debloat.DebloatObject; import io.github.muntashirakon.AppManager.filters.options.ComponentsOption; import io.github.muntashirakon.AppManager.filters.options.FreezeOption; import io.github.muntashirakon.AppManager.rules.compontents.ComponentUtils; import io.github.muntashirakon.AppManager.rules.compontents.ComponentsBlocker; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.ssaid.SsaidSettings; import io.github.muntashirakon.AppManager.types.PackageSizeInfo; import io.github.muntashirakon.AppManager.usage.AppUsageStatsManager; import io.github.muntashirakon.AppManager.usage.PackageUsageInfo; import io.github.muntashirakon.AppManager.utils.ArrayUtils; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.DigestUtils; import io.github.muntashirakon.AppManager.utils.ExUtils; import io.github.muntashirakon.AppManager.utils.KeyStoreUtils; import io.github.muntashirakon.AppManager.utils.PackageUtils; public class FilterableAppInfo implements IFilterableAppInfo { private final PackageInfo mPackageInfo; @Nullable private final PackageUsageInfo mPackageUsageInfo; private final ApplicationInfo mApplicationInfo; private final int mUserId; private final PackageManager mPm; private String mAppLabel; @Nullable private String mSsaid; private InstallSourceInfoCompat mInstallerInfo; @Nullable private SignerInfo mSignerInfo; private String[] mSignatureSubjectLines; private String[] mSignatureSha256Checksums; private Map mAllComponents; private Map mTrackerComponents; private List mUsedPermissions; private Backup[] mBackups; private List mAppOpEntries; @Nullable private PackageSizeInfo mPackageSizeInfo; @Nullable private AppUsageStatsManager.DataUsage mDataUsage; @Nullable private DebloatObject mBloatwareInfo; private Integer mFreezeFlags = null; private Boolean mUsesSensors = null; private Boolean mBatteryOptEnabled = null; private Boolean mHasKeystoreItems = null; private Integer mRulesCount = null; public FilterableAppInfo(@NonNull PackageInfo packageInfo, @Nullable PackageUsageInfo packageUsageInfo) { mPackageInfo = packageInfo; mPackageUsageInfo = packageUsageInfo; mApplicationInfo = Objects.requireNonNull(packageInfo.applicationInfo); mUserId = UserHandleHidden.getUserId(mApplicationInfo.uid); mPm = ContextUtils.getContext().getPackageManager(); } @NonNull public PackageInfo getPackageInfo() { return mPackageInfo; } @NonNull public ApplicationInfo getApplicationInfo() { return mApplicationInfo; } @Override @NonNull public String getPackageName() { return mPackageInfo.packageName; } @Override public int getUserId() { return mUserId; } @Override public int getUid() { return mApplicationInfo.uid; } @Override @NonNull public String getAppLabel() { if (mAppLabel == null) { mAppLabel = mApplicationInfo.loadLabel(mPm).toString(); } return mAppLabel; } @NonNull @Override public Drawable getAppIcon() { return mApplicationInfo.loadIcon(mPm); } @Override @Nullable public String getVersionName() { return mPackageInfo.versionName; } @Override public long getVersionCode() { return PackageInfoCompat.getLongVersionCode(mPackageInfo); } @Override public long getFirstInstallTime() { return mPackageInfo.firstInstallTime; } @Override public long getLastUpdateTime() { return mPackageInfo.lastUpdateTime; } @Override public int getTargetSdk() { return mApplicationInfo.targetSdkVersion; } @Override @RequiresApi(Build.VERSION_CODES.S) public int getCompileSdk() { return mApplicationInfo.compileSdkVersion; } @Override @RequiresApi(Build.VERSION_CODES.N) public int getMinSdk() { return mApplicationInfo.minSdkVersion; } @Override @NonNull public Backup[] getBackups() { if (mBackups == null) { mBackups = BackupUtils.getBackupMetadataFromDbNoLockValidate(getPackageName()).toArray(new Backup[0]); } return mBackups; } @Override public boolean isRunning() { for (ActivityManager.RunningAppProcessInfo info : ActivityManagerCompat.getRunningAppProcesses()) { if (ArrayUtils.contains(info.pkgList, mPackageInfo.packageName)) { return true; } } return false; } @Override @NonNull public Map getTrackerComponents() { if (mTrackerComponents == null) { Map allComponents = getAllComponents(); Map trackerComponents = new LinkedHashMap<>(); for (ComponentInfo itemInfo : allComponents.keySet()) { if (ComponentUtils.isTracker(itemInfo.name)) { trackerComponents.put(itemInfo, allComponents.get(itemInfo)); } } mTrackerComponents = trackerComponents; } return mTrackerComponents; } @Override @NonNull public List getAppOps() { if (mAppOpEntries == null && isInstalled()) { List packageOps = ExUtils.exceptionAsNull(() -> new AppOpsManagerCompat().getOpsForPackage(mApplicationInfo.uid, getPackageName(), null)); if (packageOps != null && packageOps.size() == 1) { mAppOpEntries = packageOps.get(0).getOps(); } } else mAppOpEntries = Collections.emptyList(); return mAppOpEntries; } @Override @NonNull public Map getAllComponents() { if (mAllComponents == null) { Map components = new LinkedHashMap<>(); if (mPackageInfo.activities != null) { for (ActivityInfo info : mPackageInfo.activities) { components.put(info, ComponentsOption.COMPONENT_TYPE_ACTIVITY); } } if (mPackageInfo.services != null) { for (ServiceInfo info : mPackageInfo.services) { components.put(info, ComponentsOption.COMPONENT_TYPE_SERVICE); } } if (mPackageInfo.receivers != null) { for (ActivityInfo info : mPackageInfo.receivers) { components.put(info, ComponentsOption.COMPONENT_TYPE_RECEIVER); } } if (mPackageInfo.providers != null) { for (ProviderInfo info : mPackageInfo.providers) { components.put(info, ComponentsOption.COMPONENT_TYPE_PROVIDER); } } mAllComponents = components; } return mAllComponents; } @Override @NonNull public List getAllPermissions() { if (mUsedPermissions == null) { Set usedPermissions = new HashSet<>(); if (mPackageInfo.requestedPermissions != null) { Collections.addAll(usedPermissions, mPackageInfo.requestedPermissions); } if (mPackageInfo.permissions != null) { for (PermissionInfo perm : mPackageInfo.permissions) { usedPermissions.add(perm.name); } } if (mPackageInfo.activities != null) { for (ActivityInfo info : mPackageInfo.activities) { if (info.permission != null) { usedPermissions.add(info.permission); } } } if (mPackageInfo.services != null) { for (ServiceInfo info : mPackageInfo.services) { if (info.permission != null) { usedPermissions.add(info.permission); } } } if (mPackageInfo.receivers != null) { for (ActivityInfo info : mPackageInfo.receivers) { if (info.permission != null) { usedPermissions.add(info.permission); } } } mUsedPermissions = new ArrayList<>(usedPermissions); } return mUsedPermissions; } @Override @NonNull public FeatureInfo[] getAllRequestedFeatures() { return ArrayUtils.defeatNullable(FeatureInfo.class, mPackageInfo.reqFeatures); } @Override public boolean isInstalled() { return ApplicationInfoCompat.isInstalled(mApplicationInfo); } @Override public boolean isFrozen() { return !isEnabled() || isSuspended() || isHidden(); } @Override public int getFreezeFlags() { if (mFreezeFlags != null) { return mFreezeFlags; } mFreezeFlags = 0; if (!isEnabled()) { mFreezeFlags |= FreezeOption.FREEZE_TYPE_DISABLED; } if (isHidden()) { mFreezeFlags |= FreezeOption.FREEZE_TYPE_HIDDEN; } if (isSuspended()) { mFreezeFlags |= FreezeOption.FREEZE_TYPE_SUSPENDED; } return mFreezeFlags; } @Override public boolean isStopped() { return ApplicationInfoCompat.isStopped(mApplicationInfo); } @Override public boolean isTestOnly() { return ApplicationInfoCompat.isTestOnly(mApplicationInfo); } @Override public boolean isDebuggable() { return (mApplicationInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0; } @Override public boolean isSystemApp() { return ApplicationInfoCompat.isSystemApp(mApplicationInfo); } @Override public boolean hasCode() { return (mApplicationInfo.flags & ApplicationInfo.FLAG_HAS_CODE) != 0; } @Override public boolean isPersistent() { return (mApplicationInfo.flags & ApplicationInfo.FLAG_PERSISTENT) != 0; } @Override public boolean isUpdatedSystemApp() { return (mApplicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0; } @Override public boolean backupAllowed() { return (mApplicationInfo.flags & ApplicationInfo.FLAG_ALLOW_BACKUP) != 0; } @Override public boolean installedInExternalStorage() { return (mApplicationInfo.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0; } @Override public boolean requestedLargeHeap() { return (mApplicationInfo.flags & ApplicationInfo.FLAG_LARGE_HEAP) != 0; } @Override public boolean supportsRTL() { return (mApplicationInfo.flags & ApplicationInfo.FLAG_SUPPORTS_RTL) != 0; } @Override public boolean dataOnlyApp() { return (mApplicationInfo.flags & ApplicationInfo.FLAG_IS_DATA_ONLY) != 0; } @Override @RequiresApi(Build.VERSION_CODES.M) public boolean usesHttp() { return (mApplicationInfo.flags & ApplicationInfo.FLAG_USES_CLEARTEXT_TRAFFIC) != 0; } @Override public boolean isPrivileged() { return ApplicationInfoCompat.isPrivileged(mApplicationInfo); } @RequiresApi(Build.VERSION_CODES.P) public boolean usesSensors() { if (!isInstalled()) { return false; } if (mUsesSensors == null) { if (SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.MANAGE_SENSORS)) { mUsesSensors = SensorServiceCompat.isSensorEnabled(getPackageName(), getUserId()); } else mUsesSensors = true; // Worse case: always true } return mUsesSensors; } @Override public boolean isBatteryOptEnabled() { if (!isInstalled()) { return true; } if (mBatteryOptEnabled == null) { mBatteryOptEnabled = DeviceIdleManagerCompat.isBatteryOptimizedApp(getPackageName()); } return mBatteryOptEnabled; } @Override public boolean hasKeyStoreItems() { if (!isInstalled()) { return false; } if (mHasKeystoreItems == null) { mHasKeystoreItems = KeyStoreUtils.hasKeyStore(mApplicationInfo.uid); } return mHasKeystoreItems; } @Override public int getRuleCount() { if (mRulesCount == null) { try (ComponentsBlocker cb = ComponentsBlocker.getInstance(getPackageName(), getUserId(), false)) { mRulesCount = cb.entryCount(); } } return mRulesCount; } @Override @NonNull public String getSsaid() { if (mSsaid == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { try { mSsaid = new SsaidSettings(mUserId).getSsaid(getPackageName(), mApplicationInfo.uid); } catch (IOException ignore) { } } if (mSsaid == null) { mSsaid = ""; } return mSsaid; } @Override public boolean hasDomainUrls() { return ApplicationInfoCompat.hasDomainUrls(mApplicationInfo); } @Override public boolean hasStaticSharedLibrary() { return ApplicationInfoCompat.isStaticSharedLibrary(mApplicationInfo); } @Override public boolean isHidden() { return ApplicationInfoCompat.isHidden(mApplicationInfo); } @Override public boolean isSuspended() { return ApplicationInfoCompat.isSuspended(mApplicationInfo); } @Override public boolean isEnabled() { return mApplicationInfo.enabled; } @Override @Nullable public String getSharedUserId() { return mPackageInfo.sharedUserId; } private void fetchPackageSizeInfo() { if (mPackageSizeInfo == null && isInstalled()) { mPackageSizeInfo = PackageUtils.getPackageSizeInfo(ContextUtils.getContext(), getPackageName(), mUserId, null); } } @Override public long getTotalSize() { fetchPackageSizeInfo(); return mPackageSizeInfo != null ? mPackageSizeInfo.getTotalSize() : 0; } @Override public long getApkSize() { fetchPackageSizeInfo(); return mPackageSizeInfo != null ? (mPackageSizeInfo.codeSize + mPackageSizeInfo.obbSize) : 0; } @Override public long getCacheSize() { fetchPackageSizeInfo(); return mPackageSizeInfo != null ? mPackageSizeInfo.cacheSize : 0; } @Override public long getDataSize() { fetchPackageSizeInfo(); return mPackageSizeInfo != null ? (mPackageSizeInfo.dataSize + mPackageSizeInfo.mediaSize + mPackageSizeInfo.cacheSize) : 0; } @Override @NonNull public AppUsageStatsManager.DataUsage getDataUsage() { if (mDataUsage == null && isInstalled()) { if (mPackageUsageInfo != null) { mDataUsage = AppUsageStatsManager.DataUsage.fromDataUsage(mPackageUsageInfo.mobileData, mPackageUsageInfo.wifiData); } } if (mDataUsage == null) { mDataUsage = AppUsageStatsManager.DataUsage.EMPTY; } return mDataUsage; } @Override public int getTimesOpened() { return mPackageUsageInfo != null ? mPackageUsageInfo.timesOpened : 0; } @Override public long getTotalScreenTime() { return mPackageUsageInfo != null ? mPackageUsageInfo.screenTime : 0L; } @Override public long getLastUsedTime() { return mPackageUsageInfo != null ? mPackageUsageInfo.lastUsageTime : 0L; } @Override @Nullable public SignerInfo fetchSignerInfo() { if (mSignerInfo == null) { mSignerInfo = PackageUtils.getSignerInfo(mPackageInfo, !isInstalled()); } return mSignerInfo; } @Override @NonNull public String[] getSignatureSubjectLines() { fetchSignerInfo(); if (mSignerInfo != null && mSignatureSubjectLines == null) { X509Certificate[] signatures = mSignerInfo.getAllSignerCerts(); if (signatures != null) { mSignatureSubjectLines = new String[signatures.length]; for (int i = 0; i < signatures.length; ++i) { mSignatureSubjectLines[i] = signatures[i].getSubjectX500Principal().getName(); } } } return mSignatureSubjectLines != null ? mSignatureSubjectLines : EmptyArray.STRING; } @Override @NonNull public String[] getSignatureSha256Checksums() { fetchSignerInfo(); if (mSignerInfo != null && mSignatureSha256Checksums == null) { X509Certificate[] signatures = mSignerInfo.getAllSignerCerts(); if (signatures != null) { mSignatureSha256Checksums = new String[signatures.length]; for (int i = 0; i < signatures.length; ++i) { try { mSignatureSha256Checksums[i] = DigestUtils.getHexDigest(DigestUtils.SHA_256, signatures[i].getEncoded()); } catch (CertificateEncodingException e) { mSignatureSha256Checksums[i] = ""; } } } } return mSignatureSha256Checksums != null ? mSignatureSha256Checksums : EmptyArray.STRING; } @Override @Nullable public InstallSourceInfoCompat getInstallerInfo() { if (mInstallerInfo == null && isInstalled()) { try { mInstallerInfo = PackageManagerCompat.getInstallSourceInfo(getPackageName(), mUserId); } catch (RemoteException ignore) { } } return mInstallerInfo; } @Override @Nullable public DebloatObject getBloatwareInfo() { if (mBloatwareInfo == null) { for (DebloatObject debloatObject : StaticDataset.getDebloatObjects()) { if (getPackageName().equals(debloatObject.packageName)) { mBloatwareInfo = debloatObject; break; } } } return mBloatwareInfo; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/filters/FilteringUtils.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.filters; import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.GET_SIGNING_CERTIFICATES; import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.MATCH_DISABLED_COMPONENTS; import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES; import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.MATCH_UNINSTALLED_PACKAGES; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import io.github.muntashirakon.AppManager.compat.PackageManagerCompat; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.settings.FeatureController; import io.github.muntashirakon.AppManager.usage.AppUsageStatsManager; import io.github.muntashirakon.AppManager.usage.PackageUsageInfo; import io.github.muntashirakon.AppManager.usage.TimeInterval; import io.github.muntashirakon.AppManager.usage.UsageUtils; import io.github.muntashirakon.AppManager.utils.ExUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; public final class FilteringUtils { @NonNull @WorkerThread public static List loadFilterableAppInfo(@NonNull int[] userIds) { List filterableAppInfoList = new ArrayList<>(); boolean hasUsageAccess = FeatureController.isUsageAccessEnabled() && SelfPermissions.checkUsageStatsPermission(); for (int userId : userIds) { if (ThreadUtils.isInterrupted()) return Collections.emptyList(); if (!SelfPermissions.checkCrossUserPermission(userId, false)) { // No support for cross user continue; } // List packages List packageInfoList = PackageManagerCompat.getInstalledPackages( PackageManager.GET_META_DATA | GET_SIGNING_CERTIFICATES | PackageManager.GET_ACTIVITIES | PackageManager.GET_RECEIVERS | PackageManager.GET_PROVIDERS | PackageManager.GET_SERVICES | PackageManager.GET_CONFIGURATIONS | PackageManager.GET_PERMISSIONS | PackageManager.GET_URI_PERMISSION_PATTERNS | MATCH_DISABLED_COMPONENTS | MATCH_UNINSTALLED_PACKAGES | MATCH_STATIC_SHARED_AND_SDK_LIBRARIES, userId); // List usages Map packageUsageInfoList = new HashMap<>(); if (hasUsageAccess) { TimeInterval interval = UsageUtils.getLastWeek(); List usageInfoList = ExUtils.exceptionAsNull(() -> AppUsageStatsManager.getInstance().getUsageStats(interval, userId)); if (usageInfoList != null) { for (PackageUsageInfo info : usageInfoList) { if (ThreadUtils.isInterrupted()) return Collections.emptyList(); packageUsageInfoList.put(info.packageName, info); } } } for (PackageInfo packageInfo : packageInfoList) { // Interrupt thread on request if (ThreadUtils.isInterrupted()) return Collections.emptyList(); filterableAppInfoList.add(new FilterableAppInfo(packageInfo, packageUsageInfoList.get(packageInfo.packageName))); } } return filterableAppInfoList; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/filters/FinderActivity.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.filters; import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.ViewModelProvider; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.progressindicator.LinearProgressIndicator; import java.util.Optional; import io.github.muntashirakon.AppManager.BaseActivity; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.utils.DateUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.util.UiUtils; import io.github.muntashirakon.view.ProgressIndicatorCompat; import io.github.muntashirakon.widget.MultiSelectionView; import io.github.muntashirakon.widget.RecyclerView; public class FinderActivity extends BaseActivity implements EditFiltersDialogFragment.OnSaveDialogButtonInterface { private FinderViewModel mViewModel; private LinearProgressIndicator mProgress; private RecyclerView mRecyclerView; private FinderAdapter mAdapter; private FloatingActionButton mFilterBtn; private MultiSelectionView mMultiSelectionView; @Override protected void onAuthenticated(@Nullable Bundle savedInstanceState) { setContentView(R.layout.activity_finder); setSupportActionBar(findViewById(R.id.toolbar)); mViewModel = new ViewModelProvider(this).get(FinderViewModel.class); mProgress = findViewById(R.id.progress_linear); mRecyclerView = findViewById(R.id.item_list); mFilterBtn = findViewById(R.id.floatingActionButton); mMultiSelectionView = findViewById(R.id.selection_view); UiUtils.applyWindowInsetsAsMargin(mFilterBtn); mAdapter = new FinderAdapter(); mRecyclerView.setLayoutManager(UIUtils.getGridLayoutAt450Dp(this)); mRecyclerView.setAdapter(mAdapter); mMultiSelectionView.hide(); mFilterBtn.setOnClickListener(v -> showFiltersDialog()); // Watch livedata mViewModel.getFilteredAppListLiveData().observe(this, list -> { ProgressIndicatorCompat.setVisibility(mProgress, false); mAdapter.setDefaultList(list); }); mViewModel.getLastUpdateTimeLiveData().observe(this, time -> { CharSequence subtitle; // TODO: 8/2/24 Set subtitle to "Loaded at: {time}" localised if (time < 0) { subtitle = getString(R.string.loading); } else subtitle = "Loaded at: " + DateUtils.formatDateTime(this, time); Optional.ofNullable(getSupportActionBar()).ifPresent(actionBar -> actionBar.setSubtitle(subtitle)); }); mViewModel.loadFilteredAppList(true); } private void showFiltersDialog() { EditFiltersDialogFragment dialog = new EditFiltersDialogFragment(); dialog.setOnSaveDialogButtonInterface(this); dialog.show(getSupportFragmentManager(), EditFiltersDialogFragment.TAG); } @NonNull @Override public FilterItem getFilterItem() { return mViewModel.getFilterItem(); } @Override public void onItemAltered(@NonNull FilterItem item) { mViewModel.loadFilteredAppList(false); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/filters/FinderAdapter.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.filters; import android.graphics.Color; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.UiThread; import androidx.appcompat.widget.AppCompatImageView; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.card.MaterialCardView; import com.google.android.material.materialswitch.MaterialSwitch; import com.google.android.material.textview.MaterialTextView; import java.util.ArrayList; import java.util.List; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.self.imagecache.ImageLoader; import io.github.muntashirakon.util.AdapterUtils; public class FinderAdapter extends RecyclerView.Adapter { private final List> mAdapterList = new ArrayList<>(); @UiThread public void setDefaultList(List> list) { synchronized (mAdapterList) { AdapterUtils.notifyDataSetChanged(this, mAdapterList, list); } } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_finder, parent, false); return new ViewHolder(v); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { FilterItem.FilteredItemInfo itemInfo; synchronized (mAdapterList) { itemInfo = mAdapterList.get(position); } FilterableAppInfo appInfo = itemInfo.info; ImageLoader.getInstance().displayImage(appInfo.getPackageName(), appInfo.getApplicationInfo(), holder.icon); holder.label.setText(appInfo.getAppLabel()); holder.pkg.setText(appInfo.getPackageName()); // TODO: 8/2/24 Add highlighted extras holder.item1.setVisibility(View.GONE); holder.item2.setVisibility(View.GONE); holder.item3.setVisibility(View.GONE); holder.toggleBtn.setVisibility(View.GONE); holder.itemView.setStrokeColor(Color.TRANSPARENT); } @Override public int getItemCount() { synchronized (mAdapterList) { return mAdapterList.size(); } } public static class ViewHolder extends RecyclerView.ViewHolder { public MaterialCardView itemView; public AppCompatImageView icon; public MaterialTextView label; public MaterialTextView pkg; public MaterialTextView item1; public MaterialTextView item2; public MaterialTextView item3; public MaterialSwitch toggleBtn; public ViewHolder(@NonNull View itemView) { super(itemView); this.itemView = (MaterialCardView) itemView; icon = itemView.findViewById(R.id.icon); label = itemView.findViewById(R.id.label); pkg = itemView.findViewById(R.id.package_name); item1 = itemView.findViewById(R.id.item1); item2 = itemView.findViewById(R.id.item2); item3 = itemView.findViewById(R.id.item3); toggleBtn = itemView.findViewById(R.id.toggle_button); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/filters/FinderFilterAdapter.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.filters; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.button.MaterialButton; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.filters.options.FilterOption; import io.github.muntashirakon.util.AdapterUtils; // Copyright 2012 Nolan Lawson public class FinderFilterAdapter extends RecyclerView.Adapter { private OnClickListener mClickListener; @NonNull private final FilterItem mFilterItem; public void setOnItemClickListener(OnClickListener listener) { mClickListener = listener; } public FinderFilterAdapter(@NonNull FilterItem filterItem) { mFilterItem = filterItem; } public void add(@NonNull FilterOption filter) { int position = mFilterItem.addFilterOption(filter); if (position >= 0) { notifyItemInserted(position); } } public void update(int position, @NonNull FilterOption filter) { mFilterItem.updateFilterOptionAt(position, filter); notifyItemChanged(position, AdapterUtils.STUB); } public void remove(int position, int id) { FilterOption filterOption = mFilterItem.getFilterOptionAt(position); if (filterOption.id == id && mFilterItem.removeFilterOptionAt(position)) { notifyItemRemoved(position); } } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_icon_title_subtitle, parent, false); return new ViewHolder(v); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { final FilterOption filterOption = mFilterItem.getFilterOptionAt(position); holder.titleView.setText(filterOption.getFullId()); holder.subtitleView.setText(filterOption.toLocalizedString(holder.itemView.getContext())); holder.itemView.setOnClickListener(v -> { if (mClickListener != null) { mClickListener.onEdit(holder.itemView, holder.getAbsoluteAdapterPosition(), filterOption); } }); holder.actionButton.setOnClickListener(v -> { if (mClickListener != null) { mClickListener.onRemove(holder.itemView, holder.getAbsoluteAdapterPosition(), filterOption); } }); } @Override public int getItemCount() { return mFilterItem.getSize(); } public interface OnClickListener { void onEdit(View view, int position, FilterOption filterOption); void onRemove(View view, int position, FilterOption filterOption); } public static class ViewHolder extends RecyclerView.ViewHolder { TextView titleView; TextView subtitleView; MaterialButton actionButton; public ViewHolder(View itemView) { super(itemView); itemView.findViewById(R.id.item_icon).setVisibility(View.GONE); titleView = itemView.findViewById(R.id.item_title); subtitleView = itemView.findViewById(R.id.item_subtitle); actionButton = itemView.findViewById(R.id.item_open); actionButton.setIcon(ContextCompat.getDrawable(itemView.getContext(), io.github.muntashirakon.ui.R.drawable.ic_clear)); actionButton.setContentDescription(itemView.getContext().getString(R.string.item_remove)); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/filters/FinderViewModel.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.filters; import android.app.Application; import android.os.UserHandleHidden; import androidx.annotation.WorkerThread; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.MutableLiveData; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.List; import java.util.concurrent.Future; import io.github.muntashirakon.AppManager.utils.ThreadUtils; public class FinderViewModel extends AndroidViewModel { public static final String TAG = FinderViewModel.class.getSimpleName(); private final MutableLiveData mLastUpdateTimeLiveData = new MutableLiveData<>(); private final MutableLiveData>> mFilteredAppListLiveData = new MutableLiveData<>(); private Future mAppListLoaderFuture; @Nullable private List mFilterableAppInfoList; @NotNull private final FilterItem mFilterItem = new FilterItem(); public FinderViewModel(@NotNull Application application) { super(application); } @Override protected void onCleared() { super.onCleared(); } public MutableLiveData getLastUpdateTimeLiveData() { return mLastUpdateTimeLiveData; } public MutableLiveData>> getFilteredAppListLiveData() { return mFilteredAppListLiveData; } public FilterItem getFilterItem() { return mFilterItem; } public void loadFilteredAppList(boolean refresh) { if (mAppListLoaderFuture != null) { mAppListLoaderFuture.cancel(true); } mAppListLoaderFuture = ThreadUtils.postOnBackgroundThread(() -> { mLastUpdateTimeLiveData.postValue(-1L); if (mFilterableAppInfoList == null || refresh) { loadAppList(); } if (ThreadUtils.isInterrupted() || mFilterableAppInfoList == null) return; mFilteredAppListLiveData.postValue(mFilterItem.getFilteredList(mFilterableAppInfoList)); mLastUpdateTimeLiveData.postValue(System.currentTimeMillis()); }); } @WorkerThread private void loadAppList() { // TODO: 8/2/24 Allow multiple users // TODO: 8/2/24 Include backups for uninstalled apps int[] userIds = new int[]{UserHandleHidden.myUserId()}; //Users.getUsersIds(); mFilterableAppInfoList = FilteringUtils.loadFilterableAppInfo(userIds); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/filters/IFilterableAppInfo.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.filters; import android.content.pm.ComponentInfo; import android.content.pm.FeatureInfo; import android.graphics.drawable.Drawable; import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import java.util.List; import java.util.Map; import io.github.muntashirakon.AppManager.apk.signing.SignerInfo; import io.github.muntashirakon.AppManager.compat.AppOpsManagerCompat; import io.github.muntashirakon.AppManager.compat.InstallSourceInfoCompat; import io.github.muntashirakon.AppManager.db.entity.Backup; import io.github.muntashirakon.AppManager.debloat.DebloatObject; import io.github.muntashirakon.AppManager.usage.AppUsageStatsManager; public interface IFilterableAppInfo { @NonNull String getPackageName(); int getUserId(); int getUid(); @NonNull String getAppLabel(); @NonNull Drawable getAppIcon(); @Nullable String getVersionName(); long getVersionCode(); long getFirstInstallTime(); long getLastUpdateTime(); int getTargetSdk(); @RequiresApi(Build.VERSION_CODES.S) int getCompileSdk(); @RequiresApi(Build.VERSION_CODES.N) int getMinSdk(); @NonNull Backup[] getBackups(); boolean isRunning(); @NonNull Map getTrackerComponents(); @NonNull List getAppOps(); @NonNull Map getAllComponents(); @NonNull List getAllPermissions(); @NonNull FeatureInfo[] getAllRequestedFeatures(); boolean isInstalled(); boolean isFrozen(); int getFreezeFlags(); boolean isStopped(); boolean isTestOnly(); boolean isDebuggable(); boolean isSystemApp(); boolean hasCode(); boolean isPersistent(); boolean isUpdatedSystemApp(); boolean backupAllowed(); boolean installedInExternalStorage(); boolean requestedLargeHeap(); boolean supportsRTL(); boolean dataOnlyApp(); @RequiresApi(Build.VERSION_CODES.M) boolean usesHttp(); boolean isPrivileged(); @RequiresApi(Build.VERSION_CODES.P) boolean usesSensors(); @RequiresApi(Build.VERSION_CODES.M) boolean isBatteryOptEnabled(); boolean hasKeyStoreItems(); int getRuleCount(); @Nullable String getSsaid(); boolean hasDomainUrls(); boolean hasStaticSharedLibrary(); boolean isHidden(); boolean isSuspended(); boolean isEnabled(); @Nullable String getSharedUserId(); long getTotalSize(); long getApkSize(); long getCacheSize(); long getDataSize(); @NonNull AppUsageStatsManager.DataUsage getDataUsage(); int getTimesOpened(); long getTotalScreenTime(); long getLastUsedTime(); @Nullable SignerInfo fetchSignerInfo(); @NonNull String[] getSignatureSubjectLines(); @NonNull String[] getSignatureSha256Checksums(); @Nullable InstallSourceInfoCompat getInstallerInfo(); @Nullable DebloatObject getBloatwareInfo(); } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/filters/options/ApkSizeOption.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.filters.options; import android.content.Context; import android.text.SpannableStringBuilder; import android.text.format.Formatter; import androidx.annotation.NonNull; import java.util.LinkedHashMap; import java.util.Map; import io.github.muntashirakon.AppManager.filters.IFilterableAppInfo; import io.github.muntashirakon.AppManager.utils.LangUtils; public class ApkSizeOption extends FilterOption { private final Map mKeysWithType = new LinkedHashMap() {{ put(KEY_ALL, TYPE_NONE); put("eq", TYPE_SIZE_BYTES); put("le", TYPE_SIZE_BYTES); put("ge", TYPE_SIZE_BYTES); }}; public ApkSizeOption() { super("apk_size"); } @NonNull @Override public Map getKeysWithType() { return mKeysWithType; } @NonNull @Override public TestResult test(@NonNull IFilterableAppInfo info, @NonNull TestResult result) { switch (key) { case KEY_ALL: return result.setMatched(true); case "eq": return result.setMatched(info.getApkSize() == longValue); case "le": return result.setMatched(info.getApkSize() <= longValue); case "ge": return result.setMatched(info.getApkSize() >= longValue); default: throw new UnsupportedOperationException("Invalid key " + key); } } @NonNull @Override public CharSequence toLocalizedString(@NonNull Context context) { SpannableStringBuilder sb = new SpannableStringBuilder("APK size"); switch (key) { case KEY_ALL: return sb.append(LangUtils.getSeparatorString()).append("any"); case "eq": return sb.append(" = ").append(Formatter.formatFileSize(context, longValue)); case "le": return sb.append(" ≤ ").append(Formatter.formatFileSize(context, longValue)); case "ge": return sb.append(" ≥ ").append(Formatter.formatFileSize(context, longValue)); default: throw new UnsupportedOperationException("Invalid key " + key); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/filters/options/AppLabelOption.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.filters.options; import android.content.Context; import android.text.SpannableStringBuilder; import androidx.annotation.NonNull; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; import io.github.muntashirakon.AppManager.filters.IFilterableAppInfo; import io.github.muntashirakon.AppManager.utils.LangUtils; public class AppLabelOption extends FilterOption { private final Map mKeysWithType = new LinkedHashMap() {{ put(KEY_ALL, TYPE_NONE); put("eq", TYPE_STR_SINGLE); put("contains", TYPE_STR_SINGLE); put("starts_with", TYPE_STR_SINGLE); put("ends_with", TYPE_STR_SINGLE); put("regex", TYPE_REGEX); }}; public AppLabelOption() { super("app_label"); } @NonNull @Override public Map getKeysWithType() { return mKeysWithType; } @NonNull @Override public TestResult test(@NonNull IFilterableAppInfo info, @NonNull TestResult result) { switch (key) { case KEY_ALL: return result.setMatched(true); case "eq": return result.setMatched(info.getAppLabel().equals(Objects.requireNonNull(value))); case "contains": return result.setMatched(info.getAppLabel().contains(Objects.requireNonNull(value))); case "starts_with": return result.setMatched(info.getAppLabel().startsWith(Objects.requireNonNull(value))); case "ends_with": return result.setMatched(info.getAppLabel().endsWith(Objects.requireNonNull(value))); case "regex": return result.setMatched(regexValue.matcher(info.getAppLabel()).matches()); default: throw new UnsupportedOperationException("Invalid key " + key); } } @NonNull @Override public CharSequence toLocalizedString(@NonNull Context context) { SpannableStringBuilder sb = new SpannableStringBuilder("App label"); switch (key) { case KEY_ALL: return sb.append(LangUtils.getSeparatorString()).append("any"); case "eq": return sb.append(" = '").append(value).append("'"); case "contains": return sb.append(" contains '").append(value).append("'"); case "starts_with": return sb.append(" starts with '").append(value).append("'"); case "ends_with": return sb.append(" ends with '").append(value).append("'"); case "regex": return sb.append(" matches '").append(value).append("'"); default: throw new UnsupportedOperationException("Invalid key " + key); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/filters/options/AppTypeOption.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.filters.options; import android.content.Context; import android.os.Build; import android.text.SpannableStringBuilder; import android.text.TextUtils; import androidx.annotation.NonNull; import java.util.LinkedHashMap; import java.util.Map; import io.github.muntashirakon.AppManager.compat.ManifestCompat; import io.github.muntashirakon.AppManager.filters.IFilterableAppInfo; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.utils.LangUtils; public class AppTypeOption extends FilterOption { public static final int APP_TYPE_USER = 1 << 0; public static final int APP_TYPE_SYSTEM = 1 << 1; public static final int APP_TYPE_UPDATED_SYSTEM = 1 << 2; public static final int APP_TYPE_PRIVILEGED = 1 << 3; public static final int APP_TYPE_DATA_ONLY = 1 << 4; public static final int APP_TYPE_STOPPED = 1 << 5; public static final int APP_TYPE_SENSORS = 1 << 6; public static final int APP_TYPE_LARGE_HEAP = 1 << 7; public static final int APP_TYPE_DEBUGGABLE = 1 << 8; public static final int APP_TYPE_TEST_ONLY = 1 << 9; public static final int APP_TYPE_HAS_CODE = 1 << 10; public static final int APP_TYPE_PERSISTENT = 1 << 11; public static final int APP_TYPE_ALLOW_BACKUP = 1 << 12; public static final int APP_TYPE_INSTALLED_IN_EXTERNAL = 1 << 13; public static final int APP_TYPE_HTTP_ONLY = 1 << 14; public static final int APP_TYPE_BATTERY_OPT_ENABLED = 1 << 15; public static final int APP_TYPE_PLAY_APP_SIGNING = 1 << 16; public static final int APP_TYPE_SSAID = 1 << 17; public static final int APP_TYPE_KEYSTORE = 1 << 18; public static final int APP_TYPE_WITH_RULES = 1 << 19; public static final int APP_TYPE_PWA = 1 << 20; public static final int APP_TYPE_SHORT_CODE = 1 << 21; public static final int APP_TYPE_OVERLAY = 1 << 22; private final Map mKeysWithType = new LinkedHashMap() {{ put(KEY_ALL, TYPE_NONE); put("with_flags", TYPE_INT_FLAGS); put("without_flags", TYPE_INT_FLAGS); }}; private final Map mFrozenFlags = new LinkedHashMap() {{ put(APP_TYPE_USER, "User app"); put(APP_TYPE_SYSTEM, "System app"); put(APP_TYPE_UPDATED_SYSTEM, "Updated system app"); put(APP_TYPE_PRIVILEGED, "Privileged app"); put(APP_TYPE_DATA_ONLY, "Data-only app"); put(APP_TYPE_STOPPED, "Force-stopped app"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.MANAGE_SENSORS)) { put(APP_TYPE_SENSORS, "Uses sensors"); } put(APP_TYPE_LARGE_HEAP, "Requests large heap"); put(APP_TYPE_DEBUGGABLE, "Debuggable app"); put(APP_TYPE_TEST_ONLY, "Test-only app"); put(APP_TYPE_HAS_CODE, "Has code"); put(APP_TYPE_PERSISTENT, "Persistent app"); put(APP_TYPE_ALLOW_BACKUP, "Backup allowed"); put(APP_TYPE_INSTALLED_IN_EXTERNAL, "Installed in external storage"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { put(APP_TYPE_HTTP_ONLY, "Uses cleartext (HTTP) traffic"); put(APP_TYPE_BATTERY_OPT_ENABLED, "Battery optimized"); } // put(APP_TYPE_PLAY_APP_SIGNING, "Uses Play App Signing"); // TODO: 11/21/24 put(APP_TYPE_SSAID, "Has SSAID"); put(APP_TYPE_KEYSTORE, "Uses Android KeyStore"); put(APP_TYPE_WITH_RULES, "Has rules"); // put(APP_TYPE_PWA, "Progressive web app (PWA)"); // TODO: 11/21/24 // put(APP_TYPE_SHORT_CODE, "Uses short code"); // TODO: 11/21/24 // put(APP_TYPE_OVERLAY, "Overlay app"); // TODO: 11/21/24 }}; public AppTypeOption() { super("app_type"); } @NonNull @Override public Map getKeysWithType() { return mKeysWithType; } @Override public Map getFlags(@NonNull String key) { if (key.equals("with_flags") || key.equals("without_flags")) { return mFrozenFlags; } return super.getFlags(key); } @NonNull @Override public TestResult test(@NonNull IFilterableAppInfo info, @NonNull TestResult result) { switch (key) { case KEY_ALL: return result.setMatched(true); case "with_flags": return result.setMatched(withFlagsCheck(info, intValue)); case "without_flags": return result.setMatched(withoutFlagsCheck(info, intValue)); default: throw new UnsupportedOperationException("Invalid key " + key); } } @NonNull @Override public CharSequence toLocalizedString(@NonNull Context context) { SpannableStringBuilder sb = new SpannableStringBuilder("Apps"); switch (key) { case KEY_ALL: return sb.append(LangUtils.getSeparatorString()).append("any"); case "with_flags": return sb.append(" with flags: ").append(flagsToString("with_flags", intValue)); case "without_flags": return sb.append(" without flags: ").append(flagsToString("without_flags", intValue)); default: throw new UnsupportedOperationException("Invalid key " + key); } } /** * Returns true if the given info matches all the flags in 'flag'. * Only checks those flags that are set in 'flag' to minimize expensive calls. */ public static boolean withFlagsCheck(IFilterableAppInfo info, int flag) { if ((flag & AppTypeOption.APP_TYPE_SYSTEM) != 0) { if (!info.isSystemApp()) return false; } if ((flag & AppTypeOption.APP_TYPE_USER) != 0) { if (info.isSystemApp()) return false; } if ((flag & AppTypeOption.APP_TYPE_UPDATED_SYSTEM) != 0) { if (!info.isUpdatedSystemApp()) return false; } if ((flag & AppTypeOption.APP_TYPE_PRIVILEGED) != 0) { if (!info.isPrivileged()) return false; } if ((flag & AppTypeOption.APP_TYPE_DATA_ONLY) != 0) { if (!info.dataOnlyApp()) return false; } if ((flag & AppTypeOption.APP_TYPE_STOPPED) != 0) { if (!info.isStopped()) return false; } if ((flag & AppTypeOption.APP_TYPE_LARGE_HEAP) != 0) { if (!info.requestedLargeHeap()) return false; } if ((flag & AppTypeOption.APP_TYPE_DEBUGGABLE) != 0) { if (!info.isDebuggable()) return false; } if ((flag & AppTypeOption.APP_TYPE_TEST_ONLY) != 0) { if (!info.isTestOnly()) return false; } if ((flag & AppTypeOption.APP_TYPE_HAS_CODE) != 0) { if (!info.hasCode()) return false; } if ((flag & AppTypeOption.APP_TYPE_PERSISTENT) != 0) { if (!info.isPersistent()) return false; } if ((flag & AppTypeOption.APP_TYPE_ALLOW_BACKUP) != 0) { if (!info.backupAllowed()) return false; } if ((flag & AppTypeOption.APP_TYPE_INSTALLED_IN_EXTERNAL) != 0) { if (!info.installedInExternalStorage()) return false; } if ((flag & AppTypeOption.APP_TYPE_HTTP_ONLY) != 0) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (!info.usesHttp()) return false; } // On older versions, this is always true } if ((flag & AppTypeOption.APP_TYPE_BATTERY_OPT_ENABLED) != 0) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (!info.isBatteryOptEnabled()) return false; } // On older versions, this is always true } if ((flag & AppTypeOption.APP_TYPE_SENSORS) != 0) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (!info.usesSensors()) return false; } // On older versions, this is always true } if ((flag & AppTypeOption.APP_TYPE_WITH_RULES) != 0) { if (info.getRuleCount() == 0) return false; } if ((flag & AppTypeOption.APP_TYPE_KEYSTORE) != 0) { if (!info.hasKeyStoreItems()) return false; } if ((flag & AppTypeOption.APP_TYPE_SSAID) != 0) { if (TextUtils.isEmpty(info.getSsaid())) return false; } // All requested flags are matched return true; } public static boolean withoutFlagsCheck(IFilterableAppInfo info, int flag) { if ((flag & AppTypeOption.APP_TYPE_SYSTEM) != 0) { if (!info.isSystemApp()) return true; } if ((flag & AppTypeOption.APP_TYPE_USER) != 0) { if (info.isSystemApp()) return true; } if ((flag & AppTypeOption.APP_TYPE_UPDATED_SYSTEM) != 0) { if (!info.isUpdatedSystemApp()) return true; } if ((flag & AppTypeOption.APP_TYPE_PRIVILEGED) != 0) { if (!info.isPrivileged()) return true; } if ((flag & AppTypeOption.APP_TYPE_DATA_ONLY) != 0) { if (!info.dataOnlyApp()) return true; } if ((flag & AppTypeOption.APP_TYPE_STOPPED) != 0) { if (!info.isStopped()) return true; } if ((flag & AppTypeOption.APP_TYPE_LARGE_HEAP) != 0) { if (!info.requestedLargeHeap()) return true; } if ((flag & AppTypeOption.APP_TYPE_DEBUGGABLE) != 0) { if (!info.isDebuggable()) return true; } if ((flag & AppTypeOption.APP_TYPE_TEST_ONLY) != 0) { if (!info.isTestOnly()) return true; } if ((flag & AppTypeOption.APP_TYPE_HAS_CODE) != 0) { if (!info.hasCode()) return true; } if ((flag & AppTypeOption.APP_TYPE_PERSISTENT) != 0) { if (!info.isPersistent()) return true; } if ((flag & AppTypeOption.APP_TYPE_ALLOW_BACKUP) != 0) { if (!info.backupAllowed()) return true; } if ((flag & AppTypeOption.APP_TYPE_INSTALLED_IN_EXTERNAL) != 0) { if (!info.installedInExternalStorage()) return true; } if ((flag & AppTypeOption.APP_TYPE_HTTP_ONLY) != 0) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (!info.usesHttp()) return true; } // On older versions, this is always false } if ((flag & AppTypeOption.APP_TYPE_BATTERY_OPT_ENABLED) != 0) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (!info.isBatteryOptEnabled()) return true; } // On older versions, this is always false } if ((flag & AppTypeOption.APP_TYPE_SENSORS) != 0) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (!info.usesSensors()) return true; } // On older versions, this is always false } if ((flag & AppTypeOption.APP_TYPE_WITH_RULES) != 0) { if (info.getRuleCount() == 0) return true; } if ((flag & AppTypeOption.APP_TYPE_KEYSTORE) != 0) { if (!info.hasKeyStoreItems()) return true; } if ((flag & AppTypeOption.APP_TYPE_SSAID) != 0) { if (TextUtils.isEmpty(info.getSsaid())) return true; } // All requested flags are present, so "not all flags missing" return false; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/filters/options/BackupOption.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.filters.options; import static io.github.muntashirakon.AppManager.backup.BackupFlags.BACKUP_ADB_DATA; import static io.github.muntashirakon.AppManager.backup.BackupFlags.BACKUP_APK_FILES; import static io.github.muntashirakon.AppManager.backup.BackupFlags.BACKUP_CACHE; import static io.github.muntashirakon.AppManager.backup.BackupFlags.BACKUP_EXTRAS; import static io.github.muntashirakon.AppManager.backup.BackupFlags.BACKUP_EXT_DATA; import static io.github.muntashirakon.AppManager.backup.BackupFlags.BACKUP_EXT_OBB_MEDIA; import static io.github.muntashirakon.AppManager.backup.BackupFlags.BACKUP_INT_DATA; import static io.github.muntashirakon.AppManager.backup.BackupFlags.BACKUP_RULES; import android.content.Context; import android.text.SpannableStringBuilder; import androidx.annotation.NonNull; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import io.github.muntashirakon.AppManager.db.entity.Backup; import io.github.muntashirakon.AppManager.filters.IFilterableAppInfo; import io.github.muntashirakon.AppManager.utils.DateUtils; public class BackupOption extends FilterOption { private final Map mKeysWithType = new LinkedHashMap() {{ put(KEY_ALL, TYPE_NONE); put("backups", TYPE_NONE); put("no_backups", TYPE_NONE); put("latest_backup", TYPE_NONE); put("outdated_backup", TYPE_NONE); put("made_before", TYPE_TIME_MILLIS); put("made_after", TYPE_TIME_MILLIS); put("with_flags", TYPE_INT_FLAGS); put("without_flags", TYPE_INT_FLAGS); }}; private final Map mBackupFlags = new LinkedHashMap() {{ put(BACKUP_APK_FILES, "Apk files"); put(BACKUP_INT_DATA, "Internal data"); put(BACKUP_EXT_DATA, "External data"); put(BACKUP_ADB_DATA, "ADB data"); put(BACKUP_EXT_OBB_MEDIA, "OBB and media"); put(BACKUP_CACHE, "Cache"); put(BACKUP_EXTRAS, "Extras"); put(BACKUP_RULES, "Rules"); }}; public BackupOption() { super("backup"); } @NonNull @Override public Map getKeysWithType() { return mKeysWithType; } @Override public Map getFlags(@NonNull String key) { if (key.equals("with_flags") || key.equals("without_flags")) { return mBackupFlags; } return super.getFlags(key); } @NonNull @Override public TestResult test(@NonNull IFilterableAppInfo info, @NonNull TestResult result) { List backups = result.getMatchedBackups() != null ? result.getMatchedBackups() : Arrays.asList(info.getBackups()); switch (key) { case KEY_ALL: return result.setMatched(true).setMatchedBackups(backups); case "backups": { if (!backups.isEmpty()) { return result.setMatched(true).setMatchedBackups(backups); } else return result.setMatched(false).setMatchedBackups(Collections.emptyList()); } case "no_backups": { if (backups.isEmpty()) { return result.setMatched(true).setMatchedBackups(Collections.emptyList()); } else return result.setMatched(false).setMatchedBackups(backups); } case "latest_backup": { if (!info.isInstalled()) { // If the app isn't install, all backups are latest return result.setMatched(true).setMatchedBackups(backups); } List matchedBackups = new ArrayList<>(); long versionCode = info.getVersionCode(); for (Backup backup : backups) { if (backup.versionCode >= versionCode) { matchedBackups.add(backup); } } return result.setMatched(!matchedBackups.isEmpty()) .setMatchedBackups(matchedBackups); } case "outdated_backup": { if (!info.isInstalled()) { // If the app isn't install, no backups are outdated return result.setMatched(false); } List matchedBackups = new ArrayList<>(); long versionCode = info.getVersionCode(); for (Backup backup : backups) { if (backup.versionCode < versionCode) { matchedBackups.add(backup); } } return result.setMatched(!matchedBackups.isEmpty()) .setMatchedBackups(matchedBackups); } case "made_before": { List matchedBackups = new ArrayList<>(); for (Backup backup : backups) { if (backup.backupTime <= longValue) { matchedBackups.add(backup); } } return result.setMatched(!matchedBackups.isEmpty()) .setMatchedBackups(matchedBackups); } case "made_after": { List matchedBackups = new ArrayList<>(); for (Backup backup : backups) { if (backup.backupTime >= longValue) { matchedBackups.add(backup); } } return result.setMatched(!matchedBackups.isEmpty()) .setMatchedBackups(matchedBackups); } case "with_flags": { List matchedBackups = new ArrayList<>(); for (Backup backup : backups) { if ((backup.flags & intValue) == intValue) { matchedBackups.add(backup); } } return result.setMatched(!matchedBackups.isEmpty()) .setMatchedBackups(matchedBackups); } case "without_flags": { List matchedBackups = new ArrayList<>(); for (Backup backup : backups) { if ((backup.flags & intValue) != intValue) { matchedBackups.add(backup); } } return result.setMatched(!matchedBackups.isEmpty()) .setMatchedBackups(matchedBackups); } default: throw new UnsupportedOperationException("Invalid key " + key); } } @NonNull @Override public CharSequence toLocalizedString(@NonNull Context context) { SpannableStringBuilder sb = new SpannableStringBuilder(); switch (key) { case KEY_ALL: return "Apps with or without backups"; case "backups": return "Only the apps with backups"; case "no_backups": return "only the apps without backups"; case "latest_backup": return "Only the apps having the latest backups"; case "outdated_backup": return "Only the apps having some outdated backups"; case "made_before": return sb.append("Only the apps with backups made before ").append(DateUtils.formatDate(context, longValue)); case "made_after": return sb.append("Only the apps with backups made after ").append(DateUtils.formatDate(context, longValue)); case "with_flags": return sb.append("Only the apps having backups with the flags ").append(flagsToString("with_flags", intValue)); case "without_flags": return sb.append("Only the apps having backups without the flags ").append(flagsToString("without_flags", intValue)); default: throw new UnsupportedOperationException("Invalid key " + key); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/filters/options/BloatwareOption.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.filters.options; import static io.github.muntashirakon.AppManager.debloat.DebloatObject.REMOVAL_CAUTION; import static io.github.muntashirakon.AppManager.debloat.DebloatObject.REMOVAL_REPLACE; import static io.github.muntashirakon.AppManager.debloat.DebloatObject.REMOVAL_SAFE; import static io.github.muntashirakon.AppManager.debloat.DebloatObject.REMOVAL_UNSAFE; import static io.github.muntashirakon.AppManager.debloat.DebloaterListOptions.FILTER_LIST_AOSP; import static io.github.muntashirakon.AppManager.debloat.DebloaterListOptions.FILTER_LIST_CARRIER; import static io.github.muntashirakon.AppManager.debloat.DebloaterListOptions.FILTER_LIST_GOOGLE; import static io.github.muntashirakon.AppManager.debloat.DebloaterListOptions.FILTER_LIST_MISC; import static io.github.muntashirakon.AppManager.debloat.DebloaterListOptions.FILTER_LIST_OEM; import android.content.Context; import android.text.SpannableStringBuilder; import androidx.annotation.NonNull; import java.util.LinkedHashMap; import java.util.Map; import io.github.muntashirakon.AppManager.debloat.DebloatObject; import io.github.muntashirakon.AppManager.filters.IFilterableAppInfo; import io.github.muntashirakon.AppManager.utils.LangUtils; public class BloatwareOption extends FilterOption { private final Map mKeysWithType = new LinkedHashMap() {{ put(KEY_ALL, TYPE_NONE); put("type", TYPE_INT_FLAGS); put("removal", TYPE_INT_FLAGS); }}; private final Map mBloatwareTypeFlags = new LinkedHashMap() {{ put(FILTER_LIST_AOSP, "AOSP"); put(FILTER_LIST_CARRIER, "Carrier"); put(FILTER_LIST_GOOGLE, "Google"); put(FILTER_LIST_MISC, "Misc"); put(FILTER_LIST_OEM, "OEM"); }}; private final Map mRemovalFlags = new LinkedHashMap() {{ put(REMOVAL_SAFE, "Safe"); put(REMOVAL_REPLACE, "Replace"); put(REMOVAL_CAUTION, "Caution"); put(REMOVAL_UNSAFE, "Unsafe"); }}; public BloatwareOption() { super("bloatware"); } @NonNull @Override public Map getKeysWithType() { return mKeysWithType; } @Override public Map getFlags(@NonNull String key) { if (key.equals("type")) { return mBloatwareTypeFlags; } else if (key.equals("removal")) { return mRemovalFlags; } return super.getFlags(key); } @NonNull @Override public TestResult test(@NonNull IFilterableAppInfo info, @NonNull TestResult result) { DebloatObject object = info.getBloatwareInfo(); if (object == null) { return result.setMatched(false); } // Must be a bloatware switch (key) { case KEY_ALL: return result.setMatched(true); case "type": return result.setMatched((typeToFlag(object.type) & intValue) != 0); case "removal": return result.setMatched((object.getRemoval() & intValue) != 0); default: throw new UnsupportedOperationException("Invalid key " + key); } } public int typeToFlag(@NonNull String type) { switch (type) { case "aosp": return FILTER_LIST_AOSP; case "carrier": return FILTER_LIST_CARRIER; case "google": return FILTER_LIST_GOOGLE; case "misc": return FILTER_LIST_MISC; case "oem": return FILTER_LIST_OEM; default: throw new IllegalArgumentException("Unknown type: " + type); } } @NonNull @Override public CharSequence toLocalizedString(@NonNull Context context) { SpannableStringBuilder sb = new SpannableStringBuilder("Bloatware"); switch (key) { case KEY_ALL: return sb.append(LangUtils.getSeparatorString()).append("any"); case "type": return sb.append(" with type: ").append(flagsToString("type", intValue)); case "removal": return sb.append(" with removal: ").append(flagsToString("removal", intValue)); default: throw new UnsupportedOperationException("Invalid key " + key); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/filters/options/CacheSizeOption.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.filters.options; import android.content.Context; import android.text.SpannableStringBuilder; import android.text.format.Formatter; import androidx.annotation.NonNull; import java.util.LinkedHashMap; import java.util.Map; import io.github.muntashirakon.AppManager.filters.IFilterableAppInfo; import io.github.muntashirakon.AppManager.utils.LangUtils; public class CacheSizeOption extends FilterOption { private final Map mKeysWithType = new LinkedHashMap() {{ put(KEY_ALL, TYPE_NONE); put("eq", TYPE_SIZE_BYTES); put("le", TYPE_SIZE_BYTES); put("ge", TYPE_SIZE_BYTES); }}; public CacheSizeOption() { super("cache_size"); } @NonNull @Override public Map getKeysWithType() { return mKeysWithType; } @NonNull @Override public TestResult test(@NonNull IFilterableAppInfo info, @NonNull TestResult result) { switch (key) { case KEY_ALL: return result.setMatched(true); case "eq": return result.setMatched(info.getCacheSize() == longValue); case "le": return result.setMatched(info.getCacheSize() <= longValue); case "ge": return result.setMatched(info.getCacheSize() >= longValue); default: throw new UnsupportedOperationException("Invalid key " + key); } } @NonNull @Override public CharSequence toLocalizedString(@NonNull Context context) { SpannableStringBuilder sb = new SpannableStringBuilder("Cache size"); switch (key) { case KEY_ALL: return sb.append(LangUtils.getSeparatorString()).append("any"); case "eq": return sb.append(" = ").append(Formatter.formatFileSize(context, longValue)); case "le": return sb.append(" ≤ ").append(Formatter.formatFileSize(context, longValue)); case "ge": return sb.append(" ≥ ").append(Formatter.formatFileSize(context, longValue)); default: throw new UnsupportedOperationException("Invalid key " + key); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/filters/options/CompileSdkOption.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.filters.options; import android.content.Context; import android.os.Build; import android.text.SpannableStringBuilder; import androidx.annotation.NonNull; import java.util.LinkedHashMap; import java.util.Map; import io.github.muntashirakon.AppManager.filters.IFilterableAppInfo; import io.github.muntashirakon.AppManager.utils.LangUtils; public class CompileSdkOption extends FilterOption { private final Map mKeysWithType = new LinkedHashMap() {{ put(KEY_ALL, TYPE_NONE); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { put("eq", TYPE_INT); put("le", TYPE_INT); put("ge", TYPE_INT); } }}; public CompileSdkOption() { super("compile_sdk"); } @NonNull @Override public Map getKeysWithType() { return mKeysWithType; } @NonNull @Override public TestResult test(@NonNull IFilterableAppInfo info, @NonNull TestResult result) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { return result.setMatched(true); } switch (key) { case KEY_ALL: return result.setMatched(true); case "eq": return result.setMatched(info.getCompileSdk() == intValue); case "le": return result.setMatched(info.getCompileSdk() <= intValue); case "ge": return result.setMatched(info.getCompileSdk() >= intValue); default: throw new UnsupportedOperationException("Invalid key " + key); } } @NonNull @Override public CharSequence toLocalizedString(@NonNull Context context) { SpannableStringBuilder sb = new SpannableStringBuilder("Compile SDK"); switch (key) { case KEY_ALL: return sb.append(LangUtils.getSeparatorString()).append("any"); case "eq": return sb.append(" = ").append(Integer.toString(intValue)); case "le": return sb.append(" ≤ ").append(Integer.toString(intValue)); case "ge": return sb.append(" ≥ ").append(Integer.toString(intValue)); default: throw new UnsupportedOperationException("Invalid key " + key); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/filters/options/ComponentsOption.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.filters.options; import android.content.Context; import android.content.pm.ComponentInfo; import android.text.SpannableStringBuilder; import androidx.annotation.NonNull; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; import io.github.muntashirakon.AppManager.filters.IFilterableAppInfo; import io.github.muntashirakon.AppManager.utils.LangUtils; public class ComponentsOption extends FilterOption { public static final int COMPONENT_TYPE_ACTIVITY = 1 << 0; public static final int COMPONENT_TYPE_SERVICE = 1 << 1; public static final int COMPONENT_TYPE_RECEIVER = 1 << 2; public static final int COMPONENT_TYPE_PROVIDER = 1 << 3; private final Map mKeysWithType = new LinkedHashMap() {{ put(KEY_ALL, TYPE_NONE); put("with_type", TYPE_INT_FLAGS); put("without_type", TYPE_INT_FLAGS); put("eq", TYPE_STR_SINGLE); put("contains", TYPE_STR_SINGLE); put("starts_with", TYPE_STR_SINGLE); put("ends_with", TYPE_STR_SINGLE); put("regex", TYPE_REGEX); put("count_eq", TYPE_INT); put("count_le", TYPE_INT); put("count_ge", TYPE_INT); }}; private final Map mComponentTypeFlags = new LinkedHashMap() {{ put(COMPONENT_TYPE_ACTIVITY, "Activities"); put(COMPONENT_TYPE_SERVICE, "Services"); put(COMPONENT_TYPE_RECEIVER, "Receivers"); put(COMPONENT_TYPE_PROVIDER, "Providers"); }}; public ComponentsOption() { super("components"); } @NonNull @Override public Map getKeysWithType() { return mKeysWithType; } @Override public Map getFlags(@NonNull String key) { if (key.equals("with_type") || key.equals("without_type")) { return mComponentTypeFlags; } return super.getFlags(key); } @NonNull @Override public TestResult test(@NonNull IFilterableAppInfo info, @NonNull TestResult result) { Map components = result.getMatchedComponents() != null ? result.getMatchedComponents() : info.getAllComponents(); switch (key) { case KEY_ALL: return result.setMatched(true).setMatchedComponents(components); case "with_type": { Map filteredComponents = new LinkedHashMap<>(); for (ComponentInfo component : components.keySet()) { int type = Objects.requireNonNull(components.get(component)); if ((intValue & type) != 0) { filteredComponents.put(component, type); } } return result.setMatched(!filteredComponents.isEmpty()) .setMatchedComponents(filteredComponents); } case "without_type": { Map filteredComponents = new LinkedHashMap<>(); for (ComponentInfo component : components.keySet()) { int type = Objects.requireNonNull(components.get(component)); if ((intValue & type) == 0) { filteredComponents.put(component, type); } } return result.setMatched(filteredComponents.size() == components.size()) .setMatchedComponents(filteredComponents); } case "eq": { Map filteredComponents = new LinkedHashMap<>(); for (ComponentInfo component : components.keySet()) { if (component.name.equals(value)) { filteredComponents.put(component, components.get(component)); } } return result.setMatched(!filteredComponents.isEmpty()) .setMatchedComponents(filteredComponents); } case "contains": { Objects.requireNonNull(value); Map filteredComponents = new LinkedHashMap<>(); for (ComponentInfo component : components.keySet()) { if (component.name.contains(value)) { filteredComponents.put(component, components.get(component)); } } return result.setMatched(!filteredComponents.isEmpty()) .setMatchedComponents(filteredComponents); } case "starts_with": { Objects.requireNonNull(value); Map filteredComponents = new LinkedHashMap<>(); for (ComponentInfo component : components.keySet()) { if (component.name.startsWith(value)) { filteredComponents.put(component, components.get(component)); } } return result.setMatched(!filteredComponents.isEmpty()) .setMatchedComponents(filteredComponents); } case "ends_with": { Objects.requireNonNull(value); Map filteredComponents = new LinkedHashMap<>(); for (ComponentInfo component : components.keySet()) { if (component.name.endsWith(value)) { filteredComponents.put(component, components.get(component)); } } return result.setMatched(!filteredComponents.isEmpty()) .setMatchedComponents(filteredComponents); } case "regex": { Objects.requireNonNull(value); Map filteredComponents = new LinkedHashMap<>(); for (ComponentInfo component : components.keySet()) { if (regexValue.matcher(component.name).matches()) { filteredComponents.put(component, components.get(component)); } } return result.setMatched(!filteredComponents.isEmpty()) .setMatchedComponents(filteredComponents); } case "count_eq": { return result.setMatched(components.size() == intValue) .setMatchedComponents(components); } case "count_le": { return result.setMatched(components.size() <= intValue) .setMatchedComponents(components); } case "count_ge": { return result.setMatched(components.size() >= intValue) .setMatchedComponents(components); } default: throw new UnsupportedOperationException("Invalid key " + key); } } @NonNull @Override public CharSequence toLocalizedString(@NonNull Context context) { SpannableStringBuilder sb = new SpannableStringBuilder("App components"); switch (key) { case KEY_ALL: return sb.append(LangUtils.getSeparatorString()).append("any"); case "with_type": return sb.append(" with types ").append(flagsToString("with_type", intValue)); case "without_type": return sb.append(" without types ").append(flagsToString("without_type", intValue)); case "eq": return sb.append(" = '").append(value).append("'"); case "contains": return sb.append(" contains '").append(value).append("'"); case "starts_with": return sb.append(" starts with '").append(value).append("'"); case "ends_with": return sb.append(" ends with '").append(value).append("'"); case "regex": return sb.append(" matches '").append(value).append("'"); case "count_eq": return sb.append(" count = ").append(Integer.toString(intValue)); case "count_le": return sb.append(" count ≤ ").append(Integer.toString(intValue)); case "count_ge": return sb.append(" count ≥ ").append(Integer.toString(intValue)); default: throw new UnsupportedOperationException("Invalid key " + key); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/filters/options/DataSizeOption.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.filters.options; import android.content.Context; import android.text.SpannableStringBuilder; import android.text.format.Formatter; import androidx.annotation.NonNull; import java.util.LinkedHashMap; import java.util.Map; import io.github.muntashirakon.AppManager.filters.IFilterableAppInfo; import io.github.muntashirakon.AppManager.utils.LangUtils; public class DataSizeOption extends FilterOption { private final Map mKeysWithType = new LinkedHashMap() {{ put(KEY_ALL, TYPE_NONE); put("eq", TYPE_SIZE_BYTES); put("le", TYPE_SIZE_BYTES); put("ge", TYPE_SIZE_BYTES); }}; public DataSizeOption() { super("data_size"); } @NonNull @Override public Map getKeysWithType() { return mKeysWithType; } @NonNull @Override public TestResult test(@NonNull IFilterableAppInfo info, @NonNull TestResult result) { switch (key) { case KEY_ALL: return result.setMatched(true); case "eq": return result.setMatched(info.getDataSize() == longValue); case "le": return result.setMatched(info.getDataSize() <= longValue); case "ge": return result.setMatched(info.getDataSize() >= longValue); default: throw new UnsupportedOperationException("Invalid key " + key); } } @NonNull @Override public CharSequence toLocalizedString(@NonNull Context context) { SpannableStringBuilder sb = new SpannableStringBuilder("Data size"); switch (key) { case KEY_ALL: return sb.append(LangUtils.getSeparatorString()).append("any"); case "eq": return sb.append(" = ").append(Formatter.formatFileSize(context, longValue)); case "le": return sb.append(" ≤ ").append(Formatter.formatFileSize(context, longValue)); case "ge": return sb.append(" ≥ ").append(Formatter.formatFileSize(context, longValue)); default: throw new UnsupportedOperationException("Invalid key " + key); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/filters/options/DataUsageOption.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.filters.options; import android.content.Context; import android.text.SpannableStringBuilder; import android.text.format.Formatter; import androidx.annotation.NonNull; import java.util.LinkedHashMap; import java.util.Map; import io.github.muntashirakon.AppManager.filters.IFilterableAppInfo; import io.github.muntashirakon.AppManager.utils.LangUtils; public class DataUsageOption extends FilterOption { private final Map mKeysWithType = new LinkedHashMap() {{ put(KEY_ALL, TYPE_NONE); put("eq", TYPE_SIZE_BYTES); put("le", TYPE_SIZE_BYTES); put("ge", TYPE_SIZE_BYTES); // TODO: 11/19/24 Add more curated options, e.g., mobile and wifi }}; public DataUsageOption() { super("data_usage"); } @NonNull @Override public Map getKeysWithType() { return mKeysWithType; } @NonNull @Override public TestResult test(@NonNull IFilterableAppInfo info, @NonNull TestResult result) { switch (key) { case KEY_ALL: return result.setMatched(true); case "eq": return result.setMatched(info.getDataUsage().getTotal() == longValue); case "le": return result.setMatched(info.getDataUsage().getTotal() <= longValue); case "ge": return result.setMatched(info.getDataUsage().getTotal() >= longValue); default: throw new UnsupportedOperationException("Invalid key " + key); } } @NonNull @Override public CharSequence toLocalizedString(@NonNull Context context) { SpannableStringBuilder sb = new SpannableStringBuilder("Data usage"); switch (key) { case KEY_ALL: return sb.append(LangUtils.getSeparatorString()).append("any"); case "eq": return sb.append(" = ").append(Formatter.formatFileSize(context, longValue)); case "le": return sb.append(" ≤ ").append(Formatter.formatFileSize(context, longValue)); case "ge": return sb.append(" ≥ ").append(Formatter.formatFileSize(context, longValue)); default: throw new UnsupportedOperationException("Invalid key " + key); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/filters/options/FilterOption.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.filters.options; import android.content.pm.ComponentInfo; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.CallSuper; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.json.JSONException; import org.json.JSONObject; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.regex.Pattern; import io.github.muntashirakon.AppManager.db.entity.Backup; import io.github.muntashirakon.AppManager.filters.IFilterableAppInfo; import io.github.muntashirakon.AppManager.utils.JSONUtils; import io.github.muntashirakon.util.LocalizedString; public abstract class FilterOption implements LocalizedString, Parcelable { public static final int TYPE_NONE = 0; public static final int TYPE_STR_SINGLE = 1; public static final int TYPE_STR_MULTIPLE = 2; public static final int TYPE_INT = 3; public static final int TYPE_LONG = 4; public static final int TYPE_REGEX = 5; public static final int TYPE_INT_FLAGS = 6; public static final int TYPE_TIME_MILLIS = 7; public static final int TYPE_DURATION_MILLIS = 8; public static final int TYPE_SIZE_BYTES = 9; @IntDef(value = { TYPE_NONE, TYPE_STR_SINGLE, TYPE_STR_MULTIPLE, TYPE_INT, TYPE_LONG, TYPE_REGEX, TYPE_INT_FLAGS, TYPE_TIME_MILLIS, TYPE_DURATION_MILLIS, TYPE_SIZE_BYTES, }) @Retention(RetentionPolicy.SOURCE) public @interface KeyType { } public static final String KEY_ALL = "all"; /** * Option type (e.g., target_sdk, last_update) */ public final String type; public int id; /** * A key under the option (e.g., for target_sdk: eq, le, ge, all; for last_update: before, after, all) */ @NonNull protected String key; /** * Type of the key (e.g., for target_sdk: eq, le, ge => int, all => none, for last_update: before, after => date, all => none) */ @KeyType protected int keyType; /** * Value for the key if keyType is anything but TYPE_NONE */ @Nullable protected String value; protected int intValue; protected long longValue; protected Pattern regexValue; protected String[] stringValues; public FilterOption(String type) { this.type = type; this.key = KEY_ALL; this.keyType = TYPE_NONE; } public String getFullId() { return type + "_" + id; } @NonNull public String getKey() { return key; } @KeyType public int getKeyType() { return keyType; } @Nullable public String getValue() { return value; } @CallSuper public void setKeyValue(@NonNull String key, @Nullable String value) { Integer keyType = getKeysWithType().get(key); if (keyType == null) { throw new IllegalArgumentException("Invalid key: " + key + " for type: " + type); } this.key = key; this.keyType = keyType; if (keyType != TYPE_NONE) { this.value = Objects.requireNonNull(value); switch (keyType) { case TYPE_INT: case TYPE_INT_FLAGS: this.intValue = Integer.parseInt(value); break; case TYPE_LONG: case TYPE_TIME_MILLIS: case TYPE_DURATION_MILLIS: case TYPE_SIZE_BYTES: this.longValue = Long.parseLong(value); break; case TYPE_REGEX: this.regexValue = Pattern.compile(Pattern.quote(value)); case TYPE_STR_MULTIPLE: this.stringValues = value.split("\\n"); } } } @NonNull public abstract Map getKeysWithType(); public Map getFlags(String key) { throw new UnsupportedOperationException("Flags must be returned by the corresponding subclasses. key: " + key); } @NonNull public abstract TestResult test(@NonNull IFilterableAppInfo info, @NonNull TestResult result); @NonNull @Override public String toString() { return "FilterOption{" + "type='" + type + '\'' + ", id=" + id + ", key='" + key + '\'' + ", keyType=" + keyType + ", value='" + value + '\'' + '}'; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeString(type); dest.writeInt(id); dest.writeString(key); dest.writeString(value); } @Override public int describeContents() { return 0; } public static final Creator CREATOR = new Creator() { @Override @NonNull public FilterOption createFromParcel(@NonNull Parcel in) { String type = Objects.requireNonNull(in.readString()); FilterOption filterOption = FilterOptions.create(type); filterOption.id = in.readInt(); String key = Objects.requireNonNull(in.readString()); String value = in.readString(); filterOption.setKeyValue(key, value); return filterOption; } @Override @NonNull public FilterOption[] newArray(int size) { return new FilterOption[size]; } }; @Nullable public JSONObject toJson() throws JSONException { JSONObject object = new JSONObject(); object.put("type", type); object.put("id", id); object.put("key", key); object.put("key_type", keyType); if (value != null) { object.put("value", value); } return object; } @NonNull public static FilterOption fromJson(@NonNull JSONObject object) throws JSONException { FilterOption option = FilterOptions.create(object.getString("type")); option.id = object.getInt("id"); // int keyType = object.getInt("key_type"); String key = object.getString("key"); String value = JSONUtils.optString(object, "value"); option.setKeyValue(key, value); return option; } protected String flagsToString(String key, int flags) { List result = new ArrayList<>(); for (Map.Entry entry : getFlags(key).entrySet()) { int flag = entry.getKey(); if ((flags & flag) != 0) { result.add(entry.getValue()); } } return String.join(", ", result); } public static class TestResult { private boolean mMatched = true; private List mMatchedBackups; private Map mMatchedComponents; private Map mMatchedTrackers; private List mMatchedPermissions; private List mMatchedSubjectLines; public TestResult setMatched(boolean matched) { mMatched = matched; return this; } public boolean isMatched() { return mMatched; } public TestResult setMatchedBackups(List matchedBackups) { mMatchedBackups = matchedBackups; return this; } @Nullable public List getMatchedBackups() { return mMatchedBackups; } public TestResult setMatchedComponents(Map matchedComponents) { mMatchedComponents = matchedComponents; return this; } @Nullable public Map getMatchedComponents() { return mMatchedComponents; } public TestResult setMatchedTrackers(Map matchedTrackers) { mMatchedTrackers = matchedTrackers; return this; } @Nullable public Map getMatchedTrackers() { return mMatchedTrackers; } public TestResult setMatchedPermissions(List matchedPermissions) { mMatchedPermissions = matchedPermissions; return this; } @Nullable public List getMatchedPermissions() { return mMatchedPermissions; } public TestResult setMatchedSubjectLines(List matchedSubjectLines) { mMatchedSubjectLines = matchedSubjectLines; return this; } @Nullable public List getMatchedSubjectLines() { return mMatchedSubjectLines; } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/filters/options/FilterOptions.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.filters.options; import androidx.annotation.NonNull; public final class FilterOptions { @NonNull public static FilterOption create(@NonNull String filterName) { switch (filterName) { case "apk_size": return new ApkSizeOption(); case "app_label": return new AppLabelOption(); case "app_type": return new AppTypeOption(); case "backup": return new BackupOption(); case "bloatware": return new BloatwareOption(); case "cache_size": return new CacheSizeOption(); case "compile_sdk": return new CompileSdkOption(); case "components": return new ComponentsOption(); case "data_size": return new DataSizeOption(); case "data_usage": return new DataUsageOption(); case "freeze_unfreeze": return new FreezeOption(); case "installed": return new InstalledOption(); case "installer": return new InstallerOption(); case "last_update": return new LastUpdateOption(); case "min_sdk": return new MinSdkOption(); case "permissions": return new PermissionsOption(); case "pkg_name": return new PackageNameOption(); case "running_apps": return new RunningAppsOption(); case "screen_time": return new ScreenTimeOption(); case "signature": return new SignatureOption(); case "target_sdk": return new TargetSdkOption(); case "times_opened": return new TimesOpenedOption(); case "total_size": return new TotalSizeOption(); case "trackers": return new TrackersOption(); case "uid": return new UidOption(); case "version_name": return new VersionNameOption(); } throw new IllegalArgumentException("Invalid filter: " + filterName); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/filters/options/FreezeOption.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.filters.options; import static io.github.muntashirakon.AppManager.utils.LangUtils.getSeparatorString; import android.content.Context; import android.os.Build; import androidx.annotation.NonNull; import java.util.LinkedHashMap; import java.util.Map; import io.github.muntashirakon.AppManager.filters.IFilterableAppInfo; public class FreezeOption extends FilterOption { public static final int FREEZE_TYPE_DISABLED = 1 << 0; public static final int FREEZE_TYPE_HIDDEN = 1 << 1; public static final int FREEZE_TYPE_SUSPENDED = 1 << 2; private final Map mKeysWithType = new LinkedHashMap() {{ put(KEY_ALL, TYPE_NONE); put("frozen", TYPE_NONE); put("unfrozen", TYPE_NONE); put("with_flags", TYPE_INT_FLAGS); put("without_flags", TYPE_INT_FLAGS); }}; private final Map mFrozenFlags = new LinkedHashMap() {{ put(FREEZE_TYPE_DISABLED, "Disabled"); put(FREEZE_TYPE_HIDDEN, "Hidden"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { put(FREEZE_TYPE_SUSPENDED, "Suspended"); } }}; public FreezeOption() { super("freeze_unfreeze"); } @NonNull @Override public Map getKeysWithType() { return mKeysWithType; } @Override public Map getFlags(@NonNull String key) { if (key.equals("with_flags") || key.equals("without_flags")) { return mFrozenFlags; } return super.getFlags(key); } @NonNull @Override public TestResult test(@NonNull IFilterableAppInfo info, @NonNull TestResult result) { int freezeFlags = info.getFreezeFlags(); switch (key) { case KEY_ALL: return result.setMatched(true); case "frozen": return result.setMatched(freezeFlags != 0); case "unfrozen": return result.setMatched(freezeFlags == 0); case "with_flags": return result.setMatched((freezeFlags & intValue) == intValue); case "without_flags": return result.setMatched((freezeFlags & intValue) != intValue); default: throw new UnsupportedOperationException("Invalid key " + key); } } @NonNull @Override public CharSequence toLocalizedString(@NonNull Context context) { switch (key) { case KEY_ALL: return "Frozen" + getSeparatorString() + " any"; case "frozen": return "Frozen apps only"; case "unfrozen": return "Unfrozen apps only"; case "with_flags": return "Frozen apps with types " + flagsToString("with_flags", intValue); case "without_flags": return "Frozen apps without types " + flagsToString("without_flags", intValue); default: throw new UnsupportedOperationException("Invalid key " + key); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/filters/options/InstalledOption.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.filters.options; import android.content.Context; import android.text.SpannableStringBuilder; import androidx.annotation.NonNull; import java.util.LinkedHashMap; import java.util.Map; import io.github.muntashirakon.AppManager.filters.IFilterableAppInfo; import io.github.muntashirakon.AppManager.utils.DateUtils; import io.github.muntashirakon.AppManager.utils.LangUtils; public class InstalledOption extends FilterOption { private final Map mKeysWithType = new LinkedHashMap() {{ put(KEY_ALL, TYPE_NONE); put("installed", TYPE_NONE); put("uninstalled", TYPE_NONE); put("installed_before", TYPE_TIME_MILLIS); put("installed_after", TYPE_TIME_MILLIS); }}; public InstalledOption() { super("installed"); } @NonNull @Override public Map getKeysWithType() { return mKeysWithType; } @NonNull @Override public TestResult test(@NonNull IFilterableAppInfo info, @NonNull TestResult result) { boolean installed = info.isInstalled(); switch (key) { case KEY_ALL: return result.setMatched(true); case "installed": return result.setMatched(installed); case "uninstalled": return result.setMatched(!installed); case "installed_before": return result.setMatched(installed && info.getFirstInstallTime() <= longValue); case "installed_after": return result.setMatched(installed && info.getFirstInstallTime() >= longValue); default: throw new UnsupportedOperationException("Invalid key " + key); } } @NonNull @Override public CharSequence toLocalizedString(@NonNull Context context) { SpannableStringBuilder sb = new SpannableStringBuilder("Installed"); switch (key) { case KEY_ALL: return sb.append(LangUtils.getSeparatorString()).append("any"); case "installed": return "Installed apps"; case "uninstalled": return "Uninstalled apps"; case "installed_before": return sb.append(" before ").append(DateUtils.formatDateTime(context, longValue)); case "installed_after": return sb.append(" after ").append(DateUtils.formatDateTime(context, longValue)); default: throw new UnsupportedOperationException("Invalid key " + key); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/filters/options/InstallerOption.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.filters.options; import android.content.Context; import android.text.SpannableStringBuilder; import androidx.annotation.NonNull; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import io.github.muntashirakon.AppManager.compat.InstallSourceInfoCompat; import io.github.muntashirakon.AppManager.filters.IFilterableAppInfo; import io.github.muntashirakon.AppManager.utils.LangUtils; public class InstallerOption extends FilterOption { private final Map mKeysWithType = new LinkedHashMap() {{ put(KEY_ALL, TYPE_NONE); put("installer", TYPE_STR_SINGLE); put("installer_any", TYPE_STR_MULTIPLE); put("installer_none", TYPE_STR_MULTIPLE); put("regex", TYPE_REGEX); }}; public InstallerOption() { super("installer"); } @NonNull @Override public Map getKeysWithType() { return mKeysWithType; } @NonNull @Override public TestResult test(@NonNull IFilterableAppInfo info, @NonNull TestResult result) { InstallSourceInfoCompat installSourceInfo = info.getInstallerInfo(); if (installSourceInfo == null) { return result.setMatched(key.equals(KEY_ALL)); } // There's at least one installer at this point Set installers = getInstallers(installSourceInfo); switch (key) { case KEY_ALL: return result.setMatched(false); case "installer": return result.setMatched(installers.contains(value)); case "installer_any": for (String installer: stringValues) { if (installers.contains(installer)) { return result.setMatched(true); } } return result.setMatched(false); case "installer_none": for (String installer: stringValues) { if (installers.contains(installer)) { return result.setMatched(false); } } return result.setMatched(true); case "regex": for (String installer : installers) { if (regexValue.matcher(installer).matches()) { return result.setMatched(true); } } return result.setMatched(false); default: throw new UnsupportedOperationException("Invalid key " + key); } } @NonNull private static Set getInstallers(@NonNull InstallSourceInfoCompat installSourceInfo) { Set installers = new LinkedHashSet<>(); if (installSourceInfo.getInstallingPackageName() != null) { installers.add(installSourceInfo.getInstallingPackageName()); } if (installSourceInfo.getInitiatingPackageName() != null) { installers.add(installSourceInfo.getInitiatingPackageName()); } if (installSourceInfo.getOriginatingPackageName() != null) { installers.add(installSourceInfo.getOriginatingPackageName()); } return installers; } @NonNull @Override public CharSequence toLocalizedString(@NonNull Context context) { SpannableStringBuilder sb = new SpannableStringBuilder("Only the apps with installer"); switch (key) { case KEY_ALL: return sb.append(LangUtils.getSeparatorString()).append("any"); case "installer": return sb.append(" ").append(value); case "installer_any": return sb.append(" matching any of ").append(String.join(", ", stringValues)); case "installer_none": return sb.append(" matching none of ").append(String.join(", ", stringValues)); case "regex": return sb.append(" that matches '").append(value).append("'"); default: throw new UnsupportedOperationException("Invalid key " + key); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/filters/options/LastUpdateOption.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.filters.options; import android.content.Context; import android.text.SpannableStringBuilder; import androidx.annotation.NonNull; import java.util.LinkedHashMap; import java.util.Map; import io.github.muntashirakon.AppManager.filters.IFilterableAppInfo; import io.github.muntashirakon.AppManager.utils.DateUtils; import io.github.muntashirakon.AppManager.utils.LangUtils; public class LastUpdateOption extends FilterOption { private final Map mKeysWithType = new LinkedHashMap() {{ put(KEY_ALL, TYPE_NONE); put("before", TYPE_TIME_MILLIS); put("after", TYPE_TIME_MILLIS); }}; public LastUpdateOption() { super("last_update"); } @NonNull @Override public Map getKeysWithType() { return mKeysWithType; } @NonNull @Override public TestResult test(@NonNull IFilterableAppInfo info, @NonNull TestResult result) { boolean installed = info.isInstalled(); switch (key) { case KEY_ALL: return result.setMatched(true); case "before": return result.setMatched(installed && info.getLastUpdateTime() <= longValue); case "after": return result.setMatched(installed && info.getLastUpdateTime() >= longValue); default: throw new UnsupportedOperationException("Invalid key " + key); } } @NonNull @Override public CharSequence toLocalizedString(@NonNull Context context) { SpannableStringBuilder sb = new SpannableStringBuilder("Last update"); switch (key) { case KEY_ALL: return sb.append(LangUtils.getSeparatorString()).append("any"); case "before": return sb.append(" before ").append(DateUtils.formatDateTime(context, longValue)); case "after": return sb.append(" after ").append(DateUtils.formatDateTime(context, longValue)); default: throw new UnsupportedOperationException("Invalid key " + key); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/filters/options/MinSdkOption.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.filters.options; import android.content.Context; import android.os.Build; import android.text.SpannableStringBuilder; import androidx.annotation.NonNull; import java.util.LinkedHashMap; import java.util.Map; import io.github.muntashirakon.AppManager.filters.IFilterableAppInfo; import io.github.muntashirakon.AppManager.utils.LangUtils; public class MinSdkOption extends FilterOption { private final Map mKeysWithType = new LinkedHashMap() {{ put(KEY_ALL, TYPE_NONE); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { put("eq", TYPE_INT); put("le", TYPE_INT); put("ge", TYPE_INT); } }}; public MinSdkOption() { super("min_sdk"); } @NonNull @Override public Map getKeysWithType() { return mKeysWithType; } @NonNull @Override public TestResult test(@NonNull IFilterableAppInfo info, @NonNull TestResult result) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { return result.setMatched(true); } switch (key) { case KEY_ALL: return result.setMatched(true); case "eq": return result.setMatched(info.getMinSdk() == intValue); case "le": return result.setMatched(info.getMinSdk() <= intValue); case "ge": return result.setMatched(info.getMinSdk() >= intValue); default: throw new UnsupportedOperationException("Invalid key " + key); } } @NonNull @Override public CharSequence toLocalizedString(@NonNull Context context) { SpannableStringBuilder sb = new SpannableStringBuilder("Minimum SDK"); switch (key) { case KEY_ALL: return sb.append(LangUtils.getSeparatorString()).append("any"); case "eq": return sb.append(" = ").append(Integer.toString(intValue)); case "le": return sb.append(" ≤ ").append(Integer.toString(intValue)); case "ge": return sb.append(" ≥ ").append(Integer.toString(intValue)); default: throw new UnsupportedOperationException("Invalid key " + key); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/filters/options/PackageNameOption.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.filters.options; import android.content.Context; import android.text.SpannableStringBuilder; import androidx.annotation.NonNull; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; import io.github.muntashirakon.AppManager.filters.IFilterableAppInfo; import io.github.muntashirakon.AppManager.utils.LangUtils; public class PackageNameOption extends FilterOption { private final Map mKeysWithType = new LinkedHashMap() {{ put(KEY_ALL, TYPE_NONE); put("eq", TYPE_STR_SINGLE); put("eq_any", TYPE_STR_MULTIPLE); put("eq_none", TYPE_STR_MULTIPLE); put("contains", TYPE_STR_SINGLE); put("starts_with", TYPE_STR_SINGLE); put("ends_with", TYPE_STR_SINGLE); put("regex", TYPE_REGEX); }}; public PackageNameOption() { super("pkg_name"); } @NonNull @Override public Map getKeysWithType() { return mKeysWithType; } @NonNull @Override public TestResult test(@NonNull IFilterableAppInfo info, @NonNull TestResult result) { switch (key) { case KEY_ALL: return result.setMatched(true); case "eq": return result.setMatched(info.getPackageName().equals(Objects.requireNonNull(value))); case "eq_any": for (String packageName: stringValues) { if (info.getPackageName().equals(packageName)) { return result.setMatched(true); } } return result.setMatched(false); case "eq_none": for (String packageName: stringValues) { if (info.getPackageName().equals(packageName)) { return result.setMatched(false); } } return result.setMatched(true); case "contains": return result.setMatched(info.getPackageName().contains(Objects.requireNonNull(value))); case "starts_with": return result.setMatched(info.getPackageName().startsWith(Objects.requireNonNull(value))); case "ends_with": return result.setMatched(info.getPackageName().endsWith(Objects.requireNonNull(value))); case "regex": return result.setMatched(regexValue.matcher(info.getPackageName()).matches()); default: throw new UnsupportedOperationException("Invalid key " + key); } } @NonNull @Override public CharSequence toLocalizedString(@NonNull Context context) { SpannableStringBuilder sb = new SpannableStringBuilder("Package name"); switch (key) { case KEY_ALL: return sb.append(LangUtils.getSeparatorString()).append("any"); case "eq": return sb.append(" = '").append(value).append("'"); case "eq_any": return sb.append(" matching any of ").append(String.join(", ", stringValues)); case "eq_none": return sb.append(" matching none of ").append(String.join(", ", stringValues)); case "contains": return sb.append(" contains '").append(value).append("'"); case "starts_with": return sb.append(" starts with '").append(value).append("'"); case "ends_with": return sb.append(" ends with '").append(value).append("'"); case "regex": return sb.append(" matches '").append(value).append("'"); default: throw new UnsupportedOperationException("Invalid key " + key); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/filters/options/PermissionsOption.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.filters.options; import android.content.Context; import android.text.SpannableStringBuilder; import androidx.annotation.NonNull; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import io.github.muntashirakon.AppManager.filters.IFilterableAppInfo; import io.github.muntashirakon.AppManager.utils.LangUtils; public class PermissionsOption extends FilterOption { private final Map mKeysWithType = new LinkedHashMap() {{ put(KEY_ALL, TYPE_NONE); put("eq", TYPE_STR_SINGLE); put("contains", TYPE_STR_SINGLE); put("starts_with", TYPE_STR_SINGLE); put("ends_with", TYPE_STR_SINGLE); put("regex", TYPE_REGEX); // TODO: 11/19/24 Add more curated options such as permission flags, private flags, grant }}; public PermissionsOption() { super("permissions"); } @NonNull @Override public Map getKeysWithType() { return mKeysWithType; } @NonNull @Override public TestResult test(@NonNull IFilterableAppInfo info, @NonNull TestResult result) { List permissions = result.getMatchedPermissions() != null ? result.getMatchedPermissions() : info.getAllPermissions(); switch (key) { case KEY_ALL: return result.setMatched(true).setMatchedPermissions(permissions); case "eq": { List filteredPermissions = new ArrayList<>(); for (String permission : permissions) { if (permission.equals(value)) { filteredPermissions.add(permission); } } return result.setMatched(!filteredPermissions.isEmpty()) .setMatchedPermissions(filteredPermissions); } case "contains": { Objects.requireNonNull(value); List filteredPermissions = new ArrayList<>(); for (String permission : permissions) { if (permission.contains(value)) { filteredPermissions.add(permission); } } return result.setMatched(!filteredPermissions.isEmpty()) .setMatchedPermissions(filteredPermissions); } case "starts_with": { Objects.requireNonNull(value); List filteredPermissions = new ArrayList<>(); for (String permission : permissions) { if (permission.startsWith(value)) { filteredPermissions.add(permission); } } return result.setMatched(!filteredPermissions.isEmpty()) .setMatchedPermissions(filteredPermissions); } case "ends_with": { Objects.requireNonNull(value); List filteredPermissions = new ArrayList<>(); for (String permission : permissions) { if (permission.endsWith(value)) { filteredPermissions.add(permission); } } return result.setMatched(!filteredPermissions.isEmpty()) .setMatchedPermissions(filteredPermissions); } case "regex": { Objects.requireNonNull(value); List filteredPermissions = new ArrayList<>(); for (String permission : permissions) { if (regexValue.matcher(permission).matches()) { filteredPermissions.add(permission); } } return result.setMatched(!filteredPermissions.isEmpty()) .setMatchedPermissions(filteredPermissions); } default: throw new UnsupportedOperationException("Invalid key " + key); } } @NonNull @Override public CharSequence toLocalizedString(@NonNull Context context) { SpannableStringBuilder sb = new SpannableStringBuilder("Permissions"); switch (key) { case KEY_ALL: return sb.append(LangUtils.getSeparatorString()).append("any"); case "eq": return sb.append(" = '").append(value).append("'"); case "contains": return sb.append(" contains '").append(value).append("'"); case "starts_with": return sb.append(" starts with '").append(value).append("'"); case "ends_with": return sb.append(" ends with '").append(value).append("'"); case "regex": return sb.append(" matches '").append(value).append("'"); default: throw new UnsupportedOperationException("Invalid key " + key); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/filters/options/RunningAppsOption.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.filters.options; import android.content.Context; import android.text.SpannableStringBuilder; import androidx.annotation.NonNull; import java.util.LinkedHashMap; import java.util.Map; import io.github.muntashirakon.AppManager.filters.IFilterableAppInfo; public class RunningAppsOption extends FilterOption { private final Map mKeysWithType = new LinkedHashMap() {{ put(KEY_ALL, TYPE_NONE); put("running", TYPE_NONE); put("not_running", TYPE_NONE); }}; public RunningAppsOption() { super("running_apps"); } @NonNull @Override public Map getKeysWithType() { return mKeysWithType; } @NonNull @Override public TestResult test(@NonNull IFilterableAppInfo info, @NonNull TestResult result) { switch (key) { case KEY_ALL: return result.setMatched(true); case "running": return result.setMatched(info.isRunning()); case "not_running": return result.setMatched(!info.isRunning()); default: throw new UnsupportedOperationException("Invalid key " + key); } } @NonNull @Override public CharSequence toLocalizedString(@NonNull Context context) { SpannableStringBuilder sb = new SpannableStringBuilder("App label"); switch (key) { case KEY_ALL: return "Both running and not running apps"; case "running": return "Only the running apps"; case "not_running": return "Only the not running apps"; default: throw new UnsupportedOperationException("Invalid key " + key); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/filters/options/ScreenTimeOption.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.filters.options; import android.content.Context; import android.text.SpannableStringBuilder; import androidx.annotation.NonNull; import java.util.LinkedHashMap; import java.util.Map; import io.github.muntashirakon.AppManager.filters.IFilterableAppInfo; import io.github.muntashirakon.AppManager.utils.DateUtils; import io.github.muntashirakon.AppManager.utils.LangUtils; public class ScreenTimeOption extends FilterOption { private final Map mKeysWithType = new LinkedHashMap() {{ put(KEY_ALL, TYPE_NONE); put("eq", TYPE_DURATION_MILLIS); put("le", TYPE_DURATION_MILLIS); put("ge", TYPE_DURATION_MILLIS); }}; public ScreenTimeOption() { super("screen_time"); } @NonNull @Override public Map getKeysWithType() { return mKeysWithType; } @NonNull @Override public TestResult test(@NonNull IFilterableAppInfo info, @NonNull TestResult result) { switch (key) { case KEY_ALL: return result.setMatched(true); case "eq": return result.setMatched(info.getTotalScreenTime() == longValue); case "le": return result.setMatched(info.getTotalScreenTime() <= longValue); case "ge": return result.setMatched(info.getTotalScreenTime() >= longValue); default: throw new UnsupportedOperationException("Invalid key " + key); } } @NonNull @Override public CharSequence toLocalizedString(@NonNull Context context) { SpannableStringBuilder sb = new SpannableStringBuilder("Screentime"); switch (key) { case KEY_ALL: return sb.append(LangUtils.getSeparatorString()).append("any"); case "eq": return sb.append(" = ").append(DateUtils.getFormattedDuration(context, longValue, false, true)); case "le": return sb.append(" ≤ ").append(DateUtils.getFormattedDuration(context, longValue, false, true)); case "ge": return sb.append(" ≥ ").append(DateUtils.getFormattedDuration(context, longValue, false, true)); default: throw new UnsupportedOperationException("Invalid key " + key); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/filters/options/SignatureOption.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.filters.options; import android.content.Context; import android.text.SpannableStringBuilder; import androidx.annotation.NonNull; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import io.github.muntashirakon.AppManager.apk.signing.SignerInfo; import io.github.muntashirakon.AppManager.filters.IFilterableAppInfo; import io.github.muntashirakon.AppManager.utils.LangUtils; public class SignatureOption extends FilterOption { private final Map mKeysWithType = new LinkedHashMap() {{ put(KEY_ALL, TYPE_NONE); put("no_signer", TYPE_NONE); put("with_lineage", TYPE_NONE); put("with_source_stamp", TYPE_NONE); put("without_lineage", TYPE_NONE); put("without_source_stamp", TYPE_NONE); put("sub_eq", TYPE_STR_SINGLE); put("sub_contains", TYPE_STR_SINGLE); put("sub_starts_with", TYPE_STR_SINGLE); put("sub_ends_with", TYPE_STR_SINGLE); put("sub_regex", TYPE_REGEX); put("sha256", TYPE_STR_SINGLE); }}; public SignatureOption() { super("signature"); } @NonNull @Override public Map getKeysWithType() { return mKeysWithType; } @NonNull @Override public TestResult test(@NonNull IFilterableAppInfo info, @NonNull TestResult result) { SignerInfo signerInfo = info.fetchSignerInfo(); if (signerInfo == null || signerInfo.getCurrentSignerCerts() == null) { // No singer return result.setMatched(key.equals("no_signer")).setMatchedSubjectLines(Collections.emptyList()); } List subjectLines = result.getMatchedSubjectLines() != null ? result.getMatchedSubjectLines() : Arrays.asList(info.getSignatureSubjectLines()); switch (key) { case KEY_ALL: return result.setMatched(true).setMatchedSubjectLines(subjectLines); case "no_signer": // Signer exists at this point return result.setMatched(false).setMatchedSubjectLines(subjectLines); case "with_source_stamp": return result.setMatched(signerInfo.getSourceStampCert() != null).setMatchedSubjectLines(subjectLines); case "with_lineage": return result.setMatched(signerInfo.getSignerCertsInLineage() != null).setMatchedSubjectLines(subjectLines); case "without_source_stamp": return result.setMatched(signerInfo.getSourceStampCert() == null).setMatchedSubjectLines(subjectLines); case "without_lineage": return result.setMatched(signerInfo.getSignerCertsInLineage() == null).setMatchedSubjectLines(subjectLines); case "sub_eq": { List matchedSubjectLines = new ArrayList<>(); for (String subject : subjectLines) { if (subject.equals(value)) { matchedSubjectLines.add(subject); } } return result.setMatched(!matchedSubjectLines.isEmpty()) .setMatchedSubjectLines(matchedSubjectLines); } case "sub_contains": { Objects.requireNonNull(value); List matchedSubjectLines = new ArrayList<>(); for (String subject : subjectLines) { if (subject.contains(value)) { matchedSubjectLines.add(subject); } } return result.setMatched(!matchedSubjectLines.isEmpty()) .setMatchedSubjectLines(matchedSubjectLines); } case "sub_starts_with": { Objects.requireNonNull(value); List matchedSubjectLines = new ArrayList<>(); for (String subject : subjectLines) { if (subject.startsWith(value)) { matchedSubjectLines.add(subject); } } return result.setMatched(!matchedSubjectLines.isEmpty()) .setMatchedSubjectLines(matchedSubjectLines); } case "sub_ends_with": { Objects.requireNonNull(value); List matchedSubjectLines = new ArrayList<>(); for (String subject : subjectLines) { if (subject.endsWith(value)) { matchedSubjectLines.add(subject); } } return result.setMatched(!matchedSubjectLines.isEmpty()) .setMatchedSubjectLines(matchedSubjectLines); } case "sub_regex": { Objects.requireNonNull(value); List matchedSubjectLines = new ArrayList<>(); for (String subject : subjectLines) { if (regexValue.matcher(subject).matches()) { matchedSubjectLines.add(subject); } } return result.setMatched(!matchedSubjectLines.isEmpty()) .setMatchedSubjectLines(matchedSubjectLines); } case "sha256": { String[] sha256sums = info.getSignatureSha256Checksums(); for (int i = 0; i < sha256sums.length; ++i) { if (sha256sums[i].equals(value)) { return result.setMatched(true) .setMatchedSubjectLines(Collections.singletonList(info.getSignatureSubjectLines()[i])); } } return result.setMatched(false).setMatchedSubjectLines(Collections.emptyList()); } default: throw new UnsupportedOperationException("Invalid key " + key); } } @NonNull @Override public CharSequence toLocalizedString(@NonNull Context context) { SpannableStringBuilder sb = new SpannableStringBuilder("Signatures"); switch (key) { case KEY_ALL: return sb.append(LangUtils.getSeparatorString()).append("any"); case "no_signer": return sb.append(LangUtils.getSeparatorString()).append("none"); case "with_lineage": return sb.append(" with lineages"); case "without_lineage": return sb.append(" without lineages"); case "with_source_stamp": return sb.append(" with source stamps"); case "without_source_stamp": return sb.append(" without source stamps"); case "sub_eq": return sb.append("' subject = '").append(value).append("'"); case "sub_contains": return sb.append("' subject contains '").append(value).append("'"); case "sub_starts_with": return sb.append("' subject starts with '").append(value).append("'"); case "sub_ends_with": return sb.append("' subject ends with '").append(value).append("'"); case "sub_regex": return sb.append("' subject matches '").append(value).append("'"); case "sha256": return sb.append("' SHA-256 = '").append(value).append("'"); default: throw new UnsupportedOperationException("Invalid key " + key); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/filters/options/TargetSdkOption.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.filters.options; import android.content.Context; import android.text.SpannableStringBuilder; import androidx.annotation.NonNull; import java.util.LinkedHashMap; import java.util.Map; import io.github.muntashirakon.AppManager.filters.IFilterableAppInfo; import io.github.muntashirakon.AppManager.utils.LangUtils; public class TargetSdkOption extends FilterOption { private final Map mKeysWithType = new LinkedHashMap() {{ put(KEY_ALL, TYPE_NONE); put("eq", TYPE_INT); put("le", TYPE_INT); put("ge", TYPE_INT); }}; public TargetSdkOption() { super("target_sdk"); } @NonNull @Override public Map getKeysWithType() { return mKeysWithType; } @NonNull @Override public TestResult test(@NonNull IFilterableAppInfo info, @NonNull TestResult result) { switch (key) { case KEY_ALL: return result.setMatched(true); case "eq": return result.setMatched(info.getTargetSdk() == intValue); case "le": return result.setMatched(info.getTargetSdk() <= intValue); case "ge": return result.setMatched(info.getTargetSdk() >= intValue); default: throw new UnsupportedOperationException("Invalid key " + key); } } @NonNull @Override public CharSequence toLocalizedString(@NonNull Context context) { SpannableStringBuilder sb = new SpannableStringBuilder("Target SDK"); switch (key) { case KEY_ALL: return sb.append(LangUtils.getSeparatorString()).append("any"); case "eq": return sb.append(" = ").append(Integer.toString(intValue)); case "le": return sb.append(" ≤ ").append(Integer.toString(intValue)); case "ge": return sb.append(" ≥ ").append(Integer.toString(intValue)); default: throw new UnsupportedOperationException("Invalid key " + key); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/filters/options/TimesOpenedOption.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.filters.options; import android.content.Context; import android.text.SpannableStringBuilder; import androidx.annotation.NonNull; import java.util.LinkedHashMap; import java.util.Map; import io.github.muntashirakon.AppManager.filters.IFilterableAppInfo; import io.github.muntashirakon.AppManager.utils.LangUtils; public class TimesOpenedOption extends FilterOption { private final Map mKeysWithType = new LinkedHashMap() {{ put(KEY_ALL, TYPE_NONE); put("eq", TYPE_INT); put("le", TYPE_INT); put("ge", TYPE_INT); }}; public TimesOpenedOption() { super("times_opened"); } @NonNull @Override public Map getKeysWithType() { return mKeysWithType; } @NonNull @Override public TestResult test(@NonNull IFilterableAppInfo info, @NonNull TestResult result) { switch (key) { case KEY_ALL: return result.setMatched(true); case "eq": return result.setMatched(info.getTimesOpened() == intValue); case "le": return result.setMatched(info.getTimesOpened() <= intValue); case "ge": return result.setMatched(info.getTimesOpened() >= intValue); default: throw new UnsupportedOperationException("Invalid key " + key); } } @NonNull @Override public CharSequence toLocalizedString(@NonNull Context context) { SpannableStringBuilder sb = new SpannableStringBuilder("Times opened"); switch (key) { case KEY_ALL: return sb.append(LangUtils.getSeparatorString()).append("any"); case "eq": return sb.append(" = ").append(Integer.toString(intValue)); case "le": return sb.append(" ≤ ").append(Integer.toString(intValue)); case "ge": return sb.append(" ≥ ").append(Integer.toString(intValue)); default: throw new UnsupportedOperationException("Invalid key " + key); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/filters/options/TotalSizeOption.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.filters.options; import android.content.Context; import android.text.SpannableStringBuilder; import android.text.format.Formatter; import androidx.annotation.NonNull; import java.util.LinkedHashMap; import java.util.Map; import io.github.muntashirakon.AppManager.filters.IFilterableAppInfo; import io.github.muntashirakon.AppManager.utils.LangUtils; public class TotalSizeOption extends FilterOption { private final Map mKeysWithType = new LinkedHashMap() {{ put(KEY_ALL, TYPE_NONE); put("eq", TYPE_SIZE_BYTES); put("le", TYPE_SIZE_BYTES); put("ge", TYPE_SIZE_BYTES); }}; public TotalSizeOption() { super("total_size"); } @NonNull @Override public Map getKeysWithType() { return mKeysWithType; } @NonNull @Override public TestResult test(@NonNull IFilterableAppInfo info, @NonNull TestResult result) { switch (key) { case KEY_ALL: return result.setMatched(true); case "eq": return result.setMatched(info.getTotalSize() == longValue); case "le": return result.setMatched(info.getTotalSize() <= longValue); case "ge": return result.setMatched(info.getTotalSize() >= longValue); default: throw new UnsupportedOperationException("Invalid key " + key); } } @NonNull @Override public CharSequence toLocalizedString(@NonNull Context context) { SpannableStringBuilder sb = new SpannableStringBuilder("Total size"); switch (key) { case KEY_ALL: return sb.append(LangUtils.getSeparatorString()).append("any"); case "eq": return sb.append(" = ").append(Formatter.formatFileSize(context, longValue)); case "le": return sb.append(" ≤ ").append(Formatter.formatFileSize(context, longValue)); case "ge": return sb.append(" ≥ ").append(Formatter.formatFileSize(context, longValue)); default: throw new UnsupportedOperationException("Invalid key " + key); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/filters/options/TrackersOption.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.filters.options; import android.content.Context; import android.text.SpannableStringBuilder; import androidx.annotation.NonNull; import java.util.LinkedHashMap; import java.util.Map; import io.github.muntashirakon.AppManager.filters.IFilterableAppInfo; import io.github.muntashirakon.AppManager.utils.LangUtils; public class TrackersOption extends FilterOption { private final Map mKeysWithType = new LinkedHashMap() {{ put(KEY_ALL, TYPE_NONE); put("eq", TYPE_INT); put("le", TYPE_INT); put("ge", TYPE_INT); // TODO: 7/2/24 Enhance this to include more curated options such as regex and find by tracker name }}; public TrackersOption() { super("trackers"); } @NonNull @Override public Map getKeysWithType() { return mKeysWithType; } @NonNull @Override public TestResult test(@NonNull IFilterableAppInfo info, @NonNull TestResult result) { switch (key) { case KEY_ALL: return result.setMatched(true); case "eq": return result.setMatched(info.getTrackerComponents().size() == intValue); case "le": return result.setMatched(info.getTrackerComponents().size() <= intValue); case "ge": return result.setMatched(info.getTrackerComponents().size() >= intValue); default: throw new UnsupportedOperationException("Invalid key " + key); } } @NonNull @Override public CharSequence toLocalizedString(@NonNull Context context) { SpannableStringBuilder sb = new SpannableStringBuilder("Trackers"); switch (key) { case KEY_ALL: return sb.append(LangUtils.getSeparatorString()).append("any"); case "eq": return sb.append(" = ").append(Integer.toString(intValue)); case "le": return sb.append(" ≤ ").append(Integer.toString(intValue)); case "ge": return sb.append(" ≥ ").append(Integer.toString(intValue)); default: throw new UnsupportedOperationException("Invalid key " + key); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/filters/options/UidOption.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.filters.options; import android.content.Context; import android.text.SpannableStringBuilder; import androidx.annotation.NonNull; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; import io.github.muntashirakon.AppManager.filters.IFilterableAppInfo; import io.github.muntashirakon.AppManager.utils.ArrayUtils; import io.github.muntashirakon.AppManager.utils.LangUtils; public class UidOption extends FilterOption { private final Map mKeysWithType = new LinkedHashMap() {{ put(KEY_ALL, TYPE_NONE); put("uid_eq", TYPE_INT); put("uid_le", TYPE_INT); put("uid_ge", TYPE_INT); put("with_shared", TYPE_NONE); put("without_shared", TYPE_NONE); put("shared_uid_name", TYPE_STR_SINGLE); put("shared_uid_names", TYPE_STR_MULTIPLE); put("shared_uid_name_regex", TYPE_REGEX); }}; public UidOption() { super("uid"); } @NonNull @Override public Map getKeysWithType() { return mKeysWithType; } @NonNull @Override public TestResult test(@NonNull IFilterableAppInfo info, @NonNull TestResult result) { switch (key) { case KEY_ALL: return result.setMatched(false); case "uid_eq": return result.setMatched(info.getUid() == intValue); case "uid_le": return result.setMatched(info.getUid() <= intValue); case "uid_ge": return result.setMatched(info.getUid() >= intValue); case "with_shared": return result.setMatched(info.getSharedUserId() != null); case "without_shared": return result.setMatched(info.getSharedUserId() == null); case "shared_uid_name": return result.setMatched(Objects.equals(info.getSharedUserId(), value)); case "shared_uid_names": return result.setMatched(ArrayUtils.contains(stringValues, info.getSharedUserId())); case "shared_uid_name_regex": return result.setMatched(regexValue.matcher(info.getSharedUserId()).matches()); default: throw new UnsupportedOperationException("Invalid key " + key); } } @NonNull @Override public CharSequence toLocalizedString(@NonNull Context context) { SpannableStringBuilder sb = new SpannableStringBuilder("UID"); switch (key) { case KEY_ALL: return sb.append(LangUtils.getSeparatorString()).append("any"); case "uid_eq": return sb.append(" = ").append(Integer.toString(intValue)); case "uid_le": return sb.append(" ≤ ").append(Integer.toString(intValue)); case "uid_ge": return sb.append(" ≥ ").append(Integer.toString(intValue)); case "with_shared": return "Only the apps with a shared UID"; case "without_shared": return "Only the apps without a shared UID"; case "shared_uid_name": return "Only the apps with the shared UID " + value; case "shared_uid_names": return "Only the apps with the shared UID (exclusive) " + String.join(", ", stringValues); case "shared_uid_name_regex": return "Only the apps with the shared UID that matches '" + value + "'"; default: throw new UnsupportedOperationException("Invalid key " + key); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/filters/options/VersionNameOption.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.filters.options; import android.content.Context; import android.text.SpannableStringBuilder; import androidx.annotation.NonNull; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; import io.github.muntashirakon.AppManager.filters.IFilterableAppInfo; import io.github.muntashirakon.AppManager.utils.LangUtils; public class VersionNameOption extends FilterOption { private final Map mKeysWithType = new LinkedHashMap() {{ put(KEY_ALL, TYPE_NONE); put("eq", TYPE_STR_SINGLE); put("contains", TYPE_STR_SINGLE); put("starts_with", TYPE_STR_SINGLE); put("ends_with", TYPE_STR_SINGLE); put("regex", TYPE_REGEX); }}; public VersionNameOption() { super("version_name"); } @NonNull @Override public Map getKeysWithType() { return mKeysWithType; } @NonNull @Override public TestResult test(@NonNull IFilterableAppInfo info, @NonNull TestResult result) { if (info.getVersionName() == null) { return result.setMatched(key.equals(KEY_ALL)); } switch (key) { case KEY_ALL: return result.setMatched(true); case "eq": return result.setMatched(info.getVersionName().equals(Objects.requireNonNull(value))); case "contains": return result.setMatched(info.getVersionName().contains(Objects.requireNonNull(value))); case "starts_with": return result.setMatched(info.getVersionName().startsWith(Objects.requireNonNull(value))); case "ends_with": return result.setMatched(info.getVersionName().endsWith(Objects.requireNonNull(value))); case "regex": return result.setMatched(regexValue.matcher(info.getVersionName()).matches()); default: throw new UnsupportedOperationException("Invalid key " + key); } } @NonNull @Override public CharSequence toLocalizedString(@NonNull Context context) { SpannableStringBuilder sb = new SpannableStringBuilder("Version name"); switch (key) { case KEY_ALL: return sb.append(LangUtils.getSeparatorString()).append("any"); case "eq": return sb.append(" = '").append(value).append("'"); case "contains": return sb.append(" contains '").append(value).append("'"); case "starts_with": return sb.append(" starts with '").append(value).append("'"); case "ends_with": return sb.append(" ends with '").append(value).append("'"); case "regex": return sb.append(" matches '").append(value).append("'"); default: throw new UnsupportedOperationException("Invalid key " + key); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/fm/ContentType2.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.fm; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.j256.simplemagic.entries.IanaEntries; import com.j256.simplemagic.entries.IanaEntry; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; public enum ContentType2 { APKM("application/vnd.apkm", "apkm", "apkm"), APKS("application/x-apks", "apks", "apks"), CONFIGURATION("text/plain", "configuration", "cnf", "conf", "cfg", "cf", "ini", "rc", "sys"), DEX("application/x-dex", "dex", "dex"), KOTLIN("text/x-kotlin", "kotlin", "kt"), LOG("text/plain", "log", "log"), LUA("text/x-lua", "lua", "lua"), M4A("audio/mp4a-latm", "mp4a-latm", "m4a"), MARKDOWN("text/markdown", "markdown", "md", "markdown"), PEM("application/pem-certificate-chain", "pem", "pem"), PK8("application/pkcs8", "pkcs8", "pk8"), PLIST("application/x-plist", "property-list", "plist"), PROPERTIES("text/plain", "properties", "prop", "properties"), SMALI("text/x-smali", "smali", "smali"), SQLITE3("application/vnd.sqlite3", "sqlite", "db", "db3", "s3db", "sl3", "sqlite", "sqlite3"), TOML("application/toml", "toml", "toml"), XAPK("application/xapk-package-archive", "xapk", "xapk"), YAML("text/plain", "yaml", "yml", "yaml"), /** default if no specific match to the mime-type */ OTHER("application/octet-stream", "other"), ; private final static Map sMimeTypeMap = new HashMap<>(); private final static Map sFileExtensionMap = new HashMap<>(); private static IanaEntries sIanaEntries; static { for (ContentType2 type : values()) { // NOTE: this may overwrite this mapping sMimeTypeMap.put(type.mMimeType.toLowerCase(Locale.ROOT), type); if (type.mFileExtensions != null) { for (String fileExtension : type.mFileExtensions) { // NOTE: this may overwrite this mapping sFileExtensionMap.put(fileExtension, type); } } } } @NonNull private final String mMimeType; @NonNull private final String mSimpleName; @Nullable private final String[] mFileExtensions; @Nullable private final IanaEntry mIanaEntry; ContentType2(@NonNull String mimeType, @NonNull String simpleName, @Nullable String... fileExtensions) { mMimeType = mimeType; mSimpleName = simpleName; mFileExtensions = fileExtensions; mIanaEntry = findIanaEntryByMimeType(mimeType); } /** * Get simple name of the type. */ @NonNull public String getSimpleName() { return mSimpleName; } @NonNull public String getMimeType() { return mMimeType; } @Nullable public String[] getFileExtensions() { return mFileExtensions; } /** * Return the type associated with the mime-type string or {@link #OTHER} if not found. */ @NonNull public static ContentType2 fromMimeType(String mimeType) { // NOTE: mimeType can be null if (mimeType != null) { mimeType = mimeType.toLowerCase(Locale.ROOT); } ContentType2 type = sMimeTypeMap.get(mimeType); if (type == null) { return OTHER; } else { return type; } } /** * Return the type associated with the file-extension string or {@code null} if not found. */ @Nullable public static ContentType2 fromFileExtension(@NonNull String fileExtension) { return sFileExtensionMap.get(fileExtension.toLowerCase(Locale.ROOT)); } /** * Returns the references of the mime type or null if none. */ @Nullable public List getReferences() { if (mIanaEntry == null) { return null; } else { return mIanaEntry.getReferences(); } } /** * Returns the URL of the references or null if none. */ @Nullable public List getReferenceUrls() { if (mIanaEntry == null) { return null; } else { return mIanaEntry.getReferenceUrls(); } } @Nullable private static IanaEntry findIanaEntryByMimeType(String mimeType) { if (sIanaEntries == null) { sIanaEntries = new IanaEntries(); } return sIanaEntries.lookupByMimeType(mimeType); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/fm/FmActivity.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.fm; import android.app.Activity; import android.app.Application; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.os.Parcel; import android.os.Parcelable; import android.provider.DocumentsContract; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.AppCompatImageView; import androidx.appcompat.widget.AppCompatTextView; import androidx.appcompat.widget.PopupMenu; import androidx.collection.ArrayMap; import androidx.core.os.BundleCompat; import androidx.core.os.ParcelCompat; import androidx.core.util.Pair; import androidx.documentfile.provider.DocumentFileUtils; import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.Fragment; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.button.MaterialButton; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.util.ArrayList; import java.util.List; import java.util.Objects; import io.github.muntashirakon.AppManager.BaseActivity; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.db.entity.FmFavorite; import io.github.muntashirakon.AppManager.fm.dialogs.FilePropertiesDialogFragment; import io.github.muntashirakon.AppManager.fm.dialogs.RenameDialogFragment; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.utils.ExUtils; import io.github.muntashirakon.AppManager.utils.StoragePermission; import io.github.muntashirakon.AppManager.utils.StorageUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.Utils; import io.github.muntashirakon.io.Paths; import io.github.muntashirakon.util.AdapterUtils; public class FmActivity extends BaseActivity { public static class Options implements Parcelable { public static final int OPTION_VFS = 1 << 0; public static final int OPTION_RO = 1 << 1; // read-only public static final int OPTION_MOUNT_DEX = 1 << 2; @NonNull public final Uri uri; public final int options; @Nullable private Uri mInitUriForVfs; public Options(@NonNull Uri uri) { this(uri, false, false, false); } protected Options(@NonNull Uri uri, int options) { this.uri = uri; this.options = options; } public Options(@NonNull Uri uri, boolean isVfs, boolean readOnly, boolean mountDexFiles) { this.uri = uri; int options = 0; if (isVfs) { options |= OPTION_VFS; } if (readOnly) { options |= OPTION_RO; } if (mountDexFiles) { options |= OPTION_MOUNT_DEX; } this.options = options; } public void setInitUriForVfs(@Nullable Uri initUriForVfs) { if (!isVfs() && initUriForVfs != null) { throw new IllegalArgumentException("initUri can only be set when the file system is virtual."); } this.mInitUriForVfs = initUriForVfs; } @Nullable public Uri getInitUriForVfs() { return mInitUriForVfs; } public boolean isVfs() { return (options & OPTION_VFS) != 0; } public boolean isMountDex() { return (options & OPTION_MOUNT_DEX) != 0; } protected Options(Parcel in) { uri = Objects.requireNonNull(ParcelCompat.readParcelable(in, Uri.class.getClassLoader(), Uri.class)); options = in.readInt(); mInitUriForVfs = ParcelCompat.readParcelable(in, Uri.class.getClassLoader(), Uri.class); } public static final Creator CREATOR = new Creator() { @NonNull @Override public Options createFromParcel(@NonNull Parcel in) { return new Options(in); } @NonNull @Override public Options[] newArray(int size) { return new Options[size]; } }; @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeParcelable(uri, flags); dest.writeInt(options); dest.writeParcelable(mInitUriForVfs, flags); } } public static final String LAUNCHER_ALIAS = "io.github.muntashirakon.AppManager.fm.FilesActivity"; public static final String EXTRA_OPTIONS = "opt"; private DrawerLayout mDrawerLayout; private RecyclerView mDrawerRecyclerView; private DrawerRecyclerViewAdapter mDrawerAdapter; private FmDrawerViewModel mViewModel; private final ActivityResultLauncher mAddDocumentProvider = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { try { if (result.getResultCode() != Activity.RESULT_OK) return; Intent data = result.getData(); if (data == null) return; Uri treeUri = data.getData(); if (treeUri == null) return; int takeFlags = data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); getContentResolver().takePersistableUriPermission(treeUri, takeFlags); } finally { // Display backup volumes again mViewModel.loadDrawerItems(); } }); private final StoragePermission mStoragePermission = StoragePermission.init(this); @Override protected void onAuthenticated(@Nullable Bundle savedInstanceState) { setContentView(R.layout.activity_fm); setSupportActionBar(findViewById(R.id.toolbar)); findViewById(R.id.progress_linear).setVisibility(View.GONE); mViewModel = new ViewModelProvider(this).get(FmDrawerViewModel.class); mDrawerLayout = findViewById(R.id.drawer_layout); mDrawerRecyclerView = findViewById(R.id.recycler_view); mDrawerRecyclerView.setLayoutManager(new LinearLayoutManager(this)); mDrawerAdapter = new DrawerRecyclerViewAdapter(this); mDrawerRecyclerView.setAdapter(mDrawerAdapter); ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setHomeAsUpIndicator(R.drawable.ic_menu); } mViewModel.getDrawerItemsLiveData().observe(this, drawerItems -> mDrawerAdapter.setAdapterItems(drawerItems)); FmFavoritesManager.getFavoriteAddedLiveData().observe(this, fmFavorite -> { // Reload drawer mViewModel.loadDrawerItems(); }); mViewModel.loadDrawerItems(); Uri uri = getIntent().getData(); if (uri != null && uri.getScheme() == null) { // file:// URI can have no schema. So, fix it by adding file:// if (uri.getPath() != null && uri.getAuthority() == null) { uri = uri.buildUpon().scheme(ContentResolver.SCHEME_FILE).build(); } else { // Avoid loading invalid paths uri = null; } } uri = FmUtils.sanitizeContentInput(uri); if (savedInstanceState == null) { Options options = getIntent().getExtras() != null ? BundleCompat.getParcelable(getIntent().getExtras(), EXTRA_OPTIONS, Options.class) : null; Integer position = null; if (options == null) { if (uri != null) { options = new Options(uri); } else if (Prefs.FileManager.isRememberLastOpenedPath()) { Pair> optionsUriPostionPair = Prefs.FileManager.getLastOpenedPath(); if (optionsUriPostionPair != null) { options = optionsUriPostionPair.first; if (options.isVfs()) { uri = optionsUriPostionPair.second.first; } position = optionsUriPostionPair.second.second; } } if (options == null) { // Use home options = new Options(Prefs.FileManager.getHome()); } } Uri uncheckedUri = options.uri; Uri checkedUri = ExUtils.exceptionAsNull(() -> Paths.getStrict(uncheckedUri).exists() ? uncheckedUri : null); if (checkedUri == null) { // Use default directory options = new Options(Uri.fromFile(Environment.getExternalStorageDirectory())); } if (options.isVfs()) { options.setInitUriForVfs(uri); } loadFragment(options, position); } } @Override protected void onNewIntent(@NonNull Intent intent) { super.onNewIntent(intent); Uri uri = intent.getData(); Options options = intent.getExtras() != null ? BundleCompat.getParcelable(intent.getExtras(), EXTRA_OPTIONS, Options.class) : null; if (options != null) { Intent intent2 = new Intent(this, FmActivity.class); if (uri != null) { intent2.setDataAndType(uri, DocumentsContract.Document.MIME_TYPE_DIR); } intent2.putExtra(EXTRA_OPTIONS, options); intent2.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); startActivity(intent2); return; } if (uri != null) { Intent intent2 = new Intent(this, FmActivity.class); intent2.setDataAndType(uri, DocumentsContract.Document.MIME_TYPE_DIR); intent2.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); startActivity(intent2); } } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == android.R.id.home) { mDrawerLayout.open(); return true; } return super.onOptionsItemSelected(item); } private void loadFragment(@NonNull Options options, @Nullable Integer position) { if (ContentResolver.SCHEME_FILE.equals(options.uri.getScheme())) { mStoragePermission.request(granted -> { // Return value does not matter doLoadFragment(options, position); }); } else doLoadFragment(options, position); } private void doLoadFragment(@NonNull Options options, @Nullable Integer position) { Fragment fragment = FmFragment.getNewInstance(options, position); getSupportFragmentManager() .beginTransaction() .replace(R.id.main_layout, fragment, FmFragment.TAG) .commit(); } public static class FmDrawerViewModel extends AndroidViewModel { private final MutableLiveData> mDrawerItemsLiveData = new MutableLiveData<>(); public FmDrawerViewModel(@NonNull Application application) { super(application); } public void removeFavorite(long id) { ThreadUtils.postOnBackgroundThread(() -> FmFavoritesManager.removeFromFavorite(id)); } public void renameFavorite(long id, @NonNull String newName) { ThreadUtils.postOnBackgroundThread(() -> FmFavoritesManager.renameFavorite(id, newName)); } public void releaseUri(@NonNull Uri uri) { try { getApplication() .getContentResolver() .releasePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); loadDrawerItems(); } catch (SecurityException e) { e.printStackTrace(); } } public LiveData> getDrawerItemsLiveData() { return mDrawerItemsLiveData; } public void loadDrawerItems() { ThreadUtils.postOnBackgroundThread(() -> { List drawerItems = new ArrayList<>(); Context context = getApplication(); // Favorites drawerItems.add(new FmDrawerItem(-1, context.getString(R.string.favorites), null, FmDrawerItem.ITEM_TYPE_LABEL)); List fmFavorites = FmFavoritesManager.getAllFavorites(); for (FmFavorite fmFavorite : fmFavorites) { Options options = new Options(Uri.parse(fmFavorite.uri), fmFavorite.options); options.mInitUriForVfs = fmFavorite.initUri != null ? Uri.parse(fmFavorite.initUri) : null; FmDrawerItem drawerItem = new FmDrawerItem(fmFavorite.id, fmFavorite.name, options, FmDrawerItem.ITEM_TYPE_FAVORITE); drawerItem.iconRes = getIconResFromName(fmFavorite.name); drawerItems.add(drawerItem); } // Locations drawerItems.add(new FmDrawerItem(-2, context.getString(R.string.storage), null, FmDrawerItem.ITEM_TYPE_LABEL)); ArrayMap storageLocations = StorageUtils.getAllStorageLocations(getApplication()); for (int i = 0; i < storageLocations.size(); ++i) { Uri uri = storageLocations.valueAt(i); Options options = new Options(uri); PackageManager pm = getApplication().getPackageManager(); ResolveInfo resolveInfo = DocumentFileUtils.getUriSource(getApplication(), uri); String name = resolveInfo != null ? resolveInfo.loadLabel(pm).toString() : storageLocations.keyAt(i); Drawable icon = resolveInfo != null ? resolveInfo.loadIcon(pm) : null; FmDrawerItem drawerItem = new FmDrawerItem(-4, name, options, FmDrawerItem.ITEM_TYPE_LOCATION); drawerItem.iconRes = R.drawable.ic_content_save; drawerItem.icon = icon; drawerItems.add(drawerItem); } mDrawerItemsLiveData.postValue(drawerItems); }); } private static int getIconResFromName(@NonNull String filename) { switch (filename) { case "Documents": return R.drawable.ic_file_document; case "Download": case "Downloads": return R.drawable.ic_get_app; case "Pictures": case "DCIM": return R.drawable.ic_image; case "Movies": case "Music": case "Podcasts": case "Recordings": case "Ringtones": return R.drawable.ic_audio_file; default: return R.drawable.ic_folder; } } } public static class DrawerRecyclerViewAdapter extends RecyclerView.Adapter { private final List mAdapterItems = new ArrayList<>(); private final FmActivity mFmActivity; public DrawerRecyclerViewAdapter(@NonNull FmActivity activity) { mFmActivity = activity; } public void setAdapterItems(@NonNull List adapterItems) { AdapterUtils.notifyDataSetChanged(this, mAdapterItems, adapterItems); } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, @FmDrawerItem.DrawerItemType int viewType) { int layoutId; if (viewType == FmDrawerItem.ITEM_TYPE_LABEL) { layoutId = R.layout.item_title_action; } else layoutId = R.layout.item_fm_drawer; View v = LayoutInflater.from(parent.getContext()).inflate(layoutId, parent, false); return new ViewHolder(v); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { FmDrawerItem item = mAdapterItems.get(position); holder.labelView.setText(item.name); if (item.type == FmDrawerItem.ITEM_TYPE_LABEL) { getLabelView(holder, item); } else { getView(holder, item); } } public void getLabelView(@NonNull ViewHolder holder, FmDrawerItem item) { if (holder.actionView == null) { return; } if (item.id == -1) { // Favorites holder.actionView.setVisibility(View.GONE); } else if (item.id == -2) { // Locations holder.actionView.setVisibility(View.VISIBLE); holder.actionView.setIconResource(R.drawable.ic_add); holder.actionView.setContentDescription(holder.itemView.getContext().getString(R.string.add)); holder.actionView.setOnClickListener(v -> { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) .putExtra("android.provider.extra.SHOW_ADVANCED", true); mFmActivity.mAddDocumentProvider.launch(intent); }); } else holder.actionView.setVisibility(View.GONE); } public void getView(@NonNull ViewHolder holder, @NonNull FmDrawerItem item) { Objects.requireNonNull(item.options); if (holder.iconView != null) { if (item.icon != null) { holder.iconView.setImageDrawable(item.icon); } else holder.iconView.setImageResource(item.iconRes); } holder.itemView.setOnClickListener(v -> { Options options = item.options; mFmActivity.mDrawerLayout.close(); mFmActivity.loadFragment(options, null); }); holder.itemView.setOnLongClickListener(v -> { Context context = v.getContext(); PopupMenu popupMenu = new PopupMenu(context, v); Menu menu = popupMenu.getMenu(); // Copy path menu.add(R.string.copy_this_path).setOnMenuItemClickListener(menuItem -> { Uri uri = item.options.getInitUriForVfs() != null ? item.options.getInitUriForVfs() : item.options.uri; String path = FmUtils.getDisplayablePath(uri); Utils.copyToClipboard(context, "Path", path); return true; }); // Remove item Uri uri = item.options.uri; boolean removable = item.type != FmDrawerItem.ITEM_TYPE_LOCATION || ContentResolver.SCHEME_CONTENT.equals(uri.getScheme()); if (removable) { menu.add(R.string.item_remove).setOnMenuItemClickListener(menuItem -> { new MaterialAlertDialogBuilder(mFmActivity) .setTitle(context.getString(R.string.remove_filename, item.name)) .setMessage(R.string.are_you_sure) .setNegativeButton(R.string.no, null) .setPositiveButton(R.string.yes, (dialog, which) -> { if (item.type == FmDrawerItem.ITEM_TYPE_LOCATION) { mFmActivity.mViewModel.releaseUri(uri); } else if (item.type == FmDrawerItem.ITEM_TYPE_FAVORITE) { mFmActivity.mViewModel.removeFavorite(item.id); } }) .show(); return true; }); } // Edit item if (item.type == FmDrawerItem.ITEM_TYPE_FAVORITE) { menu.add(R.string.item_edit).setOnMenuItemClickListener(menuItem -> { RenameDialogFragment dialog = RenameDialogFragment.getInstance(item.name, (prefix, extension) -> { String displayName; if (!TextUtils.isEmpty(extension)) { displayName = prefix + "." + extension; } else { displayName = prefix; } mFmActivity.mViewModel.renameFavorite(item.id, displayName); }); dialog.show(mFmActivity.getSupportFragmentManager(), RenameDialogFragment.TAG); return true; }); } // Properties if (!item.options.isVfs()) { menu.add(R.string.file_properties).setOnMenuItemClickListener(menuItem -> { FilePropertiesDialogFragment dialogFragment = FilePropertiesDialogFragment.getInstance(item.options.uri); dialogFragment.show(mFmActivity.getSupportFragmentManager(), FilePropertiesDialogFragment.TAG); return true; }); } popupMenu.show(); return true; }); } @Override public int getItemCount() { return mAdapterItems.size(); } @FmDrawerItem.DrawerItemType @Override public int getItemViewType(int position) { return mAdapterItems.get(position).type; } public static class ViewHolder extends RecyclerView.ViewHolder { @Nullable private final AppCompatImageView iconView; private final AppCompatTextView labelView; @Nullable private final MaterialButton actionView; public ViewHolder(@NonNull View itemView) { super(itemView); iconView = itemView.findViewById(R.id.item_icon); labelView = itemView.findViewById(R.id.item_title); actionView = itemView.findViewById(R.id.item_action); } } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/fm/FmAdapter.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.fm; import android.text.TextUtils; import android.text.format.Formatter; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import androidx.annotation.MainThread; import androidx.annotation.NonNull; import androidx.appcompat.widget.AppCompatTextView; import androidx.appcompat.widget.LinearLayoutCompat; import androidx.appcompat.widget.PopupMenu; import androidx.core.content.ContextCompat; import com.google.android.material.button.MaterialButton; import com.google.android.material.card.MaterialCardView; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.imageview.ShapeableImageView; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Objects; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.fm.dialogs.OpenWithDialogFragment; import io.github.muntashirakon.AppManager.fm.dialogs.RenameDialogFragment; import io.github.muntashirakon.AppManager.fm.icons.FmIconFetcher; import io.github.muntashirakon.AppManager.self.imagecache.ImageLoader; import io.github.muntashirakon.AppManager.utils.DateUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.AppManager.utils.Utils; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.util.AccessibilityUtils; import io.github.muntashirakon.util.AdapterUtils; import io.github.muntashirakon.widget.MultiSelectionView; class FmAdapter extends MultiSelectionView.Adapter { private static final List DEX_EXTENSIONS = Arrays.asList("dex", "jar"); private final List mAdapterList = Collections.synchronizedList(new ArrayList<>()); private final FmViewModel mViewModel; private final FmActivity mFmActivity; public FmAdapter(FmViewModel viewModel, FmActivity activity) { mViewModel = viewModel; mFmActivity = activity; } public void setFmList(List list) { AdapterUtils.notifyDataSetChanged(this, mAdapterList, list); notifySelectionChange(); } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_fm, parent, false); View actionView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_right_standalone_action, parent, false); LinearLayoutCompat layout = view.findViewById(android.R.id.widget_frame); layout.addView(actionView); return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { FmItem item = mAdapterList.get(position); holder.itemView.setTag(item.path); holder.title.setText(item.getName()); // Load attributes cacheAndLoadAttributes(holder, item); if (item.isDirectory) { holder.itemView.setOnClickListener(v -> { if (isInSelectionMode()) { toggleSelection(position); AccessibilityUtils.requestAccessibilityFocus(holder.itemView); return; } mViewModel.loadFiles(item.path.getUri()); }); } else { holder.itemView.setOnClickListener(v -> { if (isInSelectionMode()) { toggleSelection(position); AccessibilityUtils.requestAccessibilityFocus(holder.itemView); return; } // TODO: 16/11/22 Retrieve default open with from DB and open the file with it OpenWithDialogFragment fragment = OpenWithDialogFragment.getInstance(item.path); fragment.show(mFmActivity.getSupportFragmentManager(), OpenWithDialogFragment.TAG); }); } // Symbolic link holder.symbolicLinkIcon.setVisibility(item.path.isSymbolicLink() ? View.VISIBLE : View.GONE); // Set background colors holder.itemView.setCardBackgroundColor(ContextCompat.getColor(holder.itemView.getContext(), android.R.color.transparent)); // Set selections holder.icon.setOnClickListener(v -> { toggleSelection(position); AccessibilityUtils.requestAccessibilityFocus(holder.itemView); }); // Set actions PopupMenu popupMenu = getPopupMenu(holder.action, item, position); holder.action.setOnClickListener(v -> popupMenu.show()); holder.itemView.setOnLongClickListener(v -> { // Long click listener: Select/deselect an app. // 1) Turn selection mode on if this is the first item in the selection list // 2) Select between last selection position and this position (inclusive) if selection mode is on Path lastSelectedItem = mViewModel.getLastSelectedItem(); int lastSelectedItemPosition = -1; if (lastSelectedItem != null) { int i = 0; for (FmItem fmItem : mAdapterList) { if (fmItem.path.equals(lastSelectedItem)) { lastSelectedItemPosition = i; break; } ++i; } } if (lastSelectedItemPosition >= 0) { // Select from last selection to this selection selectRange(lastSelectedItemPosition, position); } else { toggleSelection(position); AccessibilityUtils.requestAccessibilityFocus(holder.itemView); } return true; }); super.onBindViewHolder(holder, position); } private void cacheAndLoadAttributes(@NonNull ViewHolder holder, @NonNull FmItem item) { if (item.isCached()) { loadAttributes(holder, item); } else { // TODO: 9/9/23 Store these threads in a list and cancel them when not needed ThreadUtils.postOnBackgroundThread(() -> { WeakReference holderRef = new WeakReference<>(holder); WeakReference itemRef = new WeakReference<>(item); item.cache(); ThreadUtils.postOnMainThread(() -> { ViewHolder h = holderRef.get(); FmItem i = itemRef.get(); if (h != null && i != null && Objects.equals(h.itemView.getTag(), i.path)) { loadAttributes(h, i); } }); }); } } @MainThread private void loadAttributes(@NonNull ViewHolder holder, @NonNull FmItem item) { // Set icon String tag = item.getTag(); holder.icon.setTag(tag); ImageLoader.getInstance().displayImage(tag, holder.icon, new FmIconFetcher(item)); // Set sub-icon // TODO: 24/5/23 Set sub-icon if needed // Attrs String modificationDate = DateUtils.formatDateTime(mFmActivity, item.getLastModified()); if (item.isDirectory) { holder.subtitle.setText(String.format(Locale.getDefault(), "%d • %s", item.getChildCount(), modificationDate)); } else { holder.subtitle.setText(String.format(Locale.getDefault(), "%s • %s", Formatter.formatShortFileSize(mFmActivity, item.getSize()), modificationDate)); } } @Override public long getItemId(int position) { return mAdapterList.get(position).hashCode(); } @Override public int getItemCount() { return mAdapterList.size(); } @Override protected boolean select(int position) { mViewModel.setSelectedItem(mAdapterList.get(position).path, true); return true; } @Override protected boolean deselect(int position) { mViewModel.setSelectedItem(mAdapterList.get(position).path, false); return true; } @Override protected boolean isSelected(int position) { return mViewModel.isSelected(mAdapterList.get(position).path); } @Override protected void cancelSelection() { super.cancelSelection(); mViewModel.clearSelections(); } @Override protected int getSelectedItemCount() { return mViewModel.getSelectedItemCount(); } @Override protected int getTotalItemCount() { return mAdapterList.size(); } private PopupMenu getPopupMenu(@NonNull View anchor, @NonNull FmItem item, int position) { PopupMenu popupMenu = new PopupMenu(anchor.getContext(), anchor); popupMenu.setForceShowIcon(true); popupMenu.inflate(R.menu.fragment_fm_item_actions); Menu menu = popupMenu.getMenu(); MenuItem openWithAction = menu.findItem(R.id.action_open_with); MenuItem cutAction = menu.findItem(R.id.action_cut); MenuItem copyAction = menu.findItem(R.id.action_copy); MenuItem renameAction = menu.findItem(R.id.action_rename); MenuItem deleteAction = menu.findItem(R.id.action_delete); MenuItem shareAction = menu.findItem(R.id.action_share); MenuItem selectAction = menu.findItem(R.id.action_select); // Disable actions based on criteria boolean canRead = item.path.canRead(); boolean canWrite = item.path.canWrite(); openWithAction.setEnabled(canRead); cutAction.setEnabled(canRead && canWrite); copyAction.setEnabled(canRead); renameAction.setEnabled(canRead && canWrite); deleteAction.setEnabled(canRead && canWrite); shareAction.setEnabled(canRead); // Set actions openWithAction.setOnMenuItemClickListener(menuItem -> { OpenWithDialogFragment fragment = OpenWithDialogFragment.getInstance(item.path); fragment.show(mFmActivity.getSupportFragmentManager(), OpenWithDialogFragment.TAG); return true; }); cutAction.setOnMenuItemClickListener(menuItem -> { FmTasks.FmTask fmTask = new FmTasks.FmTask(FmTasks.FmTask.TYPE_CUT, Collections.singletonList(item.path)); FmTasks.getInstance().enqueue(fmTask); UIUtils.displayShortToast(R.string.copied_to_clipboard); return false; }); copyAction.setOnMenuItemClickListener(menuItem -> { FmTasks.FmTask fmTask = new FmTasks.FmTask(FmTasks.FmTask.TYPE_COPY, Collections.singletonList(item.path)); FmTasks.getInstance().enqueue(fmTask); UIUtils.displayShortToast(R.string.copied_to_clipboard); return false; }); renameAction.setOnMenuItemClickListener(menuItem -> { RenameDialogFragment dialog = RenameDialogFragment.getInstance(item.path.getName(), (prefix, extension) -> { String displayName; if (!TextUtils.isEmpty(extension)) { displayName = prefix + "." + extension; } else { displayName = prefix; } if (item.path.renameTo(displayName)) { UIUtils.displayShortToast(R.string.renamed_successfully); mViewModel.reload(); } else { UIUtils.displayShortToast(R.string.failed); } }); dialog.show(mFmActivity.getSupportFragmentManager(), RenameDialogFragment.TAG); return false; }); deleteAction.setOnMenuItemClickListener(menuItem -> { new MaterialAlertDialogBuilder(mFmActivity) .setTitle(mFmActivity.getString(R.string.delete_filename, item.path.getName())) .setMessage(R.string.are_you_sure) .setNegativeButton(R.string.cancel, null) .setPositiveButton(R.string.confirm_file_deletion, (dialog, which) -> { if (item.path.delete()) { UIUtils.displayShortToast(R.string.deleted_successfully); mViewModel.reload(); } else { UIUtils.displayShortToast(R.string.failed); } }) .show(); return true; }); shareAction.setOnMenuItemClickListener(menuItem -> { mViewModel.shareFiles(Collections.singletonList(item.path)); return true; }); selectAction.setOnMenuItemClickListener(menuItem -> { select(position); notifySelectionChange(); notifyItemChanged(position, AdapterUtils.STUB); return true; }); boolean isVfs = mViewModel.getOptions().isVfs(); menu.findItem(R.id.action_shortcut) // TODO: 31/5/23 Enable creating shortcuts for VFS .setEnabled(!isVfs) .setVisible(!isVfs) .setOnMenuItemClickListener(menuItem -> { mViewModel.createShortcut(item); return true; }); MenuItem favItem = menu.findItem(R.id.action_add_to_favorites); favItem.setOnMenuItemClickListener(menuItem -> { mViewModel.addToFavorite(item.path, mViewModel.getOptions()); return true; }); favItem.setEnabled(item.isDirectory); favItem.setVisible(item.isDirectory); menu.findItem(R.id.action_copy_path).setOnMenuItemClickListener(menuItem -> { String path = FmUtils.getDisplayablePath(item.path); Utils.copyToClipboard(mFmActivity, "Path", path); return true; }); menu.findItem(R.id.action_properties).setOnMenuItemClickListener(menuItem -> { mViewModel.getDisplayPropertiesLiveData().setValue(item.path.getUri()); return true; }); return popupMenu; } protected static class ViewHolder extends MultiSelectionView.ViewHolder { final MaterialCardView itemView; final ShapeableImageView icon; final ShapeableImageView symbolicLinkIcon; final MaterialButton action; final AppCompatTextView title; final AppCompatTextView subtitle; public ViewHolder(@NonNull View itemView) { super(itemView); this.itemView = (MaterialCardView) itemView; icon = itemView.findViewById(android.R.id.icon); symbolicLinkIcon = itemView.findViewById(R.id.symbolic_link_icon); action = itemView.findViewById(android.R.id.button1); action.setContentDescription(itemView.getContext().getString(androidx.appcompat.R.string.abc_action_menu_overflow_description)); title = itemView.findViewById(android.R.id.title); subtitle = itemView.findViewById(android.R.id.summary); action.setIconResource(io.github.muntashirakon.ui.R.drawable.ic_more_vert); itemView.findViewById(R.id.divider).setVisibility(View.GONE); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/fm/FmDrawerItem.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.fm; import android.graphics.drawable.Drawable; import androidx.annotation.ColorInt; import androidx.annotation.DrawableRes; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; public class FmDrawerItem { public static final int ITEM_TYPE_LABEL = 0; public static final int ITEM_TYPE_FAVORITE = 1; public static final int ITEM_TYPE_LOCATION = 2; public static final int ITEM_TYPE_TAG = 3; @IntDef({ITEM_TYPE_LABEL, ITEM_TYPE_FAVORITE, ITEM_TYPE_LOCATION, ITEM_TYPE_TAG}) @Retention(RetentionPolicy.SOURCE) public @interface DrawerItemType { } public final long id; @NonNull public final String name; @Nullable public final FmActivity.Options options; @DrawerItemType public final int type; @DrawableRes public int iconRes; @Nullable public Drawable icon; @ColorInt public int color; public FmDrawerItem(long id, @NonNull String name, @Nullable FmActivity.Options options, @DrawerItemType int type) { this.id = id; this.name = name; this.options = options; this.type = type; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/fm/FmFavoritesManager.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.fm; import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import java.util.List; import io.github.muntashirakon.AppManager.db.AppsDb; import io.github.muntashirakon.AppManager.db.entity.FmFavorite; import io.github.muntashirakon.io.Path; public final class FmFavoritesManager { private static final MutableLiveData sFavoriteAddedLiveData = new MutableLiveData<>(); public static LiveData getFavoriteAddedLiveData() { return sFavoriteAddedLiveData; } @WorkerThread public static long addToFavorite(@NonNull Path path, @NonNull FmActivity.Options options) { FmFavorite fmFavorite = new FmFavorite(); fmFavorite.name = path.getName(); fmFavorite.uri = options.isVfs() ? options.uri.toString() : path.getUri().toString(); fmFavorite.initUri = options.isVfs() ? path.getUri().toString() : null; fmFavorite.options = options.options; long id = AppsDb.getInstance().fmFavoriteDao().insert(fmFavorite); sFavoriteAddedLiveData.postValue(fmFavorite); return id; } @WorkerThread public static void removeFromFavorite(long id) { AppsDb.getInstance().fmFavoriteDao().delete(id); sFavoriteAddedLiveData.postValue(null); } public static void renameFavorite(long id, @NonNull String newName) { AppsDb.getInstance().fmFavoriteDao().rename(id, newName); sFavoriteAddedLiveData.postValue(null); } @WorkerThread public static List getAllFavorites() { return AppsDb.getInstance().fmFavoriteDao().getAll(); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/fm/FmFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.fm; import static io.github.muntashirakon.AppManager.fm.FmTasks.FmTask.TYPE_CUT; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.os.SystemClock; import android.provider.DocumentsContract; import android.text.TextUtils; import android.text.format.Formatter; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.widget.ImageView; import android.widget.TextView; import androidx.activity.OnBackPressedCallback; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.LinearLayoutCompat; import androidx.appcompat.widget.SearchView; import androidx.core.content.ContextCompat; import androidx.core.os.BundleCompat; import androidx.core.provider.DocumentsContractCompat; import androidx.core.view.MenuProvider; import androidx.fragment.app.Fragment; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; import com.google.android.material.button.MaterialButton; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.progressindicator.LinearProgressIndicator; import com.leinardi.android.speeddial.SpeedDialActionItem; import com.leinardi.android.speeddial.SpeedDialView; import java.io.File; import java.io.IOException; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.Optional; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.fm.dialogs.FilePropertiesDialogFragment; import io.github.muntashirakon.AppManager.fm.dialogs.NewFileDialogFragment; import io.github.muntashirakon.AppManager.fm.dialogs.NewFolderDialogFragment; import io.github.muntashirakon.AppManager.fm.dialogs.NewSymbolicLinkDialogFragment; import io.github.muntashirakon.AppManager.fm.dialogs.RenameDialogFragment; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.settings.SettingsActivity; import io.github.muntashirakon.AppManager.shortcut.CreateShortcutDialogFragment; import io.github.muntashirakon.AppManager.utils.FileUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.AppManager.utils.Utils; import io.github.muntashirakon.dialog.TextInputDialogBuilder; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; import io.github.muntashirakon.multiselection.MultiSelectionActionsView; import io.github.muntashirakon.util.UiUtils; import io.github.muntashirakon.widget.FloatingActionButtonGroup; import io.github.muntashirakon.widget.MultiSelectionView; import io.github.muntashirakon.widget.RecyclerView; import io.github.muntashirakon.widget.SwipeRefreshLayout; public class FmFragment extends Fragment implements MenuProvider, SearchView.OnQueryTextListener, SwipeRefreshLayout.OnRefreshListener, SpeedDialView.OnActionSelectedListener, MultiSelectionActionsView.OnItemSelectedListener, MultiSelectionView.OnSelectionModeChangeListener { public static final String TAG = FmFragment.class.getSimpleName(); private static final String ARG_URI = "uri"; public static final String ARG_OPTIONS = "opt"; public static final String ARG_POSITION = "pos"; @NonNull public static FmFragment getNewInstance(@NonNull FmActivity.Options options, @Nullable Integer position) { FmFragment fragment = new FmFragment(); Bundle args = new Bundle(); args.putParcelable(ARG_OPTIONS, options); if (position != null) { args.putInt(ARG_POSITION, position); } fragment.setArguments(args); return fragment; } private FmViewModel mModel; @Nullable private RecyclerView mRecyclerView; private LinearLayoutCompat mEmptyView; private ImageView mEmptyViewIcon; private TextView mEmptyViewTitle; private TextView mEmptyViewDetails; @Nullable private FmAdapter mAdapter; @Nullable private SwipeRefreshLayout mSwipeRefresh; @Nullable private MultiSelectionView mMultiSelectionView; private FloatingActionButtonGroup mFabGroup; private FmPathListAdapter mPathListAdapter; private FmActivity mActivity; @Nullable private FolderShortInfo mFolderShortInfo; private final ViewTreeObserver.OnGlobalLayoutListener mMultiSelectionViewChangeListener = () -> { if (mFabGroup != null && getActivity() != null) { int defaultMargin = UiUtils.dpToPx(requireContext(), 16); int newMargin; if (mMultiSelectionView.getVisibility() == View.VISIBLE) { newMargin = defaultMargin + mMultiSelectionView.getHeight(); } else newMargin = defaultMargin; ViewGroup.MarginLayoutParams marginLayoutParams = (ViewGroup.MarginLayoutParams) mFabGroup.getLayoutParams(); if (marginLayoutParams.bottomMargin != newMargin) { marginLayoutParams.bottomMargin = newMargin; mFabGroup.setLayoutParams(marginLayoutParams); } } }; private final OnBackPressedCallback mExitSelectionBackPressedCallback = new OnBackPressedCallback(false) { @Override public void handleOnBackPressed() { if (mAdapter != null && mMultiSelectionView != null && mAdapter.isInSelectionMode()) { mMultiSelectionView.cancel(); return; } setEnabled(false); requireActivity().getOnBackPressedDispatcher().onBackPressed(); } }; private final OnBackPressedCallback mGoUpBackPressedCallback = new OnBackPressedCallback(false) { @Override public void handleOnBackPressed() { if (mPathListAdapter != null && mPathListAdapter.getCurrentPosition() > 0) { mModel.loadFiles(mPathListAdapter.calculateUri(mPathListAdapter.getCurrentPosition() - 1)); return; } setEnabled(false); requireActivity().getOnBackPressedDispatcher().onBackPressed(); } }; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); mModel = new ViewModelProvider(this).get(FmViewModel.class); } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_fm, container, false); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); FmActivity.Options options = null; Uri uri = null; AtomicInteger scrollPosition = new AtomicInteger(RecyclerView.NO_POSITION); if (savedInstanceState != null) { uri = BundleCompat.getParcelable(savedInstanceState, ARG_URI, Uri.class); options = BundleCompat.getParcelable(savedInstanceState, ARG_OPTIONS, FmActivity.Options.class); scrollPosition.set(savedInstanceState.getInt(ARG_POSITION, RecyclerView.NO_POSITION)); } if (options == null) { options = Objects.requireNonNull(BundleCompat.getParcelable(requireArguments(), ARG_OPTIONS, FmActivity.Options.class)); if (uri == null) { uri = options.getInitUriForVfs(); } if (requireArguments().containsKey(ARG_POSITION)) { scrollPosition.set(requireArguments().getInt(ARG_POSITION, RecyclerView.NO_POSITION)); } } mActivity = (FmActivity) requireActivity(); mSwipeRefresh = view.findViewById(R.id.swipe_refresh); mSwipeRefresh.setOnRefreshListener(this); UiUtils.applyWindowInsetsAsPadding(view.findViewById(R.id.path_container), false, true); RecyclerView pathListView = view.findViewById(R.id.path_list); pathListView.setLayoutManager(new LinearLayoutManager(mActivity, RecyclerView.HORIZONTAL, false)); mPathListAdapter = new FmPathListAdapter(mModel); mPathListAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { @Override public void onChanged() { pathListView.setSelection(mPathListAdapter.getCurrentPosition()); } @Override public void onItemRangeChanged(int positionStart, int itemCount) { onChanged(); } }); pathListView.setAdapter(mPathListAdapter); MaterialButton pathEditButton = view.findViewById(R.id.uri_edit); pathEditButton.setOnClickListener(v -> { Uri currentUri = mModel.getCurrentUri(); String path = currentUri != null ? FmUtils.getDisplayablePath(currentUri) : null; new TextInputDialogBuilder(mActivity, null) .setTitle(R.string.go_to_path) .setInputText(path) .setPositiveButton(R.string.go, (dialog, which, inputText, isChecked) -> { if (TextUtils.isEmpty(inputText)) { return; } goToRawPath(inputText.toString().trim()); }) .setNegativeButton(R.string.close, null) .show(); }); mFabGroup = view.findViewById(R.id.fab); mFabGroup.inflate(R.menu.fragment_fm_speed_dial); mFabGroup.setOnActionSelectedListener(this); mFabGroup.setContentDescription(getString(R.string.add)); UiUtils.applyWindowInsetsAsMargin(view.findViewById(R.id.fab_holder)); mEmptyView = view.findViewById(android.R.id.empty); mEmptyViewIcon = view.findViewById(R.id.icon); mEmptyViewTitle = view.findViewById(R.id.title); mEmptyViewDetails = view.findViewById(R.id.message); mRecyclerView = view.findViewById(R.id.list_item); mRecyclerView.setLayoutManager(UIUtils.getGridLayoutAt450Dp(mActivity)); mAdapter = new FmAdapter(mModel, mActivity); mAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataChangedObserver() { @Override public void onChanged() { if (mAdapter.isInSelectionMode()) { // Avoid setting a selection in selection mode (directory cannot be changed // in selection mode anyway). return; } if (scrollPosition.get() != RecyclerView.NO_POSITION) { // Update scroll position mRecyclerView.setSelection(scrollPosition.get()); scrollPosition.set(RecyclerView.NO_POSITION); } else { mRecyclerView.setSelection(mModel.getCurrentScrollPosition()); } } }); mRecyclerView.setAdapter(mAdapter); mRecyclerView.addOnScrollListener(new androidx.recyclerview.widget.RecyclerView.OnScrollListener() { @Override public void onScrolled(@NonNull androidx.recyclerview.widget.RecyclerView recyclerView, int dx, int dy) { if (mFolderShortInfo == null) { return; } if (dy < 0 && mFolderShortInfo.canWrite && !mFabGroup.isShown()) { mFabGroup.show(); } else if (dy > 0 && mFabGroup.isShown()) { mFabGroup.hide(); } } }); mMultiSelectionView = view.findViewById(R.id.selection_view); mMultiSelectionView.setOnItemSelectedListener(this); mMultiSelectionView.setOnSelectionModeChangeListener(this); mMultiSelectionView.setAdapter(mAdapter); mMultiSelectionView.updateCounter(true); mMultiSelectionView.getViewTreeObserver().addOnGlobalLayoutListener(mMultiSelectionViewChangeListener); BatchOpsHandler batchOpsHandler = new BatchOpsHandler(mMultiSelectionView); mMultiSelectionView.setOnSelectionChangeListener(batchOpsHandler); mActivity.addMenuProvider(this, getViewLifecycleOwner(), Lifecycle.State.RESUMED); // Set observer mModel.getLastUriLiveData().observe(getViewLifecycleOwner(), uri1 -> { // force disable empty view if (mEmptyView.isShown()) { mEmptyView.setVisibility(View.GONE); } // Reset subtitle Optional.ofNullable(mActivity.getSupportActionBar()).ifPresent(actionBar -> actionBar.setSubtitle(R.string.loading)); if (uri1 == null) { return; } if (mRecyclerView != null) { View v = mRecyclerView.getChildAt(0); if (v != null) { mModel.setScrollPosition(uri1, mRecyclerView.getChildAdapterPosition(v)); } mAdapter.setFmList(Collections.emptyList()); } if (mMultiSelectionView.isShown()) { mMultiSelectionView.cancel(); } }); mModel.getFmItemsLiveData().observe(getViewLifecycleOwner(), fmItems -> { if (mSwipeRefresh != null) { mSwipeRefresh.setRefreshing(false); } mAdapter.setFmList(fmItems); if (fmItems.isEmpty()) { handleEmptyView(R.drawable.ic_file, getString(R.string.empty_folder), null); } }); mModel.getFmErrorLiveData().observe(getViewLifecycleOwner(), throwable -> { if (mSwipeRefresh != null) { mSwipeRefresh.setRefreshing(false); } handleEmptyView(io.github.muntashirakon.ui.R.drawable.ic_caution, throwable.getMessage(), throwable); }); mModel.getUriLiveData().observe(getViewLifecycleOwner(), uri1 -> { FmActivity.Options options1 = mModel.getOptions(); String alternativeRootName = options1.isVfs() ? options1.uri.getLastPathSegment() : null; Optional.ofNullable(mActivity.getSupportActionBar()).ifPresent(actionBar -> { String title = uri1.getLastPathSegment(); if (TextUtils.isEmpty(title)) { title = alternativeRootName != null ? alternativeRootName : "Root"; } actionBar.setTitle(title); }); if (mSwipeRefresh != null) { mSwipeRefresh.setRefreshing(true); } mPathListAdapter.setCurrentUri(uri1); mPathListAdapter.setAlternativeRootName(alternativeRootName); mGoUpBackPressedCallback.setEnabled(mPathListAdapter.getCurrentPosition() > 0); }); mModel.getFolderShortInfoLiveData().observe(getViewLifecycleOwner(), folderShortInfo -> { mFolderShortInfo = folderShortInfo; StringBuilder subtitle = new StringBuilder(); // 1. Size if (folderShortInfo.size > 0) { subtitle.append(Formatter.formatShortFileSize(requireContext(), folderShortInfo.size)).append(" • "); } // 2. Folders and files if (folderShortInfo.folderCount > 0 && folderShortInfo.fileCount > 0) { subtitle.append(getResources().getQuantityString(R.plurals.folder_count, folderShortInfo.folderCount, folderShortInfo.folderCount)) .append(", ") .append(getResources().getQuantityString(R.plurals.file_count, folderShortInfo.fileCount, folderShortInfo.fileCount)); } else if (folderShortInfo.folderCount > 0) { subtitle.append(getResources().getQuantityString(R.plurals.folder_count, folderShortInfo.folderCount, folderShortInfo.folderCount)); } else if (folderShortInfo.fileCount > 0) { subtitle.append(getResources().getQuantityString(R.plurals.file_count, folderShortInfo.fileCount, folderShortInfo.fileCount)); } else { subtitle.append(getString(R.string.empty_folder)); } // 3. Mode if (folderShortInfo.canRead || folderShortInfo.canWrite) { subtitle.append(" • "); if (folderShortInfo.canRead) { subtitle.append("R"); } if (folderShortInfo.canWrite) { subtitle.append("W"); } } if (!folderShortInfo.canWrite) { if (mFabGroup.isShown()) { mFabGroup.hide(); } } else { if (!mFabGroup.isShown()) { mFabGroup.show(); } } Optional.ofNullable(mActivity.getSupportActionBar()).ifPresent(actionBar -> actionBar.setSubtitle(subtitle) ); }); mModel.getDisplayPropertiesLiveData().observe(getViewLifecycleOwner(), uri1 -> { FilePropertiesDialogFragment dialogFragment = FilePropertiesDialogFragment.getInstance(uri1); dialogFragment.show(mActivity.getSupportFragmentManager(), FilePropertiesDialogFragment.TAG); }); mModel.getShortcutCreatorLiveData().observe(getViewLifecycleOwner(), pathBitmapPair -> { Path path = pathBitmapPair.first; Bitmap icon = pathBitmapPair.second; FmShortcutInfo shortcutInfo = new FmShortcutInfo(path, null); if (icon != null) { shortcutInfo.setIcon(icon); } else { Drawable drawable = Objects.requireNonNull(ContextCompat.getDrawable(requireContext(), path.isDirectory() ? R.drawable.ic_folder : R.drawable.ic_file)); shortcutInfo.setIcon(UIUtils.getBitmapFromDrawable(drawable)); } CreateShortcutDialogFragment dialog = CreateShortcutDialogFragment.getInstance(shortcutInfo); dialog.show(getChildFragmentManager(), CreateShortcutDialogFragment.TAG); }); mModel.getSharableItemsLiveData().observe(getViewLifecycleOwner(), sharableItems -> mActivity.startActivity(sharableItems.toSharableIntent())); mModel.setOptions(options, uri); } @Override public void onStop() { super.onStop(); if (mModel != null && mRecyclerView != null) { Prefs.FileManager.setLastOpenedPath(mModel.getOptions(), mModel.getCurrentUri(), getRecyclerViewFirstChildPosition()); } } @Override public void onDestroyView() { if (mMultiSelectionView != null) { mMultiSelectionView.getViewTreeObserver().removeOnGlobalLayoutListener(mMultiSelectionViewChangeListener); } super.onDestroyView(); } @Override public void onSaveInstanceState(@NonNull Bundle outState) { if (mModel != null) { outState.putParcelable(ARG_URI, mModel.getCurrentUri()); outState.putParcelable(ARG_OPTIONS, mModel.getOptions()); } if (mRecyclerView != null) { View v = mRecyclerView.getChildAt(0); if (v != null) { outState.putInt(ARG_POSITION, mRecyclerView.getChildAdapterPosition(v)); } } } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); // Handle back press: The order MUST be kept same requireActivity().getOnBackPressedDispatcher().addCallback(this, mGoUpBackPressedCallback); requireActivity().getOnBackPressedDispatcher().addCallback(this, mExitSelectionBackPressedCallback); } @Override public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { inflater.inflate(R.menu.activity_fm_actions, menu); } @Override public void onPrepareMenu(@NonNull Menu menu) { MenuItem pasteMenu = menu.findItem(R.id.action_paste); if (pasteMenu != null) { FmTasks.FmTask fmTask = FmTasks.getInstance().peek(); pasteMenu.setEnabled(mFolderShortInfo != null && fmTask != null && mFolderShortInfo.canWrite && fmTask.canPaste()); } } @Override public boolean onMenuItemSelected(@NonNull MenuItem item) { int id = item.getItemId(); if (id == R.id.action_refresh) { mModel.reload(); return true; } else if (id == R.id.action_shortcut) { Uri uri = mPathListAdapter.getCurrentUri(); if (uri != null) { mModel.createShortcut(uri); } return true; } else if (id == R.id.action_list_options) { FmListOptions listOptions = new FmListOptions(); listOptions.setListOptionActions(mModel); listOptions.show(getChildFragmentManager(), FmListOptions.TAG); return true; } else if (id == R.id.action_paste) { FmTasks.FmTask task = FmTasks.getInstance().dequeue(); if (task != null) { startBatchPaste(task); } return true; } else if (id == R.id.action_new_window) { Intent intent = new Intent(mActivity, FmActivity.class); if (!mModel.getOptions().isVfs()) { intent.setDataAndType(mModel.getCurrentUri(), DocumentsContract.Document.MIME_TYPE_DIR); } intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); startActivity(intent); return true; } else if (id == R.id.action_add_to_favorites) { Uri uri = mPathListAdapter.getCurrentUri(); if (uri != null) { mModel.addToFavorite(Paths.get(uri), mModel.getOptions()); } return true; } else if (id == R.id.action_settings) { Intent intent = SettingsActivity.getSettingsIntent(requireContext(), "files_prefs"); startActivity(intent); return true; } return false; } @Override public boolean onActionSelected(@NonNull SpeedDialActionItem actionItem) { int id = actionItem.getId(); if (id == R.id.action_file) { NewFileDialogFragment dialog = NewFileDialogFragment.getInstance(this::createNewFile); dialog.show(getChildFragmentManager(), NewFileDialogFragment.TAG); } else if (id == R.id.action_folder) { NewFolderDialogFragment dialog = NewFolderDialogFragment.getInstance(this::createNewFolder); dialog.show(getChildFragmentManager(), NewFolderDialogFragment.TAG); } else if (id == R.id.action_symbolic_link) { Uri uri = mPathListAdapter.getCurrentUri(); if (uri == null) { return false; } Path path = Paths.get(uri); if (path.getFile() == null) { UIUtils.displayLongToast(R.string.symbolic_link_not_supported); return false; } NewSymbolicLinkDialogFragment dialog = NewSymbolicLinkDialogFragment.getInstance(this::createNewSymbolicLink); dialog.show(getChildFragmentManager(), NewSymbolicLinkDialogFragment.TAG); } return false; } @Override public void onSelectionModeEnabled() { mExitSelectionBackPressedCallback.setEnabled(true); } @Override public void onSelectionModeDisabled() { mExitSelectionBackPressedCallback.setEnabled(false); } @Override public boolean onNavigationItemSelected(@NonNull MenuItem item) { int id = item.getItemId(); List selectedFiles = mModel.getSelectedItems(); if (selectedFiles.isEmpty()) { // Do nothing on empty list return false; } if (id == R.id.action_share) { mModel.shareFiles(selectedFiles); } else if (id == R.id.action_rename) { RenameDialogFragment dialog = RenameDialogFragment.getInstance(null, (prefix, extension) -> startBatchRenaming(selectedFiles, prefix, extension)); dialog.show(getChildFragmentManager(), RenameDialogFragment.TAG); } else if (id == R.id.action_delete) { new MaterialAlertDialogBuilder(mActivity) .setTitle(R.string.title_confirm_deletion) .setMessage(R.string.are_you_sure) .setPositiveButton(R.string.cancel, null) .setNegativeButton(R.string.confirm_file_deletion, (dialog, which) -> startBatchDeletion(selectedFiles)) .show(); } else if (id == R.id.action_cut) { FmTasks.FmTask fmTask = new FmTasks.FmTask(TYPE_CUT, selectedFiles); FmTasks.getInstance().enqueue(fmTask); UIUtils.displayShortToast(R.string.copied_to_clipboard); } else if (id == R.id.action_copy) { FmTasks.FmTask fmTask = new FmTasks.FmTask(FmTasks.FmTask.TYPE_COPY, selectedFiles); FmTasks.getInstance().enqueue(fmTask); UIUtils.displayShortToast(R.string.copied_to_clipboard); } else if (id == R.id.action_copy_path) { List paths = new ArrayList<>(selectedFiles.size()); for (Path path : selectedFiles) { paths.add(FmUtils.getDisplayablePath(path)); } Utils.copyToClipboard(mActivity, "Paths", TextUtils.join("\n", paths)); } return false; } @Override public boolean onQueryTextSubmit(String query) { return false; } @Override public boolean onQueryTextChange(String newText) { // TODO: 11/7/21 return false; } @Override public void onRefresh() { if (mModel != null) mModel.reload(); } public int getRecyclerViewFirstChildPosition() { if (mRecyclerView != null) { View v = mRecyclerView.getChildAt(0); return mRecyclerView.getChildAdapterPosition(v); } return RecyclerView.NO_POSITION; } private void goToRawPath(@NonNull String p) { Uri uncheckedUri = Uri.parse(p); if (uncheckedUri.getScheme() != null) { Uri checkedUri = FmUtils.sanitizeContentInput(uncheckedUri); if (checkedUri != null) { // Valid path mModel.loadFiles(checkedUri); } // else bad URI return; } // Bad Uri, consider it to be a file:// if (p.startsWith(File.separator)) { // absolute file Uri checkedUri = FmUtils.sanitizeContentInput(uncheckedUri.buildUpon().scheme(ContentResolver.SCHEME_FILE).build()); if (checkedUri != null) { mModel.loadFiles(checkedUri); } // else bad file return; } // Relative path String goodPath = Paths.sanitize(p, false); if (goodPath == null || goodPath.equals(File.separator)) { // No relative path means current path which is already loaded return; } Uri currentUri = mModel.getCurrentUri(); if (DocumentsContractCompat.isDocumentUri(requireContext(), currentUri)) { List pathSegments = currentUri.getPathSegments(); if (pathSegments.size() == 4) { // For a tree URI, the 3rd index is the path String lastPathSegment = pathSegments.get(3) + File.separator + goodPath; Uri.Builder b = new Uri.Builder() .scheme(currentUri.getScheme()) .authority(currentUri.getAuthority()) .appendPath(pathSegments.get(0)) .appendPath(pathSegments.get(1)) .appendPath(pathSegments.get(2)) .appendPath(lastPathSegment); mModel.loadFiles(b.build()); } // Other document Uris don't support navigation nor do they support folders/trees return; } // For others, simply append path segments at the end @SuppressWarnings("SuspiciousRegexArgument") // We aren't on Windows String[] segments = goodPath.split(File.separator); Uri.Builder b = currentUri.buildUpon(); for (String segment : segments) { b.appendPath(segment); } mModel.loadFiles(b.build()); } private void handleEmptyView(@DrawableRes int icon, @Nullable CharSequence title, @Nullable Throwable th) { if (!mEmptyView.isShown()) { mEmptyView.setVisibility(View.VISIBLE); } mEmptyViewIcon.setImageResource(icon); mEmptyViewTitle.setText(title); if (th == null) { mEmptyViewDetails.setVisibility(View.GONE); return; } // Only log the first three lines StackTraceElement[] arr = th.getStackTrace(); StringBuilder report = new StringBuilder(th + "\n"); int i = 0; for (StackTraceElement traceElement : arr) { if (i == 3) break; report.append(" at ").append(traceElement.toString()).append("\n"); ++i; } Throwable cause = th; while ((cause = cause.getCause()) != null) { report.append(" Caused by: ").append(cause).append("\n"); arr = cause.getStackTrace(); i = 0; for (StackTraceElement stackTraceElement : arr) { if (i == 3) break; report.append(" at ").append(stackTraceElement.toString()).append("\n"); ++i; } } mEmptyViewDetails.setVisibility(View.VISIBLE); mEmptyViewDetails.setText(report); } private void createNewFolder(String name) { Uri uri = mPathListAdapter.getCurrentUri(); if (uri == null) { return; } Path path = Paths.get(uri); String displayName = findNextBestDisplayName(path, name, null); try { Path newDir = path.createNewDirectory(displayName); UIUtils.displayShortToast(R.string.done); mModel.reload(newDir.getName()); } catch (IOException e) { e.printStackTrace(); UIUtils.displayShortToast(R.string.failed); } } private void createNewFile(String prefix, @Nullable String extension, String template) { Uri uri = mPathListAdapter.getCurrentUri(); if (uri == null) { return; } Path path = Paths.get(uri); String displayName = findNextBestDisplayName(path, prefix, extension); try { Path newFile = path.createNewFile(displayName, null); FileUtils.copyFromAsset(requireContext(), "blanks/" + template, newFile); UIUtils.displayShortToast(R.string.done); mModel.reload(newFile.getName()); } catch (IOException e) { e.printStackTrace(); UIUtils.displayShortToast(R.string.failed); } } private void createNewSymbolicLink(String prefix, @Nullable String extension, String targetPath) { Uri uri = mPathListAdapter.getCurrentUri(); if (uri == null) { return; } Path basePath = Paths.get(uri); String displayName = findNextBestDisplayName(basePath, prefix, extension); Path sourcePath = Paths.build(basePath, displayName); if (sourcePath != null && sourcePath.createNewSymbolicLink(targetPath)) { UIUtils.displayShortToast(R.string.done); mModel.reload(sourcePath.getName()); } else { UIUtils.displayShortToast(R.string.failed); } } private void startBatchDeletion(@NonNull List paths) { // TODO: 27/6/23 Ideally, these should be done in a bound service AtomicReference> deletionThread = new AtomicReference<>(); View view = View.inflate(requireContext(), R.layout.dialog_progress, null); LinearProgressIndicator progress = view.findViewById(R.id.progress_linear); TextView label = view.findViewById(android.R.id.text1); TextView counter = view.findViewById(android.R.id.text2); counter.setText(String.format(Locale.getDefault(), "%d/%d", 0, paths.size())); AlertDialog dialog = new MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.delete) .setView(view) .setPositiveButton(R.string.action_stop_service, (dialog1, which) -> { if (deletionThread.get() != null) { deletionThread.get().cancel(true); } }) .setCancelable(false) .show(); deletionThread.set(ThreadUtils.postOnBackgroundThread(() -> { WeakReference progressRef = new WeakReference<>(progress); WeakReference labelRef = new WeakReference<>(label); WeakReference counterRef = new WeakReference<>(counter); WeakReference dialogRef = new WeakReference<>(dialog); try { LinearProgressIndicator p = progressRef.get(); if (p != null) { p.setMax(paths.size()); p.setProgress(0); p.setIndeterminate(false); } int i = 1; for (Path path : paths) { // Update label TextView l = labelRef.get(); if (l != null) { ThreadUtils.postOnMainThread(() -> l.setText(path.getName())); } if (ThreadUtils.isInterrupted()) { break; } // Sleep, delete, progress SystemClock.sleep(2_000); if (ThreadUtils.isInterrupted()) { break; } path.delete(); TextView c = counterRef.get(); int finalI = i; ThreadUtils.postOnMainThread(() -> { if (c != null) { c.setText(String.format(Locale.getDefault(), "%d/%d", finalI, paths.size())); } if (p != null) { p.setProgress(finalI); } }); ++i; if (ThreadUtils.isInterrupted()) { break; } } } finally { AlertDialog d = dialogRef.get(); if (d != null) { ThreadUtils.postOnMainThread(() -> { d.dismiss(); UIUtils.displayShortToast(R.string.deleted_successfully); mModel.reload(); }); } } })); } private void startBatchRenaming(List paths, String prefix, @Nullable String extension) { AtomicReference> renameThread = new AtomicReference<>(); View view = View.inflate(requireContext(), R.layout.dialog_progress, null); LinearProgressIndicator progress = view.findViewById(R.id.progress_linear); TextView label = view.findViewById(android.R.id.text1); TextView counter = view.findViewById(android.R.id.text2); counter.setText(String.format(Locale.getDefault(), "%d/%d", 0, paths.size())); AlertDialog dialog = new MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.rename) .setView(view) .setPositiveButton(R.string.action_stop_service, (dialog1, which) -> { if (renameThread.get() != null) { renameThread.get().cancel(true); } }) .setCancelable(false) .show(); renameThread.set(ThreadUtils.postOnBackgroundThread(() -> { WeakReference progressRef = new WeakReference<>(progress); WeakReference labelRef = new WeakReference<>(label); WeakReference counterRef = new WeakReference<>(counter); WeakReference dialogRef = new WeakReference<>(dialog); try { LinearProgressIndicator p = progressRef.get(); if (p != null) { p.setMax(paths.size()); p.setProgress(0); p.setIndeterminate(false); } int i = 1; for (Path path : paths) { // Update label TextView l = labelRef.get(); if (l != null) { ThreadUtils.postOnMainThread(() -> l.setText(path.getName())); } if (ThreadUtils.isInterrupted()) { break; } // Sleep, rename, progress SystemClock.sleep(2_000); if (ThreadUtils.isInterrupted()) { break; } Path basePath = path.getParent(); if (basePath != null) { String displayName = findNextBestDisplayName(basePath, prefix, extension, i); path.renameTo(displayName); } TextView c = counterRef.get(); int finalI = i; ThreadUtils.postOnMainThread(() -> { if (c != null) { c.setText(String.format(Locale.getDefault(), "%d/%d", finalI, paths.size())); } if (p != null) { p.setProgress(finalI); } }); ++i; if (ThreadUtils.isInterrupted()) { break; } } } finally { AlertDialog d = dialogRef.get(); if (d != null) { ThreadUtils.postOnMainThread(() -> { d.dismiss(); UIUtils.displayShortToast(R.string.renamed_successfully); mModel.reload(); }); } } })); } private void startBatchPaste(@NonNull FmTasks.FmTask task) { Uri uri = mPathListAdapter.getCurrentUri(); if (uri == null) { return; } AtomicReference> pasteThread = new AtomicReference<>(); View view = View.inflate(requireContext(), R.layout.dialog_progress, null); LinearProgressIndicator progress = view.findViewById(R.id.progress_linear); TextView label = view.findViewById(android.R.id.text1); TextView counter = view.findViewById(android.R.id.text2); counter.setText(String.format(Locale.getDefault(), "%d/%d", 0, task.files.size())); AlertDialog dialog = new MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.paste) .setView(view) .setPositiveButton(R.string.action_stop_service, (dialog1, which) -> { if (pasteThread.get() != null) { pasteThread.get().cancel(true); } }) .setCancelable(false) .show(); pasteThread.set(ThreadUtils.postOnBackgroundThread(() -> { WeakReference progressRef = new WeakReference<>(progress); WeakReference labelRef = new WeakReference<>(label); WeakReference counterRef = new WeakReference<>(counter); WeakReference dialogRef = new WeakReference<>(dialog); Path targetPath = Paths.get(uri); try { LinearProgressIndicator p = progressRef.get(); if (p != null) { p.setMax(task.files.size()); p.setProgress(0); p.setIndeterminate(false); } int i = 1; for (Path sourcePath : task.files) { // Update label TextView l = labelRef.get(); if (l != null) { ThreadUtils.postOnMainThread(() -> l.setText(sourcePath.getName())); } if (ThreadUtils.isInterrupted()) { break; } // Sleep, copy, progress SystemClock.sleep(2_000); if (ThreadUtils.isInterrupted()) { break; } if (!copy(sourcePath, targetPath)) { // Failed to copy, abort ThreadUtils.postOnMainThread(() -> new MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.error) .setMessage(getString(R.string.failed_to_copy_specified_file, sourcePath.getName())) .setPositiveButton(R.string.close, null) .show()); return; } if (task.type == TYPE_CUT) { if (!sourcePath.delete()) { // Failed to move, abort ThreadUtils.postOnMainThread(() -> new MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.error) .setMessage(getString(R.string.failed_to_delete_specified_file_after_copying, sourcePath.getName())) .setPositiveButton(R.string.close, null) .show()); return; } } TextView c = counterRef.get(); int finalI = i; ThreadUtils.postOnMainThread(() -> { if (c != null) { c.setText(String.format(Locale.getDefault(), "%d/%d", finalI, task.files.size())); } if (p != null) { p.setProgress(finalI); } }); ++i; if (ThreadUtils.isInterrupted()) { break; } } UIUtils.displayShortToast(task.type == TYPE_CUT ? R.string.moved_successfully : R.string.copied_successfully); } finally { AlertDialog d = dialogRef.get(); if (d != null) { ThreadUtils.postOnMainThread(() -> { d.dismiss(); mModel.reload(); }); } } })); } @WorkerThread private boolean copy(Path source, Path dest) { String name = source.getName(); if (dest.hasFile(name)) { // Duplicate found. Ask user for what to do. CountDownLatch waitForUser = new CountDownLatch(1); AtomicReference keepBoth = new AtomicReference<>(null); ThreadUtils.postOnMainThread(() -> new MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.conflict_detected_while_copying) .setMessage(getString(R.string.conflict_detected_while_copying_message, name)) .setCancelable(false) .setOnDismissListener(dialog -> waitForUser.countDown()) .setPositiveButton(R.string.replace, (dialog, which) -> keepBoth.set(false)) .setNegativeButton(R.string.action_stop_service, (dialog, which) -> keepBoth.set(null)) .setNeutralButton(R.string.copy_keep_both_file, (dialog, which) -> keepBoth.set(true)) .show()); try { waitForUser.await(); } catch (InterruptedException ignore) { } if (keepBoth.get() == null) { // Abort copying return false; } if (keepBoth.get()) { // Keep both String prefix; String extension; if (!source.isDirectory()) { prefix = Paths.trimPathExtension(name); extension = Paths.getPathExtension(name); } else { prefix = name; extension = null; } String newName = findNextBestDisplayName(dest, prefix, extension); try { Path newPath = source.isDirectory() ? dest.createNewDirectory(newName) : dest.createNewFile(newName, null); // Need to create that path again newPath.delete(); return source.copyTo(newPath) != null; } catch (IOException e) { e.printStackTrace(); return false; } } else { // Overwrite return source.copyTo(dest, true) != null; } } // Simply copy return source.copyTo(dest, false) != null; } private String findNextBestDisplayName(@NonNull Path basePath, @NonNull String prefix, @Nullable String extension) { return findNextBestDisplayName(basePath, prefix, extension, 1); } private String findNextBestDisplayName(@NonNull Path basePath, @NonNull String prefix, @Nullable String extension, int startIndex) { if (TextUtils.isEmpty(extension)) { extension = ""; } else extension = "." + extension; String displayName = prefix + extension; int i = startIndex; // We need to find the next best file name if current exists while (basePath.hasFile(displayName)) { displayName = String.format(Locale.ROOT, "%s (%d)%s", prefix, i, extension); ++i; } return displayName; } private class BatchOpsHandler implements MultiSelectionView.OnSelectionChangeListener { private final MenuItem mShareMenu; private final MenuItem mRenameMenu; private final MenuItem mDeleteMenu; private final MenuItem mCutMenu; private final MenuItem mCopyMenu; private final MenuItem mCopyPathsMenu; public BatchOpsHandler(@NonNull MultiSelectionView multiSelectionView) { Menu menu = multiSelectionView.getMenu(); mShareMenu = menu.findItem(R.id.action_share); mRenameMenu = menu.findItem(R.id.action_rename); mDeleteMenu = menu.findItem(R.id.action_delete); mCutMenu = menu.findItem(R.id.action_cut); mCopyMenu = menu.findItem(R.id.action_copy); mCopyPathsMenu = menu.findItem(R.id.action_copy_path); } @Override public boolean onSelectionChange(int selectionCount) { boolean nonZeroSelection = selectionCount > 0; boolean canRead = mFolderShortInfo != null && mFolderShortInfo.canRead; boolean canWrite = mFolderShortInfo != null && mFolderShortInfo.canWrite; mShareMenu.setEnabled(nonZeroSelection && canRead); mRenameMenu.setEnabled(nonZeroSelection && canWrite); mDeleteMenu.setEnabled(nonZeroSelection && canWrite); mCutMenu.setEnabled(nonZeroSelection && canWrite); mCopyMenu.setEnabled(nonZeroSelection && canRead); mCopyPathsMenu.setEnabled(nonZeroSelection); return false; } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/fm/FmItem.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.fm; import android.util.Base64; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Objects; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.PathAttributes; import io.github.muntashirakon.io.PathContentInfo; public class FmItem implements Comparable { public static final int UNRESOLVED = -2; final boolean isDirectory; @NonNull public final Path path; @Nullable private String mTag; @Nullable private PathContentInfo mContentInfo; @Nullable private PathAttributes mAttributes; @Nullable private String mName; private int mChildCount = UNRESOLVED; private boolean mCached = false; public FmItem(@NonNull Path path) { this.path = path; isDirectory = path.isDirectory(); } FmItem(@NonNull Path path, @NonNull PathAttributes attributes) { this.path = path; mAttributes = attributes; mName = mAttributes.name; isDirectory = mAttributes.isDirectory; } @NonNull public String getTag() { if (mTag == null) { mTag = "fm_" + Base64.encodeToString(path.toString().getBytes(StandardCharsets.UTF_8), Base64.NO_WRAP); } return mTag; } @NonNull public String getName() { if (mName != null) { return mName; } if (mAttributes != null) { return mAttributes.name; } return path.getName(); } public long getLastModified() { if (mAttributes != null) { return mAttributes.lastModified; } return path.lastModified(); } public long getSize() { if (mAttributes != null) { return mAttributes.size; } return path.length(); } public int getChildCount() { if (!isDirectory) { return 0; } if (mChildCount == UNRESOLVED) { mChildCount = path.listFiles().length; } return mChildCount; } @Nullable public PathContentInfo getContentInfo() { return mContentInfo; } public void setContentInfo(@Nullable PathContentInfo contentInfo) { this.mContentInfo = contentInfo; } public void cache() { try { getTag(); fetchAttributes(); if (ThreadUtils.isInterrupted()) { return; } if (isDirectory) { getChildCount(); } else mChildCount = 0; } finally { mCached = true; } } public boolean isCached() { return mCached; } private void fetchAttributes() { try { // WARNING: The attributes can be changed in SAF anytime from anywhere. // But we don't care because speed matters more. mAttributes = path.getAttributes(); mName = mAttributes.name; } catch (IOException e) { e.printStackTrace(); mName = path.getName(); } } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof FmItem)) return false; FmItem item = (FmItem) o; return path.equals(item.path); } @Override public int hashCode() { return Objects.hash(path); } @Override public int compareTo(FmItem o) { if (equals(o)) return 0; int typeComp = -Boolean.compare(isDirectory, o.isDirectory); if (typeComp == 0) { return path.getName().compareToIgnoreCase(o.path.getName()); } else return typeComp; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/fm/FmListOptions.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.fm; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.LinkedHashMap; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.misc.ListOptions; public class FmListOptions extends ListOptions { public static final String TAG = FmListOptions.class.getSimpleName(); @IntDef({SORT_BY_NAME, SORT_BY_LAST_MODIFIED, SORT_BY_SIZE, SORT_BY_TYPE}) @Retention(RetentionPolicy.SOURCE) public @interface SortOrder { } public static final int SORT_BY_NAME = 0; public static final int SORT_BY_LAST_MODIFIED = 1; public static final int SORT_BY_SIZE = 2; public static final int SORT_BY_TYPE = 3; @IntDef(flag = true, value = {OPTIONS_DISPLAY_DOT_FILES, OPTIONS_FOLDERS_FIRST}) @Retention(RetentionPolicy.SOURCE) public @interface Options { } public static final int OPTIONS_DISPLAY_DOT_FILES = 1 << 0; public static final int OPTIONS_FOLDERS_FIRST = 1 << 1; public static final int OPTIONS_ONLY_FOR_THIS_FOLDER = 1 << 2; // TODO: 11/12/22 private static final LinkedHashMap SORT_ITEMS_MAP = new LinkedHashMap() {{ put(SORT_BY_NAME, R.string.sort_by_filename); put(SORT_BY_LAST_MODIFIED, R.string.sort_by_last_modified); put(SORT_BY_SIZE, R.string.sort_by_file_size); put(SORT_BY_TYPE, R.string.sort_by_file_type); }}; private static final LinkedHashMap OPTIONS_MAP = new LinkedHashMap() {{ put(OPTIONS_DISPLAY_DOT_FILES, R.string.option_display_dot_files); put(OPTIONS_FOLDERS_FIRST, R.string.option_display_folders_on_top); }}; @Nullable @Override public LinkedHashMap getSortIdLocaleMap() { return SORT_ITEMS_MAP; } @Nullable @Override public LinkedHashMap getFilterFlagLocaleMap() { return null; } @Nullable @Override public LinkedHashMap getOptionIdLocaleMap() { return OPTIONS_MAP; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/fm/FmPathListAdapter.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.fm; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.provider.DocumentsContract; import android.view.LayoutInflater; import android.view.Menu; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.PopupMenu; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.color.MaterialColors; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.io.Paths; import io.github.muntashirakon.util.AdapterUtils; import io.github.muntashirakon.AppManager.utils.Utils; class FmPathListAdapter extends RecyclerView.Adapter { private final FmViewModel mViewModel; private final List mPathParts = Collections.synchronizedList(new ArrayList<>()); @Nullable private String mAlternativeRootName = null; private int mCurrentPosition = -1; @Nullable private Uri mCurrentUri; FmPathListAdapter(FmViewModel viewModel) { mViewModel = viewModel; } public void setCurrentUri(@NonNull Uri currentUri) { Uri lastPath = mCurrentUri; String lastPathStr = lastPath != null ? lastPath.toString() : null; mCurrentUri = currentUri; List paths = FmUtils.uriToPathParts(currentUri); String currentPathStr = currentUri.toString(); if (!currentPathStr.endsWith(File.separator)) { currentPathStr += File.separator; } // Two cases: // 1. currentPath is a subset of lastPath, update currentPosition // 2. Otherwise, alter pathParts and set (length - 1) as the currentPosition if (lastPathStr != null && lastPathStr.startsWith(currentPathStr)) { // Case 1 setCurrentPosition(paths.size() - 1); } else { // Case 2 mCurrentPosition = paths.size() - 1; AdapterUtils.notifyDataSetChanged(this, mPathParts, paths); } } public void setAlternativeRootName(@Nullable String alternativeRootName) { mAlternativeRootName = alternativeRootName; } @Nullable public Uri getCurrentUri() { return mCurrentUri; } public int getCurrentPosition() { return mCurrentPosition; } public Uri calculateUri(int position) { return FmUtils.uriFromPathParts(Objects.requireNonNull(mCurrentUri), mPathParts, position); } private void setCurrentPosition(int currentPosition) { int lastPosition = mCurrentPosition; mCurrentPosition = currentPosition; if (lastPosition >= 0) { notifyItemChanged(lastPosition, AdapterUtils.STUB); } notifyItemChanged(currentPosition, AdapterUtils.STUB); } @NonNull @Override public PathHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_path, parent, false); return new PathHolder(view); } @Override public void onBindViewHolder(@NonNull PathHolder holder, int position) { String actualPathPart = mPathParts.get(position); String pathPart; if (position == 0) { pathPart = mAlternativeRootName != null ? mAlternativeRootName : actualPathPart; } else pathPart = "» " + actualPathPart; holder.textView.setText(pathPart); if (position == 0 && pathPart.equals("/")) { holder.itemView.setContentDescription(holder.itemView.getContext().getString(R.string.root)); } else holder.itemView.setContentDescription(actualPathPart); holder.itemView.setOnClickListener(v -> { if (mCurrentPosition != position) { mViewModel.loadFiles(calculateUri(position)); } }); holder.itemView.setOnLongClickListener(v -> { Context context = v.getContext(); PopupMenu popupMenu = new PopupMenu(context, v); Menu menu = popupMenu.getMenu(); // Copy path menu.add(R.string.copy_this_path) .setOnMenuItemClickListener(menuItem -> { String path = FmUtils.getDisplayablePath(calculateUri(position)); Utils.copyToClipboard(context, "Path", path); return true; }); // Open in new window menu.add(R.string.open_in_new_window) .setOnMenuItemClickListener(menuItem -> { Intent intent = new Intent(context, FmActivity.class); intent.setDataAndType(calculateUri(position), DocumentsContract.Document.MIME_TYPE_DIR); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); context.startActivity(intent); return true; }); // Add to favorites menu.add(R.string.add_to_favorites) .setOnMenuItemClickListener(item -> { mViewModel.addToFavorite(Paths.get(calculateUri(position)), mViewModel.getOptions()); return true; }); // Properties menu.add(R.string.file_properties) .setOnMenuItemClickListener(menuItem -> { mViewModel.getDisplayPropertiesLiveData().setValue(calculateUri(position)); return true; }); popupMenu.show(); return true; }); holder.textView.setTextColor(mCurrentPosition == position ? MaterialColors.getColor(holder.textView, androidx.appcompat.R.attr.colorPrimary) : MaterialColors.getColor(holder.textView, android.R.attr.textColorSecondary)); } @Override public int getItemCount() { return mPathParts.size(); } public static class PathHolder extends RecyclerView.ViewHolder { public final TextView textView; public PathHolder(@NonNull View itemView) { super(itemView); this.textView = itemView.findViewById(android.R.id.text1); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/fm/FmProvider.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.fm; import android.content.ContentProvider; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.pm.ProviderInfo; import android.database.Cursor; import android.database.MatrixCursor; import android.net.Uri; import android.os.Binder; import android.os.Build; import android.os.Handler; import android.os.HandlerThread; import android.os.ParcelFileDescriptor; import android.os.Process; import android.provider.DocumentsContract; import android.provider.MediaStore; import android.provider.OpenableColumns; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import java.io.File; import java.io.FileNotFoundException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.utils.ArrayUtils; import io.github.muntashirakon.AppManager.utils.ExUtils; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; import io.github.muntashirakon.io.fs.VirtualFileSystem; // Copyright 2018 Hai Zhang // Modified from FileProvider.kt public class FmProvider extends ContentProvider { public static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".file"; @NonNull public static Uri getContentUri(@NonNull Path path) { return getContentUri(path.getUri()); } @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) @NonNull static Uri getContentUri(@NonNull Uri uri) { Uri.Builder builder = uri.buildUpon() .scheme(ContentResolver.SCHEME_CONTENT) .authority(AUTHORITY) .path(null); // Uri could be a file, content or vfs // 1. file:// Only use path // 2. content:// Use ! + authority followed by path // 3. vfs:// Use !! + authority (vfs ID) followed by path if (ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) { builder.appendPath("!" + uri.getAuthority()); } else if (VirtualFileSystem.SCHEME.equals(uri.getScheme())) { builder.appendPath("!!" + uri.getAuthority()); } for (String segment : uri.getPathSegments()) { builder.appendPath(segment); } // The rests (query params, etc.) remains the same return builder.build(); } private static final String[] DEFAULT_PROJECTION = new String[]{ OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE, MediaStore.MediaColumns.DATA, DocumentsContract.Document.COLUMN_MIME_TYPE, DocumentsContract.Document.COLUMN_LAST_MODIFIED, }; private static final String[] CHOOSER_ACTIVITY_DEFAULT_PROJECTION = new String[]{ OpenableColumns.DISPLAY_NAME }; private HandlerThread mCallbackThread; private Handler mCallbackHandler; @Override public boolean onCreate() { mCallbackThread = new HandlerThread("FmProvider.HandlerThread"); mCallbackThread.start(); mCallbackHandler = new Handler(Objects.requireNonNull(mCallbackThread.getLooper())); return true; } @Override public void shutdown() { mCallbackThread.quitSafely(); } @Override public void attachInfo(Context context, ProviderInfo info) { super.attachInfo(context, info); if (info.exported) { throw new SecurityException("Provider must not be exported"); } if (!info.grantUriPermissions) { throw new SecurityException("Provider must grant uri permissions"); } } @NonNull @Override public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) { // ContentProvider has already checked granted permissions String[] defaultProjection = getDefaultProjection(); List columns; if (projection != null) { columns = new ArrayList<>(); for (String column : projection) { if (ArrayUtils.contains(defaultProjection, column)) { columns.add(column); } } } else columns = Arrays.asList(defaultProjection); Path path = ExUtils.exceptionAsNull(() -> getFileProviderPath(uri)); if (path == null) { return new MatrixCursor(columns.toArray(new String[0]), 0); } List row = new ArrayList<>(); for (String column : columns) { switch (column) { case OpenableColumns.DISPLAY_NAME: row.add(path.getName()); break; case OpenableColumns.SIZE: row.add(path.isFile() ? path.length() : null); break; case MediaStore.MediaColumns.DATA: String filePath = path.getFilePath(); if (filePath == null || !new File(filePath).canRead() || Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { row.add(null); continue; } row.add(filePath); break; // TODO: We should actually implement a DocumentsProvider since we are handling // ACTION_OPEN_DOCUMENT. case DocumentsContract.Document.COLUMN_MIME_TYPE: row.add(path.isDirectory() ? DocumentsContract.Document.MIME_TYPE_DIR : path.getType()); break; case DocumentsContract.Document.COLUMN_LAST_MODIFIED: row.add(path.lastModified()); break; } } return new MatrixCursor(columns.toArray(new String[0]), 1) {{ addRow(row); }}; } @Nullable @Override public String getType(@NonNull Uri uri) { return ExUtils.exceptionAsNull(() -> getFileProviderPath(uri).getType()); } @Nullable @Override public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { throw new UnsupportedOperationException("No external inserts"); } @Override public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) { throw new UnsupportedOperationException("No external deletes"); } @Override public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) { throw new UnsupportedOperationException("No external updates"); } @Nullable @Override public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException { // ContentProvider has already checked granted permissions return getFileProviderPath(uri).openFileDescriptor(checkMode(mode), mCallbackHandler); } private static String[] getDefaultProjection() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && Binder.getCallingUid() == Process.SYSTEM_UID) { // com.android.internal.app.ChooserActivity.queryResolver() in Q queries with a null // projection (meaning all columns) on main thread but only actually needs the display // name (and document flags). However, if we do return all the columns, we may perform // network requests and crash it due to StrictMode. So just work around by only // returning the display name in this case. return CHOOSER_ACTIVITY_DEFAULT_PROJECTION; } else { return DEFAULT_PROJECTION; } } @NonNull private static Path getFileProviderPath(@NonNull Uri uri) throws FileNotFoundException { return Paths.getStrict(getFileProviderPathInternal(uri)); } /** * Decode path. * * @see #getContentUri(Uri) */ @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) @NonNull static Uri getFileProviderPathInternal(@NonNull Uri uri) { List pathParts = uri.getPathSegments(); int pathStartIndex = 0; String scheme = ContentResolver.SCHEME_FILE; String authority = ""; if (pathParts.size() > 0) { String firstPart = pathParts.get(0); if (firstPart.startsWith("!!")) { // Virtual File System pathStartIndex = 1; scheme = VirtualFileSystem.SCHEME; authority = firstPart.substring(2); } else if (firstPart.startsWith("!")) { // Content provider pathStartIndex = 1; scheme = ContentResolver.SCHEME_CONTENT; authority = firstPart.substring(1); } } Uri.Builder builder = uri.buildUpon() .scheme(scheme) .authority(authority) .path(null); for (int i = pathStartIndex; i < pathParts.size(); ++i) { builder.appendPath(pathParts.get(i)); } return builder.build(); } @NonNull private static String checkMode(@NonNull String mode) { // Add `t` flag if neither truncate nor append is supplied if (mode.indexOf('w') != -1 && mode.indexOf('a') == -1) { // w exists but a doesn't if (mode.indexOf('t') == -1) { return mode + 't'; } } return mode; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/fm/FmShortcutInfo.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.fm; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Parcel; import android.provider.DocumentsContract; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.os.ParcelCompat; import java.util.Objects; import java.util.UUID; import io.github.muntashirakon.AppManager.shortcut.ShortcutInfo; import io.github.muntashirakon.io.Path; public class FmShortcutInfo extends ShortcutInfo { private final boolean mIsDirectory; @NonNull private final Uri mUri; @Nullable private final String mCustomMimeType; public FmShortcutInfo(@NonNull Path path, @Nullable String customMimeType) { mIsDirectory = path.isDirectory(); mCustomMimeType = customMimeType; mUri = path.getUri(); setName(path.getName()); setId(UUID.randomUUID().toString()); } protected FmShortcutInfo(Parcel in) { super(in); mIsDirectory = ParcelCompat.readBoolean(in); mUri = Objects.requireNonNull(ParcelCompat.readParcelable(in, Uri.class.getClassLoader(), Uri.class)); mCustomMimeType = in.readString(); } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { super.writeToParcel(dest, flags); ParcelCompat.writeBoolean(dest, mIsDirectory); dest.writeParcelable(mUri, flags); dest.writeString(mCustomMimeType); } @Override public Intent toShortcutIntent(@NonNull Context context) { Intent intent; if (mIsDirectory) { intent = new Intent(context, FmActivity.class); intent.setDataAndType(mUri, mCustomMimeType != null ? mCustomMimeType : DocumentsContract.Document.MIME_TYPE_DIR); } else { intent = new Intent(context, OpenWithActivity.class); if (mCustomMimeType != null) { intent.setDataAndType(mUri, mCustomMimeType); } else intent.setData(mUri); } intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); return intent; } public static final Creator CREATOR = new Creator() { @Override public FmShortcutInfo createFromParcel(Parcel source) { return new FmShortcutInfo(source); } @Override public FmShortcutInfo[] newArray(int size) { return new FmShortcutInfo[size]; } }; } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/fm/FmTasks.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.fm; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Queue; import io.github.muntashirakon.io.Path; public class FmTasks { // It's okay to use a singleton class since we won't be following application lifecycle. // If the app instance is destroyed, tasks will cease to exist. private static final FmTasks sInstance = new FmTasks(); public static FmTasks getInstance() { return sInstance; } private final Queue taskList = new LinkedList<>(); public void enqueue(FmTask fmTask) { // Currently, we only allow a single task. So, clear the queue first. taskList.clear(); taskList.add(fmTask); } @Nullable public FmTask peek() { return taskList.peek(); } @Nullable public FmTask dequeue() { FmTask task = peek(); if (task != null && task.type == FmTask.TYPE_COPY) { // Copy is allowed multiple times but others aren't return task; } return taskList.poll(); } public boolean isEmpty() { return taskList.isEmpty(); } public static class FmTask { @IntDef({TYPE_COPY, TYPE_CUT}) @Retention(RetentionPolicy.SOURCE) public @interface TaskType { } public static final int TYPE_COPY = 0; public static final int TYPE_CUT = 1; @TaskType public final int type; public final long timestamp; public final List files; private int mFlags; public FmTask(@TaskType int type, List files) { this.type = type; this.timestamp = System.currentTimeMillis(); this.files = new ArrayList<>(files); } public boolean canPaste() { return type == TYPE_COPY || type == TYPE_CUT; } public void addFlag(int flag) { mFlags |= flag; } public void removeFlag(int flag) { mFlags &= ~flag; } public boolean hasFlag(int flag) { return (mFlags & flag) != 0; } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/fm/FmUtils.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.fm; import android.content.ContentResolver; import android.content.Intent; import android.content.pm.ResolveInfo; import android.net.Uri; import android.provider.DocumentsContract; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Objects; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; import io.github.muntashirakon.io.fs.VirtualFileSystem; public final class FmUtils { public static final String TAG = FmUtils.class.getSimpleName(); @NonNull public static String getDisplayablePath(@NonNull Path path) { return getDisplayablePath(path.getUri()); } @NonNull public static String getDisplayablePath(@NonNull Uri uri) { if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { return uri.getPath(); } return uri.toString(); } @SuppressWarnings("OctalInteger") @NonNull public static String getFormattedMode(int mode) { // Ref: https://man7.org/linux/man-pages/man7/inode.7.html String s = getSingleMode(mode >> 6, (mode & 04000) != 0, "s") + getSingleMode(mode >> 3, (mode & 02000) != 0, "s") + getSingleMode(mode, (mode & 01000) != 0, "t"); return String.format(Locale.ROOT, "%s (0%o)", s, mode & 07777); } @SuppressWarnings("OctalInteger") @NonNull private static String getSingleMode(int mode, boolean special, String specialChar) { boolean canExecute = (mode & 01) != 0; String execMode; if (canExecute) { execMode = special ? specialChar.toLowerCase(Locale.ROOT) : "x"; } else if (special) { execMode = specialChar.toUpperCase(Locale.ROOT); } else execMode = "-"; return ((mode & 04) != 0 ? "r" : "-") + ((mode & 02) != 0 ? "w" : "-") + execMode; } @Nullable public static Uri sanitizeContentInput(@Nullable Uri uri) { if (uri == null) { return null; } // App Manager supports three schemes: file, content and vfs (private) String scheme = uri.getScheme(); if (scheme == null) { // Scheme must be non-null unless explicitly required (such as in FmActivity) return null; } switch (scheme) { case ContentResolver.SCHEME_CONTENT: // Content schemes are handled by authority. if (FmProvider.AUTHORITY.equals(uri.getAuthority())) { // Sanitize the path part which must be an absolute URL Uri realUri = FmProvider.getFileProviderPathInternal(uri); Uri fixedUri = sanitizeContentInput(realUri); return fixedUri != null ? FmProvider.getContentUri(fixedUri) : null; } // If it's not App Manager itself, return as is. return uri; case ContentResolver.SCHEME_FILE: { // Sanitize the path which must be an absolute URL String path = uri.getPath(); path = Paths.relativePath(path, File.separator); return uri.buildUpon().path(path).build(); } case VirtualFileSystem.SCHEME: { if (uri.getAuthority() == null) { // VFS must have an authority return null; } // VFS path must be absolute URL String path = uri.getPath(); if (!path.startsWith(File.separator)) { path = File.separator + path; } path = Paths.relativePath(path, File.separator); return uri.buildUpon().path(path).build(); } case "package": if (!uri.isHierarchical()) { // package:package-name is not a hierarchical format return uri; } default: Log.i(TAG, "Invalid/unsupported scheme: " + scheme); // Invalid path return null; } } @SuppressWarnings("SuspiciousRegexArgument") // We're not on Windows public static List uriToPathParts(@NonNull Uri uri) { switch (uri.getScheme()) { case ContentResolver.SCHEME_CONTENT: { if (isDocumentsProvider(uri.getAuthority())) { List paths = uri.getPathSegments(); if (paths.size() == 2) { if ("document".equals(paths.get(0))) { return Collections.singletonList(paths.get(1)); } } else if (paths.size() == 4) { if ("tree".equals(paths.get(0)) && "document".equals(paths.get(2))) { String id = paths.get(1); String actualPath = paths.get(3); if (actualPath.length() > (id.length() + 1)) { // Relative path, omitting the first `/` actualPath = actualPath.substring(id.length() + 1); } else actualPath = null; List pathParts = new ArrayList<>(); pathParts.add(id); if (actualPath != null) { pathParts.addAll(Arrays.asList(actualPath.split(File.separator))); } return pathParts; } } } // Deliberate fall-through } default: case ContentResolver.SCHEME_FILE: case VirtualFileSystem.SCHEME: { List pathParts = new ArrayList<>(); pathParts.add(File.separator); pathParts.addAll(uri.getPathSegments()); return pathParts; } } } public static Uri uriFromPathParts(@NonNull Uri baseUri, @NonNull List pathParts, int endPosition) { if (endPosition >= pathParts.size()) { throw new IndexOutOfBoundsException("EndPosition: " + endPosition + ", Size: " + pathParts.size()); } Uri.Builder builder = baseUri.buildUpon(); builder.path(null); switch (Objects.requireNonNull(baseUri.getScheme())) { case ContentResolver.SCHEME_CONTENT: { if (isDocumentsProvider(Objects.requireNonNull(baseUri.getAuthority()))) { List paths = baseUri.getPathSegments(); if (paths.size() == 2) { if ("document".equals(paths.get(0))) { // index 0 = document // index 1 = (path) pathParts.get(0) builder.appendPath("document"); builder.appendPath(pathParts.get(0)); return builder.build(); } } else if (paths.size() == 4) { if ("tree".equals(paths.get(0)) && "document".equals(paths.get(2))) { // index 0 = tree // index 1 = (id) paths.get(1) // index 2 = document // index 3 = (path) pathParts.get(0..length) builder.appendPath("tree"); builder.appendPath(paths.get(1)); builder.appendPath("document"); StringBuilder pathBuilder = new StringBuilder(); for (int i = 0; i < endPosition; ++i) { pathBuilder.append(pathParts.get(i)).append(File.separator); } pathBuilder.append(pathParts.get(endPosition)); builder.appendPath(pathBuilder.toString()); return builder.build(); } } } // Deliberate fall-through } default: case ContentResolver.SCHEME_FILE: case VirtualFileSystem.SCHEME: { if (endPosition == 0) { builder.path("/"); } else { // Append up-to endPosition, skipping the root (index = 0) for (int i = 1; i <= endPosition; ++i) { builder.appendPath(pathParts.get(i)); } } return builder.build(); } } } private static boolean isDocumentsProvider(@NonNull String authority) { final Intent intent = new Intent(DocumentsContract.PROVIDER_INTERFACE); final List infos = ContextUtils.getContext().getPackageManager().queryIntentContentProviders(intent, 0); for (ResolveInfo info : infos) { if (authority.equals(info.providerInfo.authority)) { return true; } } return false; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/fm/FmViewModel.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.fm; import android.annotation.SuppressLint; import android.app.Application; import android.content.ContentResolver; import android.database.Cursor; import android.graphics.Bitmap; import android.net.Uri; import android.provider.DocumentsContract; import android.text.TextUtils; import androidx.annotation.MainThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import androidx.core.util.Pair; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import com.j256.simplemagic.ContentType; import java.io.FileNotFoundException; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.Future; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.dex.DexUtils; import io.github.muntashirakon.AppManager.fm.icons.FmIconFetcher; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.misc.AdvancedSearchView; import io.github.muntashirakon.AppManager.misc.ListOptions; import io.github.muntashirakon.AppManager.self.filecache.FileCache; import io.github.muntashirakon.AppManager.self.imagecache.ImageLoader; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.utils.AlphanumComparator; import io.github.muntashirakon.AppManager.utils.ExUtils; import io.github.muntashirakon.AppManager.utils.FileUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.io.IoUtils; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.PathAttributes; import io.github.muntashirakon.io.Paths; import io.github.muntashirakon.io.fs.VirtualFileSystem; import io.github.muntashirakon.lifecycle.SingleLiveEvent; public class FmViewModel extends AndroidViewModel implements ListOptions.ListOptionActions { public static final String TAG = FmViewModel.class.getSimpleName(); private final Object mSizeLock = new Object(); private final MutableLiveData> mFmItemsLiveData = new MutableLiveData<>(); private final MutableLiveData mFmErrorLiveData = new MutableLiveData<>(); private final MutableLiveData mFolderShortInfoLiveData = new MutableLiveData<>(); private final MutableLiveData mUriLiveData = new MutableLiveData<>(); private final MutableLiveData mLastUriLiveData = new MutableLiveData<>(); private final MutableLiveData mDisplayPropertiesLiveData = new MutableLiveData<>(); private final SingleLiveEvent> mShortcutCreatorLiveData = new SingleLiveEvent<>(); private final SingleLiveEvent mSharableItemsLiveData = new SingleLiveEvent<>(); private final List mFmItems = new ArrayList<>(); private final Set mSelectedItems = Collections.synchronizedSet(new LinkedHashSet<>()); private final HashMap mPathScrollPositionMap = new HashMap<>(); private FmActivity.Options mOptions; private Uri mCurrentUri; @FmListOptions.SortOrder private int mSortBy; private boolean mReverseSort; @FmListOptions.Options private int mSelectedOptions; @Nullable private String mQueryString; @Nullable private String mScrollToFilename; @Nullable private Future mFmFileLoaderResult; private Future mFmFileSystemLoaderResult; private final Set mVfsIdSet = new HashSet<>(); private final FileCache mFileCache = new FileCache(); public FmViewModel(@NonNull Application application) { super(application); mSortBy = Prefs.FileManager.getSortOrder(); mReverseSort = Prefs.FileManager.isReverseSort(); mSelectedOptions = Prefs.FileManager.getOptions(); } @Override protected void onCleared() { // Ensure that file loader no longer doing anything if (mFmFileLoaderResult != null) { mFmFileLoaderResult.cancel(true); } if (mFmFileSystemLoaderResult != null) { mFmFileSystemLoaderResult.cancel(true); } // Clear VFS related data for (int vfsId : mVfsIdSet) { ExUtils.exceptionAsIgnored(() -> VirtualFileSystem.unmount(vfsId)); } IoUtils.closeQuietly(mFileCache); super.onCleared(); } @Override public void setSortBy(@FmListOptions.SortOrder int sortBy) { mSortBy = sortBy; Prefs.FileManager.setSortOrder(sortBy); ThreadUtils.postOnBackgroundThread(this::filterAndSort); } @FmListOptions.SortOrder @Override public int getSortBy() { return mSortBy; } @Override public void setReverseSort(boolean reverseSort) { mReverseSort = reverseSort; Prefs.FileManager.setReverseSort(reverseSort); ThreadUtils.postOnBackgroundThread(this::filterAndSort); } @Override public boolean isReverseSort() { return mReverseSort; } @Override public boolean isOptionSelected(@FmListOptions.Options int option) { return (mSelectedOptions & option) != 0; } @Override public void onOptionSelected(@FmListOptions.Options int option, boolean selected) { if (selected) mSelectedOptions |= option; else mSelectedOptions &= ~option; Prefs.FileManager.setOptions(mSelectedOptions); ThreadUtils.postOnBackgroundThread(this::filterAndSort); } public void setQueryString(@Nullable String queryString) { mQueryString = queryString; ThreadUtils.postOnBackgroundThread(this::filterAndSort); } @MainThread public void setOptions(@NonNull FmActivity.Options options, @Nullable Uri defaultUri) { if (mFmFileLoaderResult != null) { mFmFileLoaderResult.cancel(true); } if (mFmFileSystemLoaderResult != null) { mFmFileSystemLoaderResult.cancel(true); } mOptions = options; if (!options.isVfs()) { // No need to mount anything. Options#uri is the base URI loadFiles(defaultUri != null ? defaultUri : options.uri, null); return; } // Need to mount the file system mFmFileSystemLoaderResult = ThreadUtils.postOnBackgroundThread(() -> { try { VirtualFileSystem fs = mountVfs(); int vfsId = fs.getFsId(); mVfsIdSet.add(vfsId); // vfs ID/authority has altered Uri newUri; if (defaultUri != null) { newUri = defaultUri.buildUpon().authority(String.valueOf(vfsId)).build(); } else newUri = fs.getRootPath().getUri(); // Now load files ThreadUtils.postOnMainThread(() -> loadFiles(newUri, null)); } catch (IOException e) { handleError(e, mOptions.uri); } }); } public FmActivity.Options getOptions() { return mOptions; } public Uri getCurrentUri() { return mCurrentUri; } public void setScrollPosition(Uri uri, int currentScrollPosition) { Log.d(TAG, "Store: Scroll position = %d, uri = %s", currentScrollPosition, uri); mPathScrollPositionMap.put(uri, currentScrollPosition); } public int getCurrentScrollPosition() { Integer scrollPosition = mPathScrollPositionMap.get(mCurrentUri); Log.d(TAG, "Load: Scroll position = %d, uri = %s", scrollPosition, mCurrentUri); return scrollPosition != null ? scrollPosition : 0; } public List getSelectedItems() { return new ArrayList<>(mSelectedItems); } @Nullable public Path getLastSelectedItem() { // Last selected item is the same as the last added item. Iterator it = mSelectedItems.iterator(); Path lastItem = null; while (it.hasNext()) { lastItem = it.next(); } return lastItem; } public int getSelectedItemCount() { return mSelectedItems.size(); } public void setSelectedItem(@NonNull Path path, boolean select) { if (select) { mSelectedItems.add(path); } else { mSelectedItems.remove(path); } } public boolean isSelected(@NonNull Path path) { return mSelectedItems.contains(path); } public void clearSelections() { mSelectedItems.clear(); } @MainThread public void reload() { reload(null); } @MainThread public void reload(@Nullable String scrollToFilename) { if (mOptions != null && mCurrentUri != null) { loadFiles(mCurrentUri, scrollToFilename); } } @MainThread public void loadFiles(@NonNull Uri uri) { if (mCurrentUri != null) { // May need to reload options if (!Objects.equals(mCurrentUri.getScheme(), uri.getScheme()) || !Objects.equals(mCurrentUri.getAuthority(), uri.getAuthority())) { updateOptions(uri); return; } } loadFiles(uri, null); } @MainThread private void updateOptions(@NonNull Uri refUri) { FmActivity.Options options = new FmActivity.Options(refUri); setOptions(options, null); } @SuppressLint("WrongThread") @MainThread private void loadFiles(@NonNull Uri uri, @Nullable String scrollToFilename) { if (mFmFileLoaderResult != null) { mFmFileLoaderResult.cancel(true); } mScrollToFilename = scrollToFilename; Uri lastUri = mCurrentUri; // Send last URI mLastUriLiveData.setValue(lastUri); mCurrentUri = uri; Path currentPath; try { currentPath = Paths.getStrict(uri); } catch (IOException e) { handleError(e, uri); return; } while (currentPath.isSymbolicLink()) { try { Path realPath = currentPath.getRealPath(); if (realPath == null || realPath.equals(currentPath)) { // Not a symbolic link break; } currentPath = realPath; mCurrentUri = realPath.getUri(); } catch (IOException ignore) { // Since we couldn't resolve the path, try currentPath instead } } Path path = currentPath; mFmFileLoaderResult = ThreadUtils.postOnBackgroundThread(() -> { if (!path.isDirectory()) { IOException e; if (path.exists()) { e = new FileNotFoundException(getApplication().getString(R.string.path_not_a_folder, path.getName())); } else { e = new IOException(getApplication().getString(R.string.path_does_not_exist, path.getName())); } handleError(e, mCurrentUri); return; } // Send current URI mUriLiveData.postValue(mCurrentUri); long s, e; boolean isSaf = ContentResolver.SCHEME_CONTENT.equals(mCurrentUri.getScheme()); FolderShortInfo folderShortInfo = new FolderShortInfo(); int folderCount = 0; synchronized (mFmItems) { mFmItems.clear(); if (isSaf) { // SAF needs special handling to retrieve children s = System.currentTimeMillis(); ContentResolver resolver = getApplication().getContentResolver(); Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(mCurrentUri, DocumentsContract.getDocumentId(mCurrentUri)); Cursor c = null; try { c = resolver.query(childrenUri, null, null, null, null); String[] columns = c.getColumnNames(); while (c.moveToNext()) { String documentId = null; for (int i = 0; i < columns.length; ++i) { if (DocumentsContract.Document.COLUMN_DOCUMENT_ID.equals(columns[i])) { documentId = c.getString(i); } } if (documentId == null) { // Invalid document, probably loading still? continue; } Uri documentUri = DocumentsContract.buildDocumentUriUsingTree(mCurrentUri, documentId); Path child = Paths.getTreeDocument(path, documentUri); PathAttributes attributes = Paths.getAttributesFromSafTreeCursor(documentUri, c); FmItem fmItem = new FmItem(child, attributes); mFmItems.add(fmItem); if (fmItem.isDirectory) { ++folderCount; } if (ThreadUtils.isInterrupted()) { return; } } e = System.currentTimeMillis(); Log.d(TAG, "Time to fetch files via SAF: %d ms", e - s); } catch (Exception ex) { Log.w(TAG, "Failed query: %s", ex); } finally { IoUtils.closeQuietly(c); } } else { s = System.currentTimeMillis(); Path[] children = path.listFiles(); e = System.currentTimeMillis(); Log.d(TAG, "Time to list files: %d ms", e - s); s = System.currentTimeMillis(); for (Path child : children) { FmItem fmItem = new FmItem(child); mFmItems.add(fmItem); if (fmItem.isDirectory) { ++folderCount; } if (ThreadUtils.isInterrupted()) { return; } } e = System.currentTimeMillis(); Log.d(TAG, "Time to process file list: %d ms", e - s); } } folderShortInfo.folderCount = folderCount; folderShortInfo.fileCount = mFmItems.size() - folderCount; folderShortInfo.canRead = path.canRead(); folderShortInfo.canWrite = path.canWrite(); if (ThreadUtils.isInterrupted()) { return; } // Send folder info for the first time mFolderShortInfoLiveData.postValue(folderShortInfo); // Run filter and sorting options for fmItems s = System.currentTimeMillis(); filterAndSort(); e = System.currentTimeMillis(); Log.d(TAG, "Time to sort files: %d ms", e - s); synchronized (mSizeLock) { // Calculate size and send folder info again folderShortInfo.size = Paths.size(path); if (ThreadUtils.isInterrupted()) { return; } mFolderShortInfoLiveData.postValue(folderShortInfo); } }); } public void addToFavorite(@NonNull Path path, @NonNull FmActivity.Options options) { ThreadUtils.postOnBackgroundThread(() -> { FmFavoritesManager.addToFavorite(path, options); }); } public void createShortcut(@NonNull FmItem fmItem) { ThreadUtils.postOnBackgroundThread(() -> { Bitmap bitmap = ImageLoader.getInstance().getCachedImage(fmItem.getTag()); if (bitmap == null) { ImageLoader.ImageFetcherResult result = new FmIconFetcher(fmItem).fetchImage(fmItem.getTag()); bitmap = result.bitmap != null ? result.bitmap : result.defaultImage.getImage(); } mShortcutCreatorLiveData.postValue(new Pair<>(fmItem.path, bitmap)); }); } public void shareFiles(@NonNull List pathList) { ThreadUtils.postOnBackgroundThread(() -> { SharableItems sharableItems = new SharableItems(pathList); mSharableItemsLiveData.postValue(sharableItems); }); } public void createShortcut(@NonNull Uri uri) { createShortcut(new FmItem(Paths.get(uri))); } public LiveData> getFmItemsLiveData() { return mFmItemsLiveData; } public LiveData getFmErrorLiveData() { return mFmErrorLiveData; } public LiveData getUriLiveData() { return mUriLiveData; } public LiveData getFolderShortInfoLiveData() { return mFolderShortInfoLiveData; } public LiveData getLastUriLiveData() { return mLastUriLiveData; } public MutableLiveData getDisplayPropertiesLiveData() { return mDisplayPropertiesLiveData; } public LiveData> getShortcutCreatorLiveData() { return mShortcutCreatorLiveData; } public LiveData getSharableItemsLiveData() { return mSharableItemsLiveData; } private void handleError(@NonNull Throwable th, @NonNull Uri currentUri) { FolderShortInfo folderShortInfo = new FolderShortInfo(); if (ThreadUtils.isMainThread()) { mUriLiveData.setValue(currentUri); mFolderShortInfoLiveData.setValue(folderShortInfo); mFmErrorLiveData.setValue(th); } else { mUriLiveData.postValue(currentUri); mFolderShortInfoLiveData.postValue(folderShortInfo); mFmErrorLiveData.postValue(th); } } private void filterAndSort() { boolean displayDotFiles = (mSelectedOptions & FmListOptions.OPTIONS_DISPLAY_DOT_FILES) != 0; boolean foldersOnTop = (mSelectedOptions & FmListOptions.OPTIONS_FOLDERS_FIRST) != 0; List filteredList; synchronized (mFmItems) { if (!TextUtils.isEmpty(mQueryString)) { filteredList = AdvancedSearchView.matches(mQueryString, mFmItems, FmItem::getName, AdvancedSearchView.SEARCH_TYPE_CONTAINS); } else filteredList = new ArrayList<>(mFmItems); } if (ThreadUtils.isInterrupted()) { return; } if (!displayDotFiles) { Iterator iterator = filteredList.listIterator(); while (iterator.hasNext()) { FmItem fmItem = iterator.next(); if (fmItem.getName().startsWith(".")) { iterator.remove(); } } } if (ThreadUtils.isInterrupted()) { return; } // Sort by name first Collections.sort(filteredList, (o1, o2) -> AlphanumComparator.compareStringIgnoreCase(o1.getName(), o2.getName())); if (mSortBy == FmListOptions.SORT_BY_NAME) { if (mReverseSort) { Collections.reverse(filteredList); } } else { // Other sorting options int inverse = mReverseSort ? -1 : 1; Collections.sort(filteredList, (o1, o2) -> { Path p1 = o1.path; Path p2 = o2.path; if (mSortBy == FmListOptions.SORT_BY_LAST_MODIFIED) { return -Long.compare(o1.getLastModified(), o2.getLastModified()) * inverse; } if (mSortBy == FmListOptions.SORT_BY_SIZE) { return -Long.compare(o1.getSize(), o2.getSize()) * inverse; } if (mSortBy == FmListOptions.SORT_BY_TYPE) { return p1.getType().compareToIgnoreCase(p2.getType()) * inverse; } return 0; }); } if (foldersOnTop) { // Folders should be on top Collections.sort(filteredList, (o1, o2) -> -Boolean.compare(o1.isDirectory, o2.isDirectory)); } if (ThreadUtils.isInterrupted()) { return; } if (mScrollToFilename != null) { for (int i = 0; i < filteredList.size(); ++i) { if (mScrollToFilename.equals(filteredList.get(i).getName())) { setScrollPosition(mCurrentUri, i); break; } } mScrollToFilename = null; } mFmItemsLiveData.postValue(filteredList); } @WorkerThread @NonNull private VirtualFileSystem mountVfs() throws IOException { if (!mOptions.isVfs()) { throw new IOException("VFS expected, found regular FS."); } VirtualFileSystem fs = VirtualFileSystem.getFileSystem(mOptions.uri); if (fs == null) { // TODO: 31/5/23 Handle read-only Path filePath = Paths.getStrict(mOptions.uri); Path cachedPath = Paths.get(mFileCache.getCachedFile(filePath)); String type = cachedPath.getType(); int vfsId; if (ContentType.APK.getMimeType().equals(type)) { vfsId = VirtualFileSystem.mount(filePath.getUri(), cachedPath, ContentType.APK.getMimeType()); } else if (FileUtils.isZip(cachedPath)) { vfsId = VirtualFileSystem.mount(filePath.getUri(), cachedPath, ContentType.ZIP.getMimeType()); } else if (DexUtils.isDex(cachedPath)) { vfsId = VirtualFileSystem.mount(filePath.getUri(), cachedPath, ContentType2.DEX.getMimeType()); } else { vfsId = VirtualFileSystem.mount(filePath.getUri(), cachedPath, cachedPath.getType()); } fs = VirtualFileSystem.getFileSystem(vfsId); if (fs == null) { throw new IOException("Could not mount " + mOptions.uri); } } return fs; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/fm/FolderShortInfo.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.fm; final class FolderShortInfo { public int folderCount; public int fileCount; public boolean canRead; public boolean canWrite; public long size = -1; } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/fm/OpenWithActivity.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.fm; import android.net.Uri; import android.os.Bundle; import androidx.annotation.Nullable; import io.github.muntashirakon.AppManager.BaseActivity; import io.github.muntashirakon.AppManager.fm.dialogs.OpenWithDialogFragment; public class OpenWithActivity extends BaseActivity { @Override public boolean getTransparentBackground() { return true; } @Override protected void onAuthenticated(@Nullable Bundle savedInstanceState) { Uri uri = getIntent().getData(); if (uri == null) { finish(); return; } OpenWithDialogFragment fragment = OpenWithDialogFragment.getInstance(uri, getIntent().getType(), true); fragment.show(getSupportFragmentManager(), OpenWithDialogFragment.TAG); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/fm/SharableItems.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.fm; import android.content.Intent; import android.net.Uri; import androidx.annotation.NonNull; import java.util.ArrayList; import java.util.List; import io.github.muntashirakon.io.Path; public class SharableItems { public final List pathList; public final String mimeType; public SharableItems(List pathList) { this(pathList, findBestMimeType(pathList)); } public SharableItems(List pathList, String mimeType) { this.pathList = pathList; this.mimeType = mimeType; } public Intent toSharableIntent() { Intent intent; if (pathList.size() == 1) { intent = new Intent(Intent.ACTION_SEND) .setType(mimeType) .putExtra(Intent.EXTRA_STREAM, FmProvider.getContentUri(pathList.get(0))) .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); } else { ArrayList sharableUris = new ArrayList<>(pathList.size()); for (Path path : pathList) { sharableUris.add(FmProvider.getContentUri(path)); } intent = new Intent(Intent.ACTION_SEND_MULTIPLE) .setType(mimeType) .putParcelableArrayListExtra(Intent.EXTRA_STREAM, sharableUris) .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); } return Intent.createChooser(intent, null).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } @NonNull public static String findBestMimeType(@NonNull List pathList) { String mimeType = null; boolean splitMime = false; for (Path path : pathList) { String thisMime = path.getPathContentInfo().getMimeType(); if (thisMime == null) { thisMime = path.getType(); } if (splitMime) { thisMime = thisMime.split("/")[0]; } if (mimeType == null) { mimeType = thisMime; } else if (!mimeType.equals(thisMime)) { if (splitMime) { // The first part aren't consistent return "*/*"; } String splitMimeType = mimeType.split("/")[0]; String thisSplitMime = thisMime.split("/")[0]; if (!splitMimeType.equals(thisSplitMime)) { // The first part aren't consistent return "*/*"; } splitMime = true; mimeType = splitMimeType; } } if (mimeType == null) { mimeType = ContentType2.OTHER.getMimeType(); } return splitMime ? (mimeType + "/*") : mimeType; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/fm/dialogs/ChangeFileModeDialogFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.fm.dialogs; import android.app.Dialog; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.DialogFragment; import com.google.android.material.checkbox.MaterialCheckBox; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.materialswitch.MaterialSwitch; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.fm.FmUtils; import io.github.muntashirakon.widget.TextInputTextView; public class ChangeFileModeDialogFragment extends DialogFragment { public static final String TAG = ChangeFileModeDialogFragment.class.getSimpleName(); public interface OnChangeFileModeInterface { void onChangeMode(int mode, boolean recursive); } private static final String ARG_MODE = "mode"; private static final String ARG_DISPLAY_RECURSIVE = "recursive"; @NonNull public static ChangeFileModeDialogFragment getInstance(int mode, boolean recursive, @Nullable OnChangeFileModeInterface changeFileModeInterface) { ChangeFileModeDialogFragment fragment = new ChangeFileModeDialogFragment(); Bundle args = new Bundle(); args.putInt(ARG_MODE, mode); args.putBoolean(ARG_DISPLAY_RECURSIVE, recursive); fragment.setArguments(args); fragment.setOnChangeFileModeInterface(changeFileModeInterface); return fragment; } @Nullable private OnChangeFileModeInterface mOnChangeFileModeInterface; private View mDialogView; private MaterialCheckBox mOwnerRead; private MaterialCheckBox mOwnerWrite; private MaterialCheckBox mOwnerExec; private MaterialCheckBox mGroupRead; private MaterialCheckBox mGroupWrite; private MaterialCheckBox mGroupExec; private MaterialCheckBox mOthersRead; private MaterialCheckBox mOthersWrite; private MaterialCheckBox mOthersExec; private MaterialSwitch mUidBit; private MaterialSwitch mGidBit; private MaterialSwitch mStickyBit; private TextInputTextView mPreview; private MaterialCheckBox mRecursive; private int mOldMode; private int mMode; @SuppressWarnings("OctalInteger") @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { mMode = mOldMode = requireArguments().getInt(ARG_MODE); boolean displayRecursive = requireArguments().getBoolean(ARG_DISPLAY_RECURSIVE); mDialogView = View.inflate(requireActivity(), R.layout.dialog_change_file_mode, null); mOwnerRead = mDialogView.findViewById(R.id.user_read); mOwnerRead.setChecked((mOldMode & 0400) != 0); mOwnerRead.setOnCheckedChangeListener((buttonView, isChecked) -> updateModePreview(0400, isChecked)); mOwnerWrite = mDialogView.findViewById(R.id.user_write); mOwnerWrite.setChecked((mOldMode & 0200) != 0); mOwnerWrite.setOnCheckedChangeListener((buttonView, isChecked) -> updateModePreview(0200, isChecked)); mOwnerExec = mDialogView.findViewById(R.id.user_exec); mOwnerExec.setChecked((mOldMode & 0100) != 0); mOwnerExec.setOnCheckedChangeListener((buttonView, isChecked) -> updateModePreview(0100, isChecked)); mGroupRead = mDialogView.findViewById(R.id.group_read); mGroupRead.setChecked((mOldMode & 040) != 0); mGroupRead.setOnCheckedChangeListener((buttonView, isChecked) -> updateModePreview(040, isChecked)); mGroupWrite = mDialogView.findViewById(R.id.group_write); mGroupWrite.setChecked((mOldMode & 020) != 0); mGroupWrite.setOnCheckedChangeListener((buttonView, isChecked) -> updateModePreview(020, isChecked)); mGroupExec = mDialogView.findViewById(R.id.group_exec); mGroupExec.setChecked((mOldMode & 010) != 0); mGroupExec.setOnCheckedChangeListener((buttonView, isChecked) -> updateModePreview(010, isChecked)); mOthersRead = mDialogView.findViewById(R.id.others_read); mOthersRead.setChecked((mOldMode & 04) != 0); mOthersRead.setOnCheckedChangeListener((buttonView, isChecked) -> updateModePreview(04, isChecked)); mOthersWrite = mDialogView.findViewById(R.id.others_write); mOthersWrite.setChecked((mOldMode & 02) != 0); mOthersWrite.setOnCheckedChangeListener((buttonView, isChecked) -> updateModePreview(02, isChecked)); mOthersExec = mDialogView.findViewById(R.id.others_exec); mOthersExec.setChecked((mOldMode & 01) != 0); mOthersExec.setOnCheckedChangeListener((buttonView, isChecked) -> updateModePreview(01, isChecked)); mUidBit = mDialogView.findViewById(R.id.uid_bit); mUidBit.setChecked((mOldMode & 04000) != 0); mUidBit.setOnCheckedChangeListener((buttonView, isChecked) -> updateModePreview(04000, isChecked)); mGidBit = mDialogView.findViewById(R.id.gid_bit); mGidBit.setChecked((mOldMode & 02000) != 0); mGidBit.setOnCheckedChangeListener((buttonView, isChecked) -> updateModePreview(02000, isChecked)); mStickyBit = mDialogView.findViewById(R.id.sticky_bit); mStickyBit.setChecked((mOldMode & 01000) != 0); mStickyBit.setOnCheckedChangeListener((buttonView, isChecked) -> updateModePreview(01000, isChecked)); mPreview = mDialogView.findViewById(R.id.preview); mPreview.setText(FmUtils.getFormattedMode(mOldMode)); mRecursive = mDialogView.findViewById(R.id.checkbox); mRecursive.setVisibility(displayRecursive ? View.VISIBLE : View.GONE); return new MaterialAlertDialogBuilder(requireActivity()) .setTitle(R.string.change_mode) .setView(mDialogView) .setPositiveButton(R.string.ok, (dialog, which) -> { if (mOnChangeFileModeInterface != null) { mOnChangeFileModeInterface.onChangeMode(mMode, mRecursive.isChecked()); } }) .setNegativeButton(R.string.cancel, null) .create(); } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return mDialogView; } public void setOnChangeFileModeInterface(@Nullable OnChangeFileModeInterface changeFileModeInterface) { mOnChangeFileModeInterface = changeFileModeInterface; } private void updateModePreview(int mode, boolean enabled) { if (enabled) { mMode |= mode; } else mMode &= ~mode; mPreview.setText(FmUtils.getFormattedMode(mMode)); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/fm/dialogs/ChecksumsDialogFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.fm.dialogs; import static io.github.muntashirakon.AppManager.utils.UIUtils.displayLongToast; import android.app.Application; import android.app.Dialog; import android.net.Uri; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.os.BundleCompat; import androidx.core.util.Pair; import androidx.fragment.app.DialogFragment; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.button.MaterialButton; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.textfield.TextInputLayout; import org.bouncycastle.jce.provider.BouncyCastleProvider; import java.io.InputStream; import java.security.MessageDigest; import java.security.Provider; import java.security.Security; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.zip.CRC32; import aosp.libcore.util.HexEncoding; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.utils.ClipboardUtils; import io.github.muntashirakon.AppManager.utils.DigestUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.adapters.SelectedArrayAdapter; import io.github.muntashirakon.dialog.DialogTitleBuilder; import io.github.muntashirakon.io.IoUtils; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; import io.github.muntashirakon.util.AccessibilityUtils; import io.github.muntashirakon.view.TextInputLayoutCompat; import io.github.muntashirakon.widget.MaterialSpinner; import io.github.muntashirakon.widget.TextInputTextView; public class ChecksumsDialogFragment extends DialogFragment { public static final String TAG = ChecksumsDialogFragment.class.getSimpleName(); private static final String ARG_PATH = "path"; @NonNull public static ChecksumsDialogFragment getInstance(@NonNull Path path) { ChecksumsDialogFragment fragment = new ChecksumsDialogFragment(); Bundle args = new Bundle(); args.putParcelable(ARG_PATH, path.getUri()); fragment.setArguments(args); return fragment; } private Path mPath; private View mDialogView; private RecyclerView mRecyclerView; private MaterialSpinner mSpinner; private MaterialButton mButton; private TextInputTextView mTextView; private ChecksumsViewModel mViewModel; private ChecksumsRecyclerViewAdapter mAdapter; @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { mViewModel = new ViewModelProvider(this).get(ChecksumsViewModel.class); mPath = Paths.get(Objects.requireNonNull(BundleCompat.getParcelable(requireArguments(), ARG_PATH, Uri.class))); mAdapter = new ChecksumsRecyclerViewAdapter(); mDialogView = View.inflate(requireActivity(), R.layout.dialog_checksums, null); mRecyclerView = mDialogView.findViewById(R.id.recycler_view); mRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); mRecyclerView.setAdapter(mAdapter); mSpinner = mDialogView.findViewById(R.id.spinner); SelectedArrayAdapter algorithmsAdapter = new SelectedArrayAdapter<>(requireContext(), io.github.muntashirakon.ui.R.layout.auto_complete_dropdown_item_small, getAlgorithms()); mSpinner.setAdapter(algorithmsAdapter); mButton = mDialogView.findViewById(R.id.action); mButton.setOnClickListener(v -> { String algorithm = mSpinner.getEditText().getText().toString().trim(); mViewModel.loadChecksum(algorithm, mPath); }); mTextView = mDialogView.findViewById(R.id.text); mTextView.setVisibility(View.GONE); DialogTitleBuilder titleBuilder = new DialogTitleBuilder(requireActivity()) .setTitle(R.string.checksums) .setSubtitle(mPath.getName()) .setEndIcon(R.drawable.ic_content_paste, v -> { String data = ClipboardUtils.readHashValueFromClipboard(v.getContext()); if (data != null) { for (Map.Entry digest : mAdapter.mNameChecksumMap) { if (digest.getValue().equals(data)) { if (digest.getValue().equals(DigestUtils.MD5) || digest.getValue().equals(DigestUtils.SHA_1)) { displayLongToast(R.string.verified_using_unreliable_hash); } else { displayLongToast(R.string.verified); } return; } } displayLongToast(R.string.not_verified); } }) .setEndIconContentDescription(R.string.paste); return new MaterialAlertDialogBuilder(requireActivity()) .setCustomTitle(titleBuilder.build()) .setView(mDialogView) .setNegativeButton(R.string.close, null) .create(); } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return mDialogView; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { if (mViewModel != null) { mViewModel.getChecksumsLiveData().observe(getViewLifecycleOwner(), map -> mAdapter.setDefaultList(map)); mViewModel.getChecksumLiveData().observe(getViewLifecycleOwner(), algoHashPair -> { mTextView.setVisibility(View.VISIBLE); mTextView.setText(algoHashPair.second); TextInputLayoutCompat.fromTextInputEditText(mTextView).setHint(algoHashPair.first); AccessibilityUtils.requestAccessibilityFocus(mTextView); }); mViewModel.loadChecksums(mPath); } } private List getAlgorithms() { Provider provider = Security.getProvider(BouncyCastleProvider.PROVIDER_NAME); List algorithms = new ArrayList<>(); for (Provider.Service service : provider.getServices()) { if ("MessageDigest".equals(service.getType())) { algorithms.add(service.getAlgorithm()); } } return algorithms; } private static class ChecksumsRecyclerViewAdapter extends RecyclerView.Adapter { @NonNull private final List> mNameChecksumMap = new ArrayList<>(); public void setDefaultList(@NonNull Map map) { synchronized (mNameChecksumMap) { mNameChecksumMap.clear(); mNameChecksumMap.addAll(map.entrySet()); notifyDataSetChanged(); } } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_text_input_layout_monospace, parent, false); return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { Map.Entry entry; synchronized (mNameChecksumMap) { entry = mNameChecksumMap.get(position); } holder.input.setHint(entry.getKey()); holder.textView.setText(entry.getValue()); } @Override public int getItemCount() { synchronized (mNameChecksumMap) { return mNameChecksumMap.size(); } } static class ViewHolder extends RecyclerView.ViewHolder { TextInputLayout input; TextInputTextView textView; public ViewHolder(@NonNull View itemView) { super(itemView); input = (TextInputLayout) itemView; textView = (TextInputTextView) Objects.requireNonNull(input.getEditText()); } } } public static class ChecksumsViewModel extends AndroidViewModel { private final MutableLiveData> mChecksumsLiveData = new MutableLiveData<>(); private final MutableLiveData> mChecksumLiveData = new MutableLiveData<>(); public ChecksumsViewModel(@NonNull Application application) { super(application); } public LiveData> getChecksumsLiveData() { return mChecksumsLiveData; } public LiveData> getChecksumLiveData() { return mChecksumLiveData; } public void loadChecksum(@NonNull String algo, @NonNull Path path) { ThreadUtils.postOnBackgroundThread(() -> { try { MessageDigest digest = MessageDigest.getInstance(algo, BouncyCastleProvider.PROVIDER_NAME); try (InputStream is = path.openInputStream()) { byte[] buffer = new byte[IoUtils.DEFAULT_BUFFER_SIZE]; int length; while ((length = is.read(buffer)) != -1) { digest.update(buffer, 0, length); } } mChecksumLiveData.postValue(new Pair<>(algo, HexEncoding.encodeToString(digest.digest(), false))); } catch (Exception e) { e.printStackTrace(); mChecksumLiveData.postValue(new Pair<>(null, null)); } }); } public void loadChecksums(@NonNull Path path) { ThreadUtils.postOnBackgroundThread(() -> { try { mChecksumsLiveData.postValue(generateChecksums(path)); } catch (Exception e) { e.printStackTrace(); mChecksumsLiveData.postValue(Collections.emptyMap()); } }); } private Map generateChecksums(@NonNull Path path) throws Exception { String[] nativeAlgo = new String[]{"MD5", "SHA-1", "SHA-256", "SHA-512"}; String[] bcAlgo = new String[]{"SHA3-256", "SHA3-512"}; MessageDigest[] messageDigests = new MessageDigest[nativeAlgo.length + bcAlgo.length]; String[] algoNames = new String[]{"MD5 (unreliable)", "SHA-1 (unreliable)", "SHA-256", "SHA-512", "SHA3-256", "SHA3-512"}; for (int i = 0; i < nativeAlgo.length; ++i) { messageDigests[i] = MessageDigest.getInstance(nativeAlgo[i]); } for (int i = 0; i < bcAlgo.length; ++i) { messageDigests[nativeAlgo.length + i] = MessageDigest.getInstance(bcAlgo[i], BouncyCastleProvider.PROVIDER_NAME); } CRC32 crc32 = new CRC32(); try (InputStream is = path.openInputStream()) { byte[] buffer = new byte[IoUtils.DEFAULT_BUFFER_SIZE]; int length; while ((length = is.read(buffer)) != -1) { for (MessageDigest messageDigest : messageDigests) { messageDigest.update(buffer, 0, length); } crc32.update(buffer, 0, length); } } Map checksums = new LinkedHashMap<>(messageDigests.length + 1); checksums.put("CRC32 (insecure)", HexEncoding.encodeToString(DigestUtils.longToBytes(crc32.getValue()), false)); for (int i = 0; i < messageDigests.length; ++i) { checksums.put(algoNames[i], HexEncoding.encodeToString(messageDigests[i].digest(), false)); } return checksums; } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/fm/dialogs/FilePropertiesDialogFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.fm.dialogs; import android.app.Application; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.system.ErrnoException; import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.text.format.Formatter; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import androidx.core.os.BundleCompat; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModelProvider; import com.google.android.material.button.MaterialButton; import com.google.android.material.checkbox.MaterialCheckBox; import com.google.android.material.textfield.TextInputLayout; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.concurrent.Future; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.fm.FmItem; import io.github.muntashirakon.AppManager.fm.FmUtils; import io.github.muntashirakon.AppManager.fm.icons.FmIconFetcher; import io.github.muntashirakon.AppManager.self.imagecache.ImageLoader; import io.github.muntashirakon.AppManager.settings.Ops; import io.github.muntashirakon.AppManager.users.Groups; import io.github.muntashirakon.AppManager.users.Owners; import io.github.muntashirakon.AppManager.utils.DateUtils; import io.github.muntashirakon.AppManager.utils.PackageUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.dialog.CapsuleBottomSheetDialogFragment; import io.github.muntashirakon.dialog.SearchableSingleChoiceDialogBuilder; import io.github.muntashirakon.dialog.TextInputDialogBuilder; import io.github.muntashirakon.io.ExtendedFile; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.PathContentInfo; import io.github.muntashirakon.io.Paths; import io.github.muntashirakon.io.UidGidPair; import io.github.muntashirakon.util.LocalizedString; import io.github.muntashirakon.view.TextInputLayoutCompat; import io.github.muntashirakon.widget.TextInputTextView; public class FilePropertiesDialogFragment extends CapsuleBottomSheetDialogFragment { public static final String TAG = FilePropertiesDialogFragment.class.getSimpleName(); private static final String ARG_PATH = "path"; @NonNull public static FilePropertiesDialogFragment getInstance(@NonNull Uri uri) { FilePropertiesDialogFragment fragment = new FilePropertiesDialogFragment(); Bundle args = new Bundle(); args.putParcelable(ARG_PATH, uri); fragment.setArguments(args); return fragment; } private ImageView mIconView; private ImageView mSymlinkIconView; private TextView mNameView; private TextView mSummaryView; private TextView mChecksumsView; private MaterialButton mMoreButton; private TextInputTextView mPathView; private TextInputTextView mTypeView; private TextInputTextView mTargetPathView; private TextInputLayout mTargetPathLayout; private TextInputTextView mOpenWithView; private TextInputLayout mOpenWithLayout; private TextInputTextView mSizeView; private TextInputTextView mDateCreatedView; private TextInputTextView mDateModifiedView; private TextInputLayout mDateModifiedLayout; private TextInputTextView mDateAccessedView; private TextInputLayout mDateAccessedLayout; private TextInputTextView mMoreInfoView; private TextInputTextView mModeView; private TextInputLayout mModeLayout; private TextInputTextView mOwnerView; private TextInputLayout mOwnerLayout; private TextInputTextView mGroupView; private TextInputLayout mGroupLayout; private TextInputTextView mSelinuxContextView; private TextInputLayout mSelinuxContextLayout; private FilePropertiesViewModel mViewModel; @Nullable private FileProperties mFileProperties; @NonNull @Override public View initRootView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.dialog_file_properties, container, false); } @Override public void onBodyInitialized(@NonNull View bodyView, @Nullable Bundle savedInstanceState) { Path path = Paths.get(Objects.requireNonNull(BundleCompat.getParcelable(requireArguments(), ARG_PATH, Uri.class))); mViewModel = new ViewModelProvider(this).get(FilePropertiesViewModel.class); mIconView = bodyView.findViewById(android.R.id.icon); mSymlinkIconView = bodyView.findViewById(R.id.symbolic_link_icon); mNameView = bodyView.findViewById(R.id.name); mSummaryView = bodyView.findViewById(R.id.summary); mChecksumsView = bodyView.findViewById(R.id.checksums); mChecksumsView.setOnClickListener(v -> { ChecksumsDialogFragment fragment = ChecksumsDialogFragment.getInstance(path); fragment.show(getChildFragmentManager(), ChecksumsDialogFragment.TAG); }); mMoreButton = bodyView.findViewById(R.id.more); mMoreButton.setVisibility(View.GONE); mPathView = bodyView.findViewById(R.id.path); mTypeView = bodyView.findViewById(R.id.type); mTargetPathView = bodyView.findViewById(R.id.target_file); mTargetPathLayout = TextInputLayoutCompat.fromTextInputEditText(mTargetPathView); mOpenWithView = bodyView.findViewById(R.id.open_with); mOpenWithLayout = TextInputLayoutCompat.fromTextInputEditText(mOpenWithView); TextInputLayoutCompat.fixEndIcon(mOpenWithLayout); // TODO: 16/11/22 Handle open with mOpenWithLayout.setVisibility(View.GONE); mSizeView = bodyView.findViewById(R.id.size); mDateCreatedView = bodyView.findViewById(R.id.date_created); mDateModifiedView = bodyView.findViewById(R.id.date_modified); mDateModifiedLayout = TextInputLayoutCompat.fromTextInputEditText(mDateModifiedView); TextInputLayoutCompat.fixEndIcon(mDateModifiedLayout); mDateModifiedLayout.setEndIconOnClickListener(v -> { if (mFileProperties != null) { mViewModel.setModificationTime(mFileProperties, System.currentTimeMillis()); } }); mDateAccessedView = bodyView.findViewById(R.id.date_accessed); mDateAccessedLayout = TextInputLayoutCompat.fromTextInputEditText(mDateAccessedView); mDateAccessedLayout.setEndIconOnClickListener(v -> { if (mFileProperties != null) { mViewModel.setLastAccessTime(mFileProperties, System.currentTimeMillis()); } }); TextInputLayoutCompat.fixEndIcon(mDateAccessedLayout); mMoreInfoView = bodyView.findViewById(R.id.more_info); TextInputLayoutCompat.fromTextInputEditText(mMoreInfoView).setVisibility(View.GONE); mModeView = bodyView.findViewById(R.id.file_mode); mModeLayout = TextInputLayoutCompat.fromTextInputEditText(mModeView); TextInputLayoutCompat.fixEndIcon(mModeLayout); mModeLayout.setEndIconOnClickListener(v -> { if (mFileProperties != null) { ChangeFileModeDialogFragment dialog = ChangeFileModeDialogFragment.getInstance(mFileProperties.mode, mFileProperties.isDirectory, (mode, recursive) -> mViewModel.setMode(mFileProperties, mode, recursive)); dialog.show(getChildFragmentManager(), ChangeFileModeDialogFragment.TAG); } }); mOwnerView = bodyView.findViewById(R.id.owner_id); mOwnerLayout = TextInputLayoutCompat.fromTextInputEditText(mOwnerView); TextInputLayoutCompat.fixEndIcon(mOwnerLayout); mOwnerLayout.setEndIconOnClickListener(v -> { if (mFileProperties != null) { mViewModel.fetchOwnerList(); } }); mGroupView = bodyView.findViewById(R.id.group_id); mGroupLayout = TextInputLayoutCompat.fromTextInputEditText(mGroupView); TextInputLayoutCompat.fixEndIcon(mGroupLayout); mGroupLayout.setEndIconOnClickListener(v -> { if (mFileProperties != null) { mViewModel.fetchGroupList(); } }); mSelinuxContextView = bodyView.findViewById(R.id.selinux_context); mSelinuxContextLayout = TextInputLayoutCompat.fromTextInputEditText(mSelinuxContextView); TextInputLayoutCompat.fixEndIcon(mSelinuxContextLayout); mSelinuxContextLayout.setEndIconOnClickListener(v -> displaySeContextUpdater()); // Live data mViewModel.getFilePropertiesLiveData().observe(getViewLifecycleOwner(), this::updateProperties); mViewModel.getFmItemLiveData().observe(getViewLifecycleOwner(), fmItem -> { ImageLoader.getInstance().displayImage(fmItem.getTag(), mIconView, new FmIconFetcher(fmItem)); PathContentInfo contentInfo = fmItem.getContentInfo(); if (contentInfo != null) { String name = contentInfo.getName(); String mime = contentInfo.getMimeType(); String message = contentInfo.getMessage(); if (mime != null) { mTypeView.setText(String.format(Locale.ROOT, "%s (%s)", name, mime)); } else { mTypeView.setText(name); } if (message != null) { TextInputLayoutCompat.fromTextInputEditText(mMoreInfoView).setVisibility(View.VISIBLE); mMoreInfoView.setText(message); } } }); mViewModel.getOwnerListLiveData().observe(getViewLifecycleOwner(), this::displayUidUpdater); mViewModel.getGroupListLiveData().observe(getViewLifecycleOwner(), this::displayGidUpdater); mViewModel.getOwnerLiveData().observe(getViewLifecycleOwner(), ownerName -> { assert mFileProperties != null; assert mFileProperties.uidGidPair != null; mOwnerView.setText(String.format(Locale.ROOT, "%s (%d)", ownerName, mFileProperties.uidGidPair.uid)); }); mViewModel.getGroupLiveData().observe(getViewLifecycleOwner(), groupName -> { assert mFileProperties != null; assert mFileProperties.uidGidPair != null; mGroupView.setText(String.format(Locale.ROOT, "%s (%d)", groupName, mFileProperties.uidGidPair.gid)); }); // Load live data mViewModel.loadFileProperties(path); mViewModel.loadFmItem(path); } private void updateProperties(@NonNull FileProperties fileProperties) { boolean noInit = mFileProperties == null; if (noInit && fileProperties.isDirectory) { mChecksumsView.setVisibility(View.GONE); } boolean uidGidChanged = noInit || mFileProperties.uidGidPair != fileProperties.uidGidPair; if (noInit || mFileProperties.isDirectory != fileProperties.isDirectory) { if (fileProperties.isDirectory) { mIconView.setImageResource(R.drawable.ic_folder); } } if (noInit || mFileProperties.isSymlink != fileProperties.isSymlink) { mSymlinkIconView.setVisibility(fileProperties.isSymlink ? View.VISIBLE : View.GONE); } if (noInit || !Objects.equals(mFileProperties.name, fileProperties.name)) { mNameView.setText(fileProperties.name); } if (noInit || !Objects.equals(mFileProperties.readablePath, fileProperties.readablePath)) { mPathView.setText(fileProperties.readablePath); } if (noInit || !Objects.equals(mFileProperties.targetPath, fileProperties.targetPath)) { if (fileProperties.targetPath != null) { mTargetPathView.setText(fileProperties.targetPath); } else { mTargetPathLayout.setVisibility(View.GONE); } } if (noInit || mFileProperties.size != fileProperties.size) { if (fileProperties.size != -1) { mSizeView.setText(String.format(Locale.getDefault(), "%s (%,d bytes)", Formatter.formatShortFileSize(requireContext(), fileProperties.size), fileProperties.size)); } } if (noInit || mFileProperties.size != fileProperties.size || mFileProperties.lastModified != fileProperties.lastModified || mFileProperties.fileCount != fileProperties.fileCount || mFileProperties.folderCount != fileProperties.folderCount) { updateSummary(fileProperties); } if (noInit || mFileProperties.lastModified != fileProperties.lastModified) { mDateModifiedView.setText(DateUtils.formatDateTime(requireContext(), fileProperties.lastModified)); } if (noInit || mFileProperties.creationTime != fileProperties.creationTime) { mDateCreatedView.setText(fileProperties.creationTime > 0 ? DateUtils.formatDateTime(requireContext(), fileProperties.creationTime) : "--"); } if (noInit || mFileProperties.lastAccess != fileProperties.lastAccess) { mDateAccessedView.setText(fileProperties.lastAccess > 0 ? DateUtils.formatDateTime(requireContext(), fileProperties.lastAccess) : "--"); } if (noInit || mFileProperties.canWrite != fileProperties.canWrite) { boolean isPhysicalWritable = fileProperties.canWrite && fileProperties.isPhysicalFs; mDateModifiedLayout.setEndIconVisible(isPhysicalWritable); mDateAccessedLayout.setEndIconVisible(isPhysicalWritable); mOwnerLayout.setEndIconVisible(isPhysicalWritable); mGroupLayout.setEndIconVisible(isPhysicalWritable); mModeLayout.setEndIconVisible(isPhysicalWritable); mSelinuxContextLayout.setEndIconVisible(Ops.isWorkingUidRoot() && isPhysicalWritable); } if (noInit || mFileProperties.mode != fileProperties.mode) { mModeView.setText(fileProperties.mode != 0 ? FmUtils.getFormattedMode(fileProperties.mode) : "--"); } if (uidGidChanged) { if (fileProperties.uidGidPair == null) { mOwnerView.setText("--"); mGroupView.setText("--"); } } if (noInit || !Objects.equals(mFileProperties.context, fileProperties.context)) { mSelinuxContextView.setText(fileProperties.context != null ? fileProperties.context : "--"); } mFileProperties = fileProperties; // Load others if (fileProperties.size == -1) { mViewModel.loadFileSize(fileProperties); } if (fileProperties.uidGidPair != null && uidGidChanged) { mViewModel.loadOwnerInfo(fileProperties.uidGidPair.uid); mViewModel.loadGroupInfo(fileProperties.uidGidPair.gid); } } private void updateSummary(@NonNull FileProperties fileProperties) { StringBuilder summary = new StringBuilder(); // 1. Date modified summary.append(DateUtils.formatDateTime(requireContext(), fileProperties.lastModified)); // 2. Size if (fileProperties.size > 0) { summary.append(" • ").append(Formatter.formatShortFileSize(requireContext(), fileProperties.size)); } // 3. Folders and files if (fileProperties.folderCount > 0 && fileProperties.fileCount > 0) { summary.append(" • ") .append(getResources().getQuantityString(R.plurals.folder_count, fileProperties.folderCount, fileProperties.folderCount)) .append(", ") .append(getResources().getQuantityString(R.plurals.file_count, fileProperties.fileCount, fileProperties.fileCount)); } else if (fileProperties.folderCount > 0) { summary.append(" • ") .append(getResources().getQuantityString(R.plurals.folder_count, fileProperties.folderCount, fileProperties.folderCount)); } else if (fileProperties.fileCount > 0) { summary.append(" • ") .append(getResources().getQuantityString(R.plurals.file_count, fileProperties.fileCount, fileProperties.fileCount)); } mSummaryView.setText(summary); } private void displayUidUpdater(@NonNull List owners) { assert mFileProperties != null; List uidNames = new ArrayList<>(owners.size()); for (AndroidId androidId : owners) { uidNames.add(androidId.toLocalizedString(requireContext())); } AndroidId selectedUid; if (mFileProperties.uidGidPair != null) { selectedUid = new AndroidId(); selectedUid.id = mFileProperties.uidGidPair.uid; } else selectedUid = null; View view; MaterialCheckBox checkBox; if (mFileProperties.isDirectory) { view = View.inflate(requireContext(), R.layout.item_checkbox, null); checkBox = view.findViewById(R.id.checkbox); checkBox.setText(R.string.apply_recursively); } else { view = null; checkBox = null; } new SearchableSingleChoiceDialogBuilder<>(requireContext(), owners, uidNames) .setSelection(selectedUid) .setTitle(R.string.change_owner_uid) .setView(view) .setPositiveButton(R.string.ok, (dialog, which, uid) -> { if (uid != null) { mViewModel.setUid(mFileProperties, uid.id, checkBox != null && checkBox.isChecked()); } }) .setNegativeButton(R.string.cancel, null) .show(); } private void displayGidUpdater(@NonNull List groups) { assert mFileProperties != null; List gidNames = new ArrayList<>(groups.size()); for (AndroidId androidId : groups) { gidNames.add(androidId.toLocalizedString(requireContext())); } AndroidId selectedGid; if (mFileProperties.uidGidPair != null) { selectedGid = new AndroidId(); selectedGid.id = mFileProperties.uidGidPair.gid; } else selectedGid = null; View view; MaterialCheckBox checkBox; if (mFileProperties.isDirectory) { view = View.inflate(requireContext(), R.layout.item_checkbox, null); checkBox = view.findViewById(R.id.checkbox); checkBox.setText(R.string.apply_recursively); } else { view = null; checkBox = null; } new SearchableSingleChoiceDialogBuilder<>(requireContext(), groups, gidNames) .setSelection(selectedGid) .setTitle(R.string.change_group_gid) .setView(view) .setPositiveButton(R.string.ok, (dialog, which, gid) -> { if (gid != null) { mViewModel.setGid(mFileProperties, gid.id, checkBox != null && checkBox.isChecked()); } }) .setNegativeButton(R.string.cancel, null) .show(); } private void displaySeContextUpdater() { assert mFileProperties != null; new TextInputDialogBuilder(requireContext(), null) .setTitle(R.string.title_change_selinux_context) .setInputText(mFileProperties.context) .setCheckboxLabel(mFileProperties.isDirectory ? R.string.apply_recursively : 0) .setPositiveButton(R.string.ok, (dialog, which, context, recursive) -> { if (!TextUtils.isEmpty(context)) { mViewModel.setSeContext(mFileProperties, context.toString().trim(), recursive); } }) .setNegativeButton(R.string.cancel, null) .setNeutralButton(R.string.restore, (dialog, which, context, recursive) -> mViewModel.restorecon(mFileProperties, recursive)) .show(); } public static class FilePropertiesViewModel extends AndroidViewModel { private final MutableLiveData mFilePropertiesLiveData = new MutableLiveData<>(); private final MutableLiveData mFmItemLiveData = new MutableLiveData<>(); private final MutableLiveData> mOwnerListLiveData = new MutableLiveData<>(); private final MutableLiveData> mGroupListLiveData = new MutableLiveData<>(); private final MutableLiveData mOwnerLiveData = new MutableLiveData<>(); private final MutableLiveData mGroupLiveData = new MutableLiveData<>(); private final List mOwnerList = new ArrayList<>(); private final List mGroupList = new ArrayList<>(); @Nullable private Future sizeResult; public FilePropertiesViewModel(@NonNull Application application) { super(application); } @Override protected void onCleared() { // Size checks can take forever, so it's a good idea to terminate the process when the dialog is exited if (sizeResult != null) { sizeResult.cancel(true); } super.onCleared(); } public void setMode(@NonNull FileProperties properties, int mode, boolean recursive) { ThreadUtils.postOnBackgroundThread(() -> { ExtendedFile file = properties.path.getFile(); if (file == null) { return; } if (recursive) { setModeRecursive(file, mode); } try { file.setMode(mode); FileProperties newProperties = new FileProperties(properties); newProperties.mode = newProperties.path.getMode(); mFilePropertiesLiveData.postValue(newProperties); } catch (ErrnoException e) { e.printStackTrace(); } }); } public void fetchOwnerList() { ThreadUtils.postOnBackgroundThread(() -> { if (mOwnerList.isEmpty()) { getOwnersAndGroupsInternal(); } mOwnerListLiveData.postValue(new ArrayList<>(mOwnerList)); }); } public void fetchGroupList() { ThreadUtils.postOnBackgroundThread(() -> { if (mGroupList.isEmpty()) { getOwnersAndGroupsInternal(); } mGroupListLiveData.postValue(new ArrayList<>(mGroupList)); }); } @WorkerThread private void getOwnersAndGroupsInternal() { mOwnerList.clear(); mGroupList.clear(); // Add owners Map uidOwnerMap = Owners.getUidOwnerMap(false); for (int uid : uidOwnerMap.keySet()) { AndroidId id = new AndroidId(); id.id = uid; id.name = Objects.requireNonNull(uidOwnerMap.get(uid)); // TODO: 30/6/23 Add a more readable description from android_filesystem_config.h id.description = "System"; mOwnerList.add(id); } // Add groups Map gidGroupMap = Groups.getGidGroupMap(false); for (int gid : gidGroupMap.keySet()) { AndroidId id = new AndroidId(); id.id = gid; id.name = Objects.requireNonNull(gidGroupMap.get(gid)); id.description = "System"; mGroupList.add(id); } List applicationInfoList = PackageUtils.getAllApplications(0); Map uidList = new HashMap<>(applicationInfoList.size()); PackageManager pm = getApplication().getPackageManager(); for (ApplicationInfo info : applicationInfoList) { if (uidOwnerMap.containsKey(info.uid)) { // Omit system UID/GIDs continue; } if (!uidList.containsKey(info.uid)) { // Include only the first app label // TODO: 30/6/23 Include all app names? uidList.put(info.uid, info.loadLabel(pm)); } } for (int uid : uidList.keySet()) { AndroidId id = new AndroidId(); id.id = uid; id.name = Owners.formatUid(uid); id.description = Objects.requireNonNull(uidList.get(uid)); mOwnerList.add(id); mGroupList.add(id); // Add cached uid if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { int gid = Groups.getCacheAppGid(uid); if (gid == -1 || gid == uid) { continue; } AndroidId cachedGid = new AndroidId(); cachedGid.id = gid; cachedGid.name = Groups.formatGid(gid); cachedGid.description = id.description; mGroupList.add(cachedGid); } } Collections.sort(mOwnerList, (o1, o2) -> Integer.compare(o1.id, o2.id)); Collections.sort(mGroupList, (o1, o2) -> Integer.compare(o1.id, o2.id)); } public void setUid(@NonNull FileProperties properties, int uid, boolean recursive) { ThreadUtils.postOnBackgroundThread(() -> { ExtendedFile file = properties.path.getFile(); if (file == null) { return; } if (recursive) { setUidRecursive(file, uid); } try { UidGidPair pair = file.getUidGid(); file.setUidGid(uid, pair.gid); FileProperties newProperties = new FileProperties(properties); newProperties.uidGidPair = newProperties.path.getUidGid(); mFilePropertiesLiveData.postValue(newProperties); } catch (ErrnoException e) { e.printStackTrace(); } }); } public void setGid(@NonNull FileProperties properties, int gid, boolean recursive) { ThreadUtils.postOnBackgroundThread(() -> { ExtendedFile file = properties.path.getFile(); if (file == null) { return; } if (recursive) { setGidRecursive(file, gid); } try { UidGidPair pair = file.getUidGid(); file.setUidGid(pair.uid, gid); FileProperties newProperties = new FileProperties(properties); newProperties.uidGidPair = newProperties.path.getUidGid(); mFilePropertiesLiveData.postValue(newProperties); } catch (ErrnoException e) { e.printStackTrace(); } }); } public void restorecon(@NonNull FileProperties properties, boolean recursive) { ThreadUtils.postOnBackgroundThread(() -> { ExtendedFile file = properties.path.getFile(); if (file == null) { return; } if (recursive) { restoreconRecursive(file); } if (file.restoreSelinuxContext()) { FileProperties newProperties = new FileProperties(properties); newProperties.context = newProperties.path.getSelinuxContext(); mFilePropertiesLiveData.postValue(newProperties); } }); } public void setSeContext(@NonNull FileProperties properties, @NonNull String newContext, boolean recursive) { ThreadUtils.postOnBackgroundThread(() -> { ExtendedFile file = properties.path.getFile(); if (file == null) { return; } if (recursive) { setSeContextRecursive(file, newContext); } if (file.setSelinuxContext(newContext)) { FileProperties newProperties = new FileProperties(properties); newProperties.context = newProperties.path.getSelinuxContext(); mFilePropertiesLiveData.postValue(newProperties); } }); } public void setModificationTime(@NonNull FileProperties properties, long time) { ThreadUtils.postOnBackgroundThread(() -> { if (properties.path.setLastModified(time)) { FileProperties newProperties = new FileProperties(properties); newProperties.lastModified = newProperties.path.lastModified(); mFilePropertiesLiveData.postValue(newProperties); } }); } public void setLastAccessTime(@NonNull FileProperties properties, long time) { ThreadUtils.postOnBackgroundThread(() -> { if (properties.path.setLastAccess(time)) { FileProperties newProperties = new FileProperties(properties); newProperties.lastAccess = newProperties.path.lastAccess(); mFilePropertiesLiveData.postValue(newProperties); } }); } public void loadFileProperties(@NonNull Path path) { ThreadUtils.postOnBackgroundThread(() -> { FileProperties properties = new FileProperties(); Path[] children = path.listFiles(); int count = children.length; int folderCount = 0; for (Path child : children) { if (child.isDirectory()) { ++folderCount; } } properties.path = path; properties.isPhysicalFs = path.getFile() != null; properties.name = path.getName(); properties.readablePath = FmUtils.getDisplayablePath(path); properties.folderCount = folderCount; properties.fileCount = count - folderCount; properties.isDirectory = path.isDirectory(); properties.isSymlink = path.isSymbolicLink(); properties.canRead = path.canRead(); properties.canWrite = path.canWrite(); properties.lastAccess = path.lastAccess(); properties.lastModified = path.lastModified(); properties.creationTime = path.creationTime(); properties.mode = path.getMode(); properties.uidGidPair = path.getUidGid(); properties.context = path.getSelinuxContext(); if (properties.isSymlink) { try { properties.targetPath = path.getRealFilePath(); } catch (IOException ignore) { } } mFilePropertiesLiveData.postValue(properties); }); } public void loadFileSize(@NonNull FileProperties properties) { sizeResult = ThreadUtils.postOnBackgroundThread(() -> { FileProperties newProperties = new FileProperties(properties); newProperties.size = Paths.size(newProperties.path); mFilePropertiesLiveData.postValue(newProperties); }); } public void loadFmItem(@NonNull Path path) { ThreadUtils.postOnBackgroundThread(() -> { FmItem fmItem = new FmItem(path); fmItem.setContentInfo(path.getPathContentInfo()); mFmItemLiveData.postValue(fmItem); }); } public void loadOwnerInfo(int uid) { ThreadUtils.postOnBackgroundThread(() -> { String ownerName = Owners.getOwnerName(uid); mOwnerLiveData.postValue(ownerName); }); } public void loadGroupInfo(int gid) { ThreadUtils.postOnBackgroundThread(() -> { String groupName = Groups.getGroupName(gid); mGroupLiveData.postValue(groupName); }); } public LiveData getFilePropertiesLiveData() { return mFilePropertiesLiveData; } public LiveData getFmItemLiveData() { return mFmItemLiveData; } public LiveData> getOwnerListLiveData() { return mOwnerListLiveData; } public LiveData> getGroupListLiveData() { return mGroupListLiveData; } public LiveData getOwnerLiveData() { return mOwnerLiveData; } public LiveData getGroupLiveData() { return mGroupLiveData; } private boolean setModeRecursive(@NonNull ExtendedFile dir, int mode) { if (dir.isSymlink()) { // Avoid following symbolic links return true; } ExtendedFile[] files = dir.listFiles(); boolean success = true; if (files != null) { for (ExtendedFile file : files) { if (file.isDirectory()) { success &= setModeRecursive(file, mode); } try { file.setMode(mode); } catch (ErrnoException e) { Log.w(TAG, "Failed to set mode " + mode + " on " + file); success = false; } } } return success; } private boolean setUidRecursive(@NonNull ExtendedFile dir, int uid) { if (dir.isSymlink()) { // Avoid following symbolic links return true; } ExtendedFile[] files = dir.listFiles(); boolean success = true; if (files != null) { for (ExtendedFile file : files) { if (file.isDirectory()) { success &= setUidRecursive(file, uid); } try { UidGidPair pair = file.getUidGid(); file.setUidGid(uid, pair.gid); } catch (ErrnoException e) { Log.w(TAG, "Failed to set UID " + uid + " on " + file); success = false; } } } return success; } private boolean setGidRecursive(@NonNull ExtendedFile dir, int gid) { if (dir.isSymlink()) { // Avoid following symbolic links return true; } ExtendedFile[] files = dir.listFiles(); boolean success = true; if (files != null) { for (ExtendedFile file : files) { if (file.isDirectory()) { success &= setGidRecursive(file, gid); } try { UidGidPair pair = file.getUidGid(); file.setUidGid(pair.uid, gid); } catch (ErrnoException e) { Log.w(TAG, "Failed to set GID " + gid + " on " + file); success = false; } } } return success; } private boolean restoreconRecursive(@NonNull ExtendedFile dir) { if (dir.isSymlink()) { // Avoid following symbolic links return true; } ExtendedFile[] files = dir.listFiles(); boolean success = true; if (files != null) { for (ExtendedFile file : files) { if (file.isDirectory()) { success &= restoreconRecursive(file); } if (!file.restoreSelinuxContext()) { Log.w(TAG, "Failed to restorecon on " + file); success = false; } } } return success; } private boolean setSeContextRecursive(@NonNull ExtendedFile dir, @NonNull String newContext) { if (dir.isSymlink()) { // Avoid following symbolic links return true; } ExtendedFile[] files = dir.listFiles(); boolean success = true; if (files != null) { for (ExtendedFile file : files) { if (file.isDirectory()) { success &= setSeContextRecursive(file, newContext); } if (!file.setSelinuxContext(newContext)) { Log.w(TAG, "Failed to set SELinux context on " + file); success = false; } } } return success; } } public static class AndroidId implements LocalizedString { public int id; public String name; public CharSequence description; @NonNull @Override public CharSequence toLocalizedString(@NonNull Context context) { return new SpannableStringBuilder(name).append(" (").append(String.valueOf(id)).append(")\n") .append(UIUtils.getSmallerText(UIUtils.getSecondaryText(context, description))); } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof AndroidId)) return false; AndroidId androidId = (AndroidId) o; return id == androidId.id; } @Override public int hashCode() { return Objects.hash(id); } } public static class FileProperties { public Path path; public boolean isPhysicalFs; public String name; public String readablePath; public int folderCount; public int fileCount; public boolean isDirectory; public boolean isSymlink; public boolean canRead; public boolean canWrite; public long size = -1; public long lastAccess; public long lastModified; public long creationTime; public int mode; @Nullable public UidGidPair uidGidPair; @Nullable public String context; @Nullable public String targetPath; public FileProperties() { } public FileProperties(@NonNull FileProperties fileProperties) { path = fileProperties.path; isPhysicalFs = fileProperties.isPhysicalFs; name = fileProperties.name; readablePath = fileProperties.readablePath; folderCount = fileProperties.folderCount; fileCount = fileProperties.fileCount; isDirectory = fileProperties.isDirectory; isSymlink = fileProperties.isSymlink; canRead = fileProperties.canRead; canWrite = fileProperties.canWrite; size = fileProperties.size; lastAccess = fileProperties.lastAccess; lastModified = fileProperties.lastModified; creationTime = fileProperties.creationTime; mode = fileProperties.mode; uidGidPair = fileProperties.uidGidPair; context = fileProperties.context; targetPath = fileProperties.targetPath; } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/fm/dialogs/NewFileDialogFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.fm.dialogs; import android.app.Dialog; import android.os.Bundle; import android.text.Editable; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.DialogFragment; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.textfield.TextInputEditText; import java.lang.ref.WeakReference; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.adapters.SelectedArrayAdapter; import io.github.muntashirakon.io.Paths; import io.github.muntashirakon.lifecycle.SoftInputLifeCycleObserver; import io.github.muntashirakon.widget.MaterialSpinner; public class NewFileDialogFragment extends DialogFragment { public static final String TAG = NewFileDialogFragment.class.getSimpleName(); public interface OnCreateNewFileInterface { void onCreate(@NonNull String prefix, @Nullable String extension, @NonNull String template); } @NonNull public static NewFileDialogFragment getInstance(@Nullable OnCreateNewFileInterface createNewFileInterface) { NewFileDialogFragment fragment = new NewFileDialogFragment(); fragment.setOnCreateNewFileInterface(createNewFileInterface); return fragment; } private static final int TYPE_TEXT = 0; private static final int TYPE_PDF = 1; private static final int TYPE_DOCS = 2; private static final int TYPE_SHEET = 3; private static final int TYPE_PRESENTATION = 4; private static final int TYPE_DB = 5; private static final String[] TYPE_LABELS = new String[]{ "Text", // TYPE_TEXT "PDF", // TYPE_PDF "Document (.docx, .odt)", // TYPE_DOCS "Sheet (.xlsx, .ods)", // TYPE_SHEET "Presentation (.ppt, .odp)", // TYPE_PRESENTATION "Database", // TYPE_DB }; private static final String[] TYPE_DEFAULT_EXTENSIONS = new String[]{ "txt", // TYPE_TEXT "pdf", // TYPE_PDF "docx", // TYPE_DOCS "xlsx", // TYPE_SHEET "pptx", // TYPE_PRESENTATION "db", // TYPE_DB }; @Nullable private OnCreateNewFileInterface mOnCreateNewFileInterface; private View mDialogView; private TextInputEditText mEditText; private int mType; @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { mDialogView = View.inflate(requireActivity(), R.layout.dialog_new_file, null); mEditText = mDialogView.findViewById(R.id.name); String name = "New file.txt"; mEditText.setText(name); handleFilename(name, null); MaterialSpinner spinner = mDialogView.findViewById(R.id.type_selector_spinner); ArrayAdapter spinnerAdapter = new SelectedArrayAdapter<>(requireContext(), io.github.muntashirakon.ui.R.layout.auto_complete_dropdown_item, TYPE_LABELS); spinner.setAdapter(spinnerAdapter); spinner.setSelection(TYPE_TEXT); spinner.setOnItemClickListener((parent, view, position, id) -> { if (mType != position) { mType = position; handleFilename(mEditText.getText(), TYPE_DEFAULT_EXTENSIONS[mType]); } }); return new MaterialAlertDialogBuilder(requireActivity()) .setTitle(R.string.create_new_file) .setView(mDialogView) .setPositiveButton(R.string.ok, (dialog, which) -> { Editable editable = mEditText.getText(); if (!TextUtils.isEmpty(editable) && mOnCreateNewFileInterface != null) { String newName = editable.toString(); String prefix = Paths.trimPathExtension(newName); String extension = Paths.getPathExtension(newName, false); String template = getFileTemplateFromTypeAndExtension(mType, extension); mOnCreateNewFileInterface.onCreate(prefix, extension, template); } }) .setNegativeButton(R.string.cancel, null) .create(); } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return mDialogView; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { getLifecycle().addObserver(new SoftInputLifeCycleObserver(new WeakReference<>(mEditText))); } public void setOnCreateNewFileInterface(@Nullable OnCreateNewFileInterface createNewFileInterface) { mOnCreateNewFileInterface = createNewFileInterface; } private void handleFilename(@Nullable CharSequence charSequence, @Nullable String newExtension) { if (charSequence == null) { return; } String name = charSequence.toString(); int lastIndex = name.lastIndexOf('.'); if (newExtension != null && lastIndex != -1) { // Change extension before setting selection name = name.substring(0, lastIndex) + "." + newExtension; mEditText.setText(name); lastIndex = name.lastIndexOf('.'); } if (lastIndex != -1 || lastIndex == name.length() - 1) { mEditText.setSelection(0, lastIndex); } else { mEditText.selectAll(); } } @NonNull private static String getFileTemplateFromTypeAndExtension(int type, @Nullable String extension) { String prefix = "blank"; switch (type) { default: case TYPE_TEXT: return prefix + ".txt"; case TYPE_PDF: return prefix + ".pdf"; case TYPE_DOCS: return prefix + ("odt".equals(extension) ? ".odt" : ".docx"); case TYPE_SHEET: return prefix + ("ods".equals(extension) ? ".ods" : ".xlsx"); case TYPE_PRESENTATION: return prefix + ("odp".equals(extension) ? ".odp" : ".pptx"); case TYPE_DB: return prefix + ".db"; } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/fm/dialogs/NewFolderDialogFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.fm.dialogs; import android.annotation.SuppressLint; import android.app.Dialog; import android.os.Bundle; import android.text.Editable; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.DialogFragment; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.textfield.TextInputEditText; import java.lang.ref.WeakReference; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.lifecycle.SoftInputLifeCycleObserver; public class NewFolderDialogFragment extends DialogFragment { public static final String TAG = NewFolderDialogFragment.class.getSimpleName(); public interface OnCreateNewFolderInterface { void onCreate(@NonNull String name); } @NonNull public static NewFolderDialogFragment getInstance(@Nullable OnCreateNewFolderInterface createNewFolderInterface) { NewFolderDialogFragment fragment = new NewFolderDialogFragment(); fragment.setOnCreateNewFolderInterface(createNewFolderInterface); return fragment; } @Nullable private OnCreateNewFolderInterface mOnCreateNewFolderInterface; private View mDialogView; private TextInputEditText mEditText; @SuppressLint("SetTextI18n") @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { mDialogView = View.inflate(requireActivity(), R.layout.dialog_rename, null); mEditText = mDialogView.findViewById(R.id.rename); mEditText.setText("New folder"); mEditText.selectAll(); return new MaterialAlertDialogBuilder(requireActivity()) .setTitle(R.string.create_new_folder) .setView(mDialogView) .setPositiveButton(R.string.ok, (dialog, which) -> { Editable editable = mEditText.getText(); if (!TextUtils.isEmpty(editable) && mOnCreateNewFolderInterface != null) { mOnCreateNewFolderInterface.onCreate(editable.toString()); } }) .setNegativeButton(R.string.cancel, null) .create(); } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return mDialogView; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { getLifecycle().addObserver(new SoftInputLifeCycleObserver(new WeakReference<>(mEditText))); } public void setOnCreateNewFolderInterface(@Nullable OnCreateNewFolderInterface createNewFolderInterface) { mOnCreateNewFolderInterface = createNewFolderInterface; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/fm/dialogs/NewSymbolicLinkDialogFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.fm.dialogs; import android.annotation.SuppressLint; import android.app.Dialog; import android.os.Bundle; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.DialogFragment; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.textfield.TextInputEditText; import com.google.android.material.textfield.TextInputLayout; import java.lang.ref.WeakReference; import java.util.Objects; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; import io.github.muntashirakon.lifecycle.SoftInputLifeCycleObserver; public class NewSymbolicLinkDialogFragment extends DialogFragment { public static final String TAG = NewSymbolicLinkDialogFragment.class.getSimpleName(); public interface OnCreateNewLinkInterface { void onCreate(@NonNull String prefix, @Nullable String extension, @NonNull String targetPath); } @NonNull public static NewSymbolicLinkDialogFragment getInstance(@Nullable OnCreateNewLinkInterface createNewLinkInterface) { NewSymbolicLinkDialogFragment fragment = new NewSymbolicLinkDialogFragment(); fragment.setOnCreateNewLinkInterface(createNewLinkInterface); return fragment; } @Nullable private OnCreateNewLinkInterface mOnCreateNewLinkInterface; private View mDialogView; private TextInputEditText mNameField; private TextInputLayout mTargetPathLayout; private TextInputEditText mTargetPathField; private boolean mValidPath = false; private final TextWatcher mPathValidator = new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } @Override public void afterTextChanged(Editable s) { if (TextUtils.isEmpty(s)) { return; } Path targetPath = Paths.get(s.toString()); mValidPath = targetPath.getFile() != null && targetPath.exists(); if (!mValidPath) { mTargetPathLayout.setError(getText(R.string.invalid_target_path)); } else { mTargetPathLayout.setError(null); } } }; @SuppressLint("SetTextI18n") @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { mDialogView = View.inflate(requireActivity(), R.layout.dialog_new_symlink, null); mNameField = mDialogView.findViewById(R.id.name); mNameField.setText("New link"); mNameField.selectAll(); mTargetPathField = mDialogView.findViewById(R.id.target_file); mTargetPathLayout = mDialogView.findViewById(R.id.target_file_layout); mTargetPathField.addTextChangedListener(mPathValidator); return new MaterialAlertDialogBuilder(requireActivity()) .setTitle(R.string.create_new_symbolic_link) .setView(mDialogView) .setPositiveButton(R.string.ok, (dialog, which) -> { Editable name = mNameField.getText(); Editable targetPath = mTargetPathField.getText(); if (!TextUtils.isEmpty(name) && mValidPath && mOnCreateNewLinkInterface != null) { String newName = name.toString(); String prefix = Paths.trimPathExtension(newName); String extension = Paths.getPathExtension(newName, false); mOnCreateNewLinkInterface.onCreate(prefix, extension, Objects.requireNonNull(targetPath).toString()); } }) .setNegativeButton(R.string.cancel, null) .create(); } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return mDialogView; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { getLifecycle().addObserver(new SoftInputLifeCycleObserver(new WeakReference<>(mNameField))); } public void setOnCreateNewLinkInterface(@Nullable OnCreateNewLinkInterface createNewLinkInterface) { mOnCreateNewLinkInterface = createNewLinkInterface; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/fm/dialogs/OpenWithDialogFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.fm.dialogs; import android.app.Activity; import android.app.Application; import android.app.Dialog; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.os.UserHandleHidden; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.CheckBox; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.core.os.BundleCompat; import androidx.fragment.app.DialogFragment; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Objects; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.compat.ActivityManagerCompat; import io.github.muntashirakon.AppManager.fm.FmProvider; import io.github.muntashirakon.AppManager.intercept.ActivityInterceptor; import io.github.muntashirakon.AppManager.self.imagecache.ImageLoader; import io.github.muntashirakon.AppManager.settings.FeatureController; import io.github.muntashirakon.util.AdapterUtils; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.dialog.DialogTitleBuilder; import io.github.muntashirakon.dialog.SearchableItemsDialogBuilder; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.PathContentInfo; import io.github.muntashirakon.io.Paths; import io.github.muntashirakon.lifecycle.SingleLiveEvent; import io.github.muntashirakon.widget.SearchView; public class OpenWithDialogFragment extends DialogFragment { public static final String TAG = OpenWithDialogFragment.class.getSimpleName(); private static final String ARG_PATH = "path"; private static final String ARG_TYPE = "type"; private static final String ARG_CLOSE_ACTIVITY = "close"; @NonNull public static OpenWithDialogFragment getInstance(@NonNull Path path) { return getInstance(path, null); } @NonNull public static OpenWithDialogFragment getInstance(@NonNull Path path, @Nullable String type) { return getInstance(path.getUri(), type, false); } @NonNull public static OpenWithDialogFragment getInstance(@NonNull Uri uri, @Nullable String type, boolean closeActivity) { OpenWithDialogFragment fragment = new OpenWithDialogFragment(); Bundle args = new Bundle(); args.putParcelable(ARG_PATH, uri); args.putString(ARG_TYPE, type); args.putBoolean(ARG_CLOSE_ACTIVITY, closeActivity); fragment.setArguments(args); return fragment; } private static class ResolvedActivityInfo { @NonNull public final ResolveInfo resolveInfo; @NonNull public final String packageName; @NonNull public final String name; @NonNull public final String shortName; @NonNull public final CharSequence label; @NonNull public final CharSequence appLabel; private ResolvedActivityInfo(@NonNull ResolveInfo resolveInfo, @NonNull CharSequence label, @NonNull CharSequence appLabel) { this.resolveInfo = resolveInfo; this.packageName = resolveInfo.activityInfo.packageName; this.name = resolveInfo.activityInfo.name; this.shortName = getShortActivityName(this.name); this.label = label; this.appLabel = appLabel; } public boolean matches(@Nullable String constraint) { if (constraint == null) { return true; } // Match the following: // 1. Label // 2. Short name // 3. App label return label.toString().toLowerCase(Locale.getDefault()).contains(constraint) || shortName.contains(constraint) || appLabel.toString().toLowerCase(Locale.getDefault()).contains(constraint); } @NonNull private String getShortActivityName(@NonNull String longName) { int idxOfDot = longName.lastIndexOf('.'); if (idxOfDot == -1) { return longName; } return longName.substring(idxOfDot + 1); } } private Path mPath; private String mCustomType; private boolean mCloseActivity; private View mDialogView; private SearchView mSearchView; private OpenWithViewModel mViewModel; private MatchingActivitiesRecyclerViewAdapter mAdapter; @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { mViewModel = new ViewModelProvider(this).get(OpenWithViewModel.class); mPath = Paths.get(Objects.requireNonNull(BundleCompat.getParcelable(requireArguments(), ARG_PATH, Uri.class))); mCustomType = requireArguments().getString(ARG_TYPE, null); mCloseActivity = requireArguments().getBoolean(ARG_CLOSE_ACTIVITY, false); mAdapter = new MatchingActivitiesRecyclerViewAdapter(mViewModel, requireActivity()); mAdapter.setIntent(getIntent(mPath, mCustomType)); mDialogView = View.inflate(requireActivity(), R.layout.dialog_open_with, null); mSearchView = mDialogView.findViewById(io.github.muntashirakon.ui.R.id.action_search); mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { return false; } @Override public boolean onQueryTextChange(String newText) { mAdapter.setFilteredItems(newText); return true; } }); RecyclerView matchingActivitiesView = mDialogView.findViewById(R.id.intent_matching_activities); matchingActivitiesView.setLayoutManager(new LinearLayoutManager(requireContext())); matchingActivitiesView.setAdapter(mAdapter); // TODO: 19/11/22 Add support for always open and only for this file CheckBox alwaysOpen = mDialogView.findViewById(R.id.always_open); CheckBox openForThisFileOnly = mDialogView.findViewById(R.id.only_for_this_file); alwaysOpen.setVisibility(View.GONE); openForThisFileOnly.setVisibility(View.GONE); DialogTitleBuilder titleBuilder = new DialogTitleBuilder(requireActivity()) .setTitle(R.string.file_open_with) .setSubtitle(mPath.getName()) .setEndIcon(R.drawable.ic_open_in_new, v1 -> { if (mAdapter != null && mAdapter.getIntent().resolveActivityInfo(requireActivity() .getPackageManager(), 0) != null) { startActivity(mAdapter.getIntent()); } dismiss(); }) .setEndIconContentDescription(R.string.file_open_with_os_default_dialog); AlertDialog alertDialog = new MaterialAlertDialogBuilder(requireActivity()) .setCustomTitle(titleBuilder.build()) .setView(mDialogView) .setPositiveButton(R.string.file_open_as, null) .setNeutralButton(R.string.file_open_with_custom_activity, null) .create(); alertDialog.setOnShowListener(dialog -> { Button fileOpenAsButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); Button customButton = alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL); fileOpenAsButton.setOnClickListener(v -> { String[] customTypes = requireContext().getResources().getStringArray(R.array.file_open_as_option_types); new SearchableItemsDialogBuilder<>(requireActivity(), R.array.file_open_as_options) .setTitle(R.string.file_open_as) .hideSearchBar(true) .setOnItemClickListener((dialog1, which, item) -> { mCustomType = customTypes[which]; if (mAdapter != null) { mAdapter.setIntent(getIntent(mPath, mCustomType)); if (mViewModel != null) { // Reload activities mViewModel.loadMatchingActivities(mAdapter.getIntent()); } } dialog1.dismiss(); }) .setNegativeButton(R.string.close, null) .show(); }); // TODO: 20/11/22 Add option to set custom activity customButton.setVisibility(View.GONE); }); return alertDialog; } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return mDialogView; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { if (mViewModel != null) { mViewModel.getMatchingActivitiesLiveData().observe(getViewLifecycleOwner(), matchingActivities -> { mAdapter.setDefaultList(matchingActivities); // Don't display search bar if items are less than 6 mSearchView.setVisibility(matchingActivities.size() < 6 ? View.GONE : View.VISIBLE); }); mViewModel.getPathContentInfoLiveData().observe(getViewLifecycleOwner(), pathContentInfo -> { if (mAdapter != null) { mAdapter.setIntent(getIntent(mPath, pathContentInfo.getMimeType())); if (mViewModel != null) { // Reload activities mViewModel.loadMatchingActivities(mAdapter.getIntent()); } } }); mViewModel.getIntentLiveData().observe(getViewLifecycleOwner(), intent -> { try { // Resolved activities may contain non-exported activity ActivityManagerCompat.startActivity(intent, UserHandleHidden.myUserId()); dismiss(); } catch (SecurityException e) { UIUtils.displayLongToast("Failed: " + e.getMessage()); } }); if (mCustomType == null) { mViewModel.loadFileContentInfo(mPath); } if (mAdapter != null) { mViewModel.loadMatchingActivities(mAdapter.getIntent()); } } } @Override public void onDestroy() { super.onDestroy(); if (mCloseActivity) { requireActivity().finish(); } } @NonNull private Intent getIntent(@NonNull Path path, @Nullable String customType) { int flags = Intent.FLAG_ACTIVITY_NEW_TASK; if (path.canRead()) { flags |= Intent.FLAG_GRANT_READ_URI_PERMISSION; } if (path.canWrite()) { flags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION; } Intent intent = new Intent(Intent.ACTION_VIEW); intent.setDataAndType(FmProvider.getContentUri(path), customType != null ? customType : path.getType()); intent.setFlags(flags); return intent; } private static class MatchingActivitiesRecyclerViewAdapter extends RecyclerView.Adapter { @NonNull private final List mMatchingActivities = new ArrayList<>(); @NonNull private final ArrayList mFilteredItems = new ArrayList<>(); private final Activity mActivity; private final OpenWithViewModel mViewModel; private final ImageLoader mImageLoader = ImageLoader.getInstance(); private Intent mIntent; @Nullable private String mConstraint; public MatchingActivitiesRecyclerViewAdapter(OpenWithViewModel viewModel, Activity activity) { mViewModel = viewModel; mActivity = activity; } public Intent getIntent() { return mIntent; } public void setIntent(Intent intent) { mIntent = intent; } public void setDefaultList(@Nullable List matchingActivities) { mMatchingActivities.clear(); if (matchingActivities != null) { mMatchingActivities.addAll(matchingActivities); } filterItems(); } void setFilteredItems(@Nullable String constraint) { mConstraint = TextUtils.isEmpty(constraint) ? null : constraint.toLowerCase(Locale.getDefault()); filterItems(); } private void filterItems() { synchronized (mFilteredItems) { int lastCount = mFilteredItems.size(); mFilteredItems.clear(); for (int i = 0; i < mMatchingActivities.size(); ++i) { if (mConstraint == null || mMatchingActivities.get(i).matches(mConstraint)) { mFilteredItems.add(i); } } AdapterUtils.notifyDataSetChanged(this, lastCount, mFilteredItems.size()); } } @NonNull @Override public MatchingActivitiesRecyclerViewAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(io.github.muntashirakon.ui.R.layout.m3_preference, parent, false); return new MatchingActivitiesRecyclerViewAdapter.ViewHolder(view); } @Override public void onBindViewHolder(@NonNull MatchingActivitiesRecyclerViewAdapter.ViewHolder holder, int position) { int index; synchronized (mFilteredItems) { index = mFilteredItems.get(position); } ResolvedActivityInfo resolvedInfo = mMatchingActivities.get(index); holder.title.setText(resolvedInfo.label); String activityName = resolvedInfo.name; String summary = resolvedInfo.appLabel + "\n" + resolvedInfo.shortName; holder.summary.setText(summary); String tag = resolvedInfo.packageName + "_" + resolvedInfo.label; holder.icon.setTag(tag); mImageLoader.displayImage(tag, holder.icon, new ResolveInfoImageFetcher(resolvedInfo.resolveInfo)); holder.itemView.setOnClickListener(v -> { Intent intent = new Intent(mIntent); intent.setClassName(resolvedInfo.packageName, activityName); mViewModel.openIntent(intent); }); holder.itemView.setOnLongClickListener(v -> { if (!FeatureController.isInterceptorEnabled()) { return false; } Intent intent = new Intent(mIntent); intent.putExtra(ActivityInterceptor.EXTRA_PACKAGE_NAME, resolvedInfo.packageName); intent.putExtra(ActivityInterceptor.EXTRA_CLASS_NAME, activityName); intent.setClassName(mActivity, ActivityInterceptor.class.getName()); mViewModel.openIntent(intent); return true; }); } @Override public int getItemCount() { synchronized (mFilteredItems) { return mFilteredItems.size(); } } static class ViewHolder extends RecyclerView.ViewHolder { TextView title; TextView summary; ImageView icon; public ViewHolder(@NonNull View itemView) { super(itemView); title = itemView.findViewById(android.R.id.title); summary = itemView.findViewById(android.R.id.summary); icon = itemView.findViewById(android.R.id.icon); icon.setContentDescription(itemView.getContext().getString(R.string.icon)); } } } public static class OpenWithViewModel extends AndroidViewModel { private final MutableLiveData> mMatchingActivitiesLiveData = new MutableLiveData<>(); private final MutableLiveData mPathContentInfoLiveData = new MutableLiveData<>(); private final SingleLiveEvent mIntentLiveData = new SingleLiveEvent<>(); private final PackageManager mPm; public OpenWithViewModel(@NonNull Application application) { super(application); mPm = application.getPackageManager(); } public void loadMatchingActivities(@NonNull Intent intent) { ThreadUtils.postOnBackgroundThread(() -> { List resolveInfoList = mPm.queryIntentActivities(intent, 0); List resolvedActivityInfoList = new ArrayList<>(resolveInfoList.size()); for (ResolveInfo resolveInfo : resolveInfoList) { CharSequence label = resolveInfo.loadLabel(mPm); CharSequence appLabel = resolveInfo.activityInfo.applicationInfo.loadLabel(mPm); resolvedActivityInfoList.add(new ResolvedActivityInfo(resolveInfo, label, appLabel)); } mMatchingActivitiesLiveData.postValue(resolvedActivityInfoList); }); } public void loadFileContentInfo(@NonNull Path path) { ThreadUtils.postOnBackgroundThread(() -> mPathContentInfoLiveData.postValue(path.getPathContentInfo())); } public void openIntent(@NonNull Intent intent) { mIntentLiveData.setValue(intent); } public LiveData> getMatchingActivitiesLiveData() { return mMatchingActivitiesLiveData; } public LiveData getPathContentInfoLiveData() { return mPathContentInfoLiveData; } public LiveData getIntentLiveData() { return mIntentLiveData; } @Override protected void onCleared() { super.onCleared(); } } private static class ResolveInfoImageFetcher implements ImageLoader.ImageFetcherInterface { @Nullable private final ResolveInfo mInfo; public ResolveInfoImageFetcher(@Nullable ResolveInfo info) { mInfo = info; } @Override @NonNull public ImageLoader.ImageFetcherResult fetchImage(@NonNull String tag) { PackageManager pm = ContextUtils.getContext().getPackageManager(); Drawable drawable = mInfo != null ? mInfo.loadIcon(pm) : null; return new ImageLoader.ImageFetcherResult(tag, drawable != null ? UIUtils.getBitmapFromDrawable(drawable) : null, false, true, new ImageLoader.DefaultImageDrawable("android_default_icon", pm.getDefaultActivityIcon())); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/fm/dialogs/RenameDialogFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.fm.dialogs; import android.app.Dialog; import android.os.Bundle; import android.text.Editable; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.DialogFragment; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.textfield.TextInputEditText; import java.lang.ref.WeakReference; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.io.Paths; import io.github.muntashirakon.lifecycle.SoftInputLifeCycleObserver; public class RenameDialogFragment extends DialogFragment { public static final String TAG = RenameDialogFragment.class.getSimpleName(); public interface OnRenameFilesInterface { void onRename(@NonNull String prefix, @Nullable String extension); } private static final String ARG_NAME = "name"; @NonNull public static RenameDialogFragment getInstance(@Nullable String name, @Nullable OnRenameFilesInterface renameFilesInterface) { RenameDialogFragment fragment = new RenameDialogFragment(); Bundle args = new Bundle(); args.putString(ARG_NAME, name); fragment.setArguments(args); fragment.setOnRenameFilesInterface(renameFilesInterface); return fragment; } @Nullable private OnRenameFilesInterface mOnRenameFilesInterface; private View mDialogView; private TextInputEditText mEditText; @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { String name = getArguments() != null ? requireArguments().getString(ARG_NAME) : null; mDialogView = View.inflate(requireActivity(), R.layout.dialog_rename, null); mEditText = mDialogView.findViewById(R.id.rename); mEditText.setText(name); if (name != null) { int lastIndex = name.lastIndexOf('.'); if (lastIndex != -1 || lastIndex == name.length() - 1) { mEditText.setSelection(0, lastIndex); } else { mEditText.selectAll(); } } return new MaterialAlertDialogBuilder(requireActivity()) .setTitle(R.string.rename) .setView(mDialogView) .setPositiveButton(R.string.save, (dialog, which) -> { Editable editable = mEditText.getText(); if (!TextUtils.isEmpty(editable) && mOnRenameFilesInterface != null) { String newName = editable.toString(); String prefix = Paths.trimPathExtension(newName); String extension = Paths.getPathExtension(newName, false); mOnRenameFilesInterface.onRename(prefix, extension); } }) .setNegativeButton(R.string.cancel, null) .create(); } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return mDialogView; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { getLifecycle().addObserver(new SoftInputLifeCycleObserver(new WeakReference<>(mEditText))); } public void setOnRenameFilesInterface(@Nullable OnRenameFilesInterface renameFilesInterface) { mOnRenameFilesInterface = renameFilesInterface; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/fm/icons/EpubCoverGenerator.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.fm.icons; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import io.github.muntashirakon.compat.xml.Xml; final class EpubCoverGenerator { @Nullable public static Bitmap generateFromFile(@NonNull File file) { try(ZipFile zipFile = new ZipFile(file)) { String opfFile = getOpfFileLocation(zipFile, zipFile.getEntry("META-INF/container.xml")); if (opfFile == null) { return null; } String coverId = getCoverId(zipFile, zipFile.getEntry(opfFile)); if (coverId == null) { return null; } String coverImage = getCover(zipFile, zipFile.getEntry(opfFile), coverId); if (coverImage == null) { return null; } String parent = new File(opfFile).getParent(); String coverImageLocation; if (parent != null) { coverImageLocation = parent + File.separator + coverImage; } else coverImageLocation = coverImage; ZipEntry coverEntry = zipFile.getEntry(coverImageLocation); if (coverEntry != null) { return BitmapFactory.decodeStream(zipFile.getInputStream(coverEntry)); } } catch (IOException e) { e.printStackTrace(); } return null; } @Nullable private static String getOpfFileLocation(@NonNull ZipFile zipFile, @Nullable ZipEntry zipEntry) { if (zipEntry == null) { return null; } try { XmlPullParser parser = Xml.newFastPullParser(); parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false); parser.setInput(zipFile.getInputStream(zipEntry), StandardCharsets.UTF_8.name()); parser.nextTag(); parser.require(XmlPullParser.START_TAG, null, "container"); int event = parser.next(); while (event != XmlPullParser.END_DOCUMENT) { if (event == XmlPullParser.START_TAG) { String tagName = parser.getName(); if ("rootfile".equals(tagName)) { return parser.getAttributeValue(null, "full-path"); } } event = parser.next(); } } catch (IOException | XmlPullParserException ignore) { } return null; } @Nullable private static String getCoverId(@NonNull ZipFile zipFile, @Nullable ZipEntry zipEntry) { if (zipEntry == null) { return null; } try { XmlPullParser parser = Xml.newFastPullParser(); parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false); parser.setInput(zipFile.getInputStream(zipEntry), StandardCharsets.UTF_8.name()); parser.nextTag(); parser.require(XmlPullParser.START_TAG, null, "package"); int event = parser.next(); while (event != XmlPullParser.END_DOCUMENT) { if (event == XmlPullParser.START_TAG) { String tagName = parser.getName(); if ("meta".equals(tagName) && "cover".equals(parser.getAttributeValue(null, "name"))) { return parser.getAttributeValue(null, "content"); } } event = parser.next(); } } catch (IOException | XmlPullParserException ignore) { } return null; } @Nullable private static String getCover(@NonNull ZipFile zipFile, @Nullable ZipEntry zipEntry, @NonNull String coverId) { if (zipEntry == null) { return null; } try { XmlPullParser parser = Xml.newFastPullParser(); parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false); parser.setInput(zipFile.getInputStream(zipEntry), StandardCharsets.UTF_8.name()); parser.nextTag(); parser.require(XmlPullParser.START_TAG, null, "package"); int event = parser.next(); while (event != XmlPullParser.END_DOCUMENT) { if (event == XmlPullParser.START_TAG) { String tagName = parser.getName(); String id = parser.getAttributeValue(null, "id"); if ("item".equals(tagName) && id == null) { event = parser.next(); continue; } String properties = parser.getAttributeValue(null, "properties"); if (coverId.equals(id)) { return parser.getAttributeValue(null, "href"); } else if ("cover-image".equals(properties)) { return parser.getAttributeValue(null, "href"); } } event = parser.next(); } } catch (IOException | XmlPullParserException ignore) { } return null; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/fm/icons/FmIconFetcher.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.fm.icons; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.media.ThumbnailUtils; import android.util.Size; import androidx.annotation.NonNull; import com.j256.simplemagic.ContentType; import java.io.IOException; import java.io.InputStream; import java.util.HashSet; import java.util.Locale; import java.util.Set; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.compat.ThumbnailUtilsCompat; import io.github.muntashirakon.AppManager.fm.ContentType2; import io.github.muntashirakon.AppManager.fm.FmItem; import io.github.muntashirakon.AppManager.fm.FmProvider; import io.github.muntashirakon.AppManager.self.imagecache.ImageLoader; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.io.PathContentInfo; import io.github.muntashirakon.svg.SVG; import io.github.muntashirakon.svg.SVGParser; import io.github.muntashirakon.util.UiUtils; public class FmIconFetcher implements ImageLoader.ImageFetcherInterface { private static final Set OPEN_DOCUMENT_FORMAT_MIME_TYPES = new HashSet() {{ add("application/vnd.oasis.opendocument.text"); add("application/vnd.oasis.opendocument.spreadsheet"); add("application/vnd.oasis.opendocument.presentation"); add("application/vnd.oasis.opendocument.graphics"); add("application/vnd.oasis.opendocument.chart"); add("application/vnd.oasis.opendocument.formula"); add("application/vnd.oasis.opendocument.image"); add("application/vnd.oasis.opendocument.text-master"); add("application/vnd.sun.xml.base"); add("application/vnd.oasis.opendocument.base"); add("application/vnd.oasis.opendocument.database"); // Templates add("application/vnd.oasis.opendocument.text-template"); add("application/vnd.oasis.opendocument.spreadsheet-template"); add("application/vnd.oasis.opendocument.presentation-template"); add("application/vnd.oasis.opendocument.graphics-template"); add("application/vnd.oasis.opendocument.chart-template"); add("application/vnd.oasis.opendocument.formula-template"); add("application/vnd.oasis.opendocument.text-web"); }}; @NonNull private final FmItem mFmItem; public FmIconFetcher(@NonNull FmItem fmItem) { mFmItem = fmItem; } @NonNull @Override public ImageLoader.ImageFetcherResult fetchImage(@NonNull String tag) { PathContentInfo contentInfo = mFmItem.getContentInfo(); if (contentInfo == null) { contentInfo = mFmItem.path.getPathContentInfo(); mFmItem.setContentInfo(contentInfo); } String mimeType = contentInfo.getMimeType(); int drawableRes = FmIcons.getDrawableFromType(mimeType); int padding = UiUtils.dpToPx(ContextUtils.getContext(), 4); int length = UiUtils.dpToPx(ContextUtils.getContext(), 40); ImageLoader.DefaultImage defaultImage = new ImageLoader.DefaultImageDrawableRes("drawable_" + drawableRes, drawableRes, padding); Size size = new Size(length, length); if (OPEN_DOCUMENT_FORMAT_MIME_TYPES.contains(mimeType)) { // Open document format Bitmap bitmap = FmIcons.getOpenDocumentThumbnail(mFmItem.path); if (bitmap != null) { return new ImageLoader.ImageFetcherResult(tag, getThumbnail(bitmap, size, true), false, true, defaultImage); } } // Others if (FmIcons.isApk(drawableRes)) { if (ContentType.APK.getMimeType().equals(mimeType)) { Bitmap bitmap = FmIcons.generateApkIcon(mFmItem.path); if (bitmap != null) { return new ImageLoader.ImageFetcherResult(tag, getThumbnail(bitmap, size, true), false, true, defaultImage); } } else if (ContentType2.APKM.getMimeType().equals(mimeType)) { Bitmap bitmap = FmIcons.getApkmIcon(mFmItem.path); if (bitmap != null) { return new ImageLoader.ImageFetcherResult(tag, getThumbnail(bitmap, size, true), false, true, defaultImage); } } else { Bitmap bitmap = FmIcons.getApksIcon(mFmItem.path); if (bitmap != null) { return new ImageLoader.ImageFetcherResult(tag, getThumbnail(bitmap, size, true), false, true, defaultImage); } } } else if (FmIcons.isArchive(drawableRes)) { if (ContentType.JAVA_ARCHIVE.getMimeType().equals(mimeType)) { Bitmap bitmap = FmIcons.generateJ2meIcon(mFmItem.path); if (bitmap != null) { return new ImageLoader.ImageFetcherResult(tag, getThumbnail(bitmap, size, true), false, true, defaultImage); } } } else if (FmIcons.isAudio(drawableRes)) { try { Bitmap bitmap = ThumbnailUtilsCompat.createAudioThumbnail(ContextUtils.getContext(), FmProvider.getContentUri(mFmItem.path), size, null); return new ImageLoader.ImageFetcherResult(tag, bitmap, false, true, defaultImage); } catch (IOException e) { e.printStackTrace(); } } else if (FmIcons.isVideo(drawableRes)) { try { Bitmap bitmap = ThumbnailUtilsCompat.createVideoThumbnail(ContextUtils.getContext(), FmProvider.getContentUri(mFmItem.path), size, null); return new ImageLoader.ImageFetcherResult(tag, getThumbnail(bitmap, size, true), false, true, defaultImage); } catch (IOException e) { e.printStackTrace(); } } else if (FmIcons.isImage(drawableRes)) { if (ContentType.SVG.getMimeType().equals(mimeType)) { // Load SVG image try (InputStream is = mFmItem.path.openInputStream()) { SVG svg = SVGParser.getSVGFromInputStream(is); Bitmap bitmap = svg.getBitmap(); return new ImageLoader.ImageFetcherResult(tag, getThumbnail(bitmap, size, true), false, true, defaultImage); } catch (Throwable th) { // There can be runtime exceptions th.printStackTrace(); } } else { byte[] bytes = mFmItem.path.getContentAsBinary(); if (bytes.length > 0) { Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length); if (bitmap != null) { return new ImageLoader.ImageFetcherResult(tag, getThumbnail(bitmap, size, true), false, true, defaultImage); } } } } else if (FmIcons.isEbook(drawableRes)) { if (ContentType.EPUB.getMimeType().equals(mimeType)) { Bitmap bitmap = FmIcons.generateEpubCover(mFmItem.path); if (bitmap != null) { return new ImageLoader.ImageFetcherResult(tag, getThumbnail(bitmap, size, true), false, true, defaultImage); } } } else if (FmIcons.isFont(drawableRes)) { Bitmap bitmap = FmIcons.generateFontBitmap(mFmItem.path); if (bitmap != null) { return new ImageLoader.ImageFetcherResult(tag, bitmap, false, true, defaultImage); } } else if (FmIcons.isPdf(drawableRes)) { Bitmap bitmap = FmIcons.generatePdfBitmap(ContextUtils.getContext(), FmProvider.getContentUri(mFmItem.path)); if (bitmap != null) { return new ImageLoader.ImageFetcherResult(tag, getThumbnail(bitmap, size, true), false, true, defaultImage); } } else if (FmIcons.isGeneric(drawableRes)) { String extension = mFmItem.path.getExtension(); if (extension != null) { // Generate icon from extension (at most 4 characters) int len = Math.min(extension.length(), 4); String shortExt = extension.substring(0, len).toUpperCase(Locale.ROOT); String extTag = "fm_ext_" + shortExt; return new ImageLoader.ImageFetcherResult(tag, null, new ImageLoader.DefaultImageString(extTag, shortExt)); } if (mFmItem.path.canExecute()) { // Generate executable string drawableRes = R.drawable.ic_frost_termux; return new ImageLoader.ImageFetcherResult(tag, null, new ImageLoader.DefaultImageDrawableRes("drawable_" + drawableRes, drawableRes, padding)); } } return new ImageLoader.ImageFetcherResult(tag, null, defaultImage); } private Bitmap getThumbnail(@NonNull Bitmap bitmap, @NonNull Size size, boolean recycle) { return ThumbnailUtils.extractThumbnail(bitmap, size.getWidth(), size.getHeight(), recycle ? ThumbnailUtils.OPTIONS_RECYCLE_INPUT : 0); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/fm/icons/FmIcons.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.fm.icons; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Color; import android.graphics.Typeface; import android.graphics.pdf.PdfRenderer; import android.net.Uri; import android.provider.DocumentsContract; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.util.Pair; import java.io.File; import java.io.IOException; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.apk.ApkFile; import io.github.muntashirakon.AppManager.apk.UriApkSource; import io.github.muntashirakon.AppManager.self.filecache.FileCache; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.FileUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.io.Path; // Mostly taken from: https://github.com/zhanghai/MaterialFiles/blob/43a22b31d59a59e44a05a972269a3bd9cd0c9b8b/app/src/main/java/me/zhanghai/android/files/file/MimeTypeIcon.kt // Others are freelanced. final class FmIcons { private static final int DRAWABLE_APK = R.drawable.ic_android; private static final int DRAWABLE_ARCHIVE = R.drawable.ic_archive; private static final int DRAWABLE_AUDIO = R.drawable.ic_audio_file; private static final int DRAWABLE_CALENDAR = R.drawable.ic_calendar_month; private static final int DRAWABLE_CERTIFICATE = R.drawable.ic_shield_key; private static final int DRAWABLE_CODE = R.drawable.ic_code; private static final int DRAWABLE_CONTACT = R.drawable.ic_contact_page; private static final int DRAWABLE_DATABASE = R.drawable.ic_database; private static final int DRAWABLE_DIRECTORY = R.drawable.ic_folder; private static final int DRAWABLE_DOCUMENT = R.drawable.ic_file; private static final int DRAWABLE_EBOOK = R.drawable.ic_book; private static final int DRAWABLE_EMAIL = R.drawable.ic_email; private static final int DRAWABLE_FONT = R.drawable.ic_font_download; private static final int DRAWABLE_GENERIC = R.drawable.ic_file; private static final int DRAWABLE_IMAGE = R.drawable.ic_image; private static final int DRAWABLE_PDF = R.drawable.ic_pdf_file; private static final int DRAWABLE_PRESENTATION = R.drawable.ic_presentation; private static final int DRAWABLE_SPREADSHEET = R.drawable.ic_table; private static final int DRAWABLE_TEXT = R.drawable.ic_file_document; private static final int DRAWABLE_VIDEO = R.drawable.ic_video_file; private static final int DRAWABLE_WORD = DRAWABLE_DOCUMENT; private static final int DRAWABLE_EXCEL = DRAWABLE_SPREADSHEET; private static final int DRAWABLE_POWERPOINT = DRAWABLE_PRESENTATION; private static final Map sMimeTypeToIconMap = new HashMap() { { put("application/vnd.android.package-archive", DRAWABLE_APK); put("application/vnd.apkm", DRAWABLE_APK); put("application/x-apks", DRAWABLE_APK); put("application/xapk-package-archive", DRAWABLE_APK); put("application/gzip", DRAWABLE_ARCHIVE); // Not in IANA list, but Mozilla and Wikipedia say so. put("application/java-archive", DRAWABLE_ARCHIVE); put("application/mac-binhex40", DRAWABLE_ARCHIVE); // Not in IANA list, but AOSP MimeUtils says so. put("application/rar", DRAWABLE_ARCHIVE); put("application/zip", DRAWABLE_ARCHIVE); put("application/vnd.debian.binary-package", DRAWABLE_ARCHIVE); put("application/vnd.ms-cab-compressed", DRAWABLE_ARCHIVE); put("application/vnd.rar", DRAWABLE_ARCHIVE); put("application/x-7z-compressed", DRAWABLE_ARCHIVE); put("application/x-apple-diskimage", DRAWABLE_ARCHIVE); put("application/x-bzip", DRAWABLE_ARCHIVE); put("application/x-bzip2", DRAWABLE_ARCHIVE); put("application/x-compress", DRAWABLE_ARCHIVE); put("application/x-cpio", DRAWABLE_ARCHIVE); put("application/x-deb", DRAWABLE_ARCHIVE); put("application/x-debian-package", DRAWABLE_ARCHIVE); put("application/x-gtar", DRAWABLE_ARCHIVE); put("application/x-gtar-compressed", DRAWABLE_ARCHIVE); put("application/x-gzip", DRAWABLE_ARCHIVE); put("application/x-iso9660-image", DRAWABLE_ARCHIVE); put("application/x-java-archive", DRAWABLE_ARCHIVE); put("application/x-lha", DRAWABLE_ARCHIVE); put("application/x-lzh", DRAWABLE_ARCHIVE); put("application/x-lzma", DRAWABLE_ARCHIVE); put("application/x-lzx", DRAWABLE_ARCHIVE); put("application/x-rar", DRAWABLE_ARCHIVE); put("application/x-rar-compressed", DRAWABLE_ARCHIVE); put("application/x-stuffit", DRAWABLE_ARCHIVE); put("application/x-tar", DRAWABLE_ARCHIVE); put("application/x-webarchive", DRAWABLE_ARCHIVE); put("application/x-webarchive-xml", DRAWABLE_ARCHIVE); put("application/x-xz", DRAWABLE_ARCHIVE); put("application/ogg", DRAWABLE_AUDIO); put("application/x-flac", DRAWABLE_AUDIO); put("text/calendar", DRAWABLE_CALENDAR); put("text/x-vcalendar", DRAWABLE_CALENDAR); put("application/pem-certificate-chain", DRAWABLE_CERTIFICATE); put("application/pgp", DRAWABLE_CERTIFICATE); put("application/pgp-encrypted", DRAWABLE_CERTIFICATE); put("application/pgp-keys", DRAWABLE_CERTIFICATE); put("application/pgp-signature", DRAWABLE_CERTIFICATE); put("application/pkcs8", DRAWABLE_CERTIFICATE); put("application/x-pkcs12", DRAWABLE_CERTIFICATE); put("application/x-pkcs7-certificates", DRAWABLE_CERTIFICATE); put("application/x-pkcs7-certreqresp", DRAWABLE_CERTIFICATE); put("application/x-pkcs7-crl", DRAWABLE_CERTIFICATE); put("application/x-pkcs7-mime", DRAWABLE_CERTIFICATE); put("application/x-pkcs7-signature", DRAWABLE_CERTIFICATE); put("application/x-x509-ca-cert", DRAWABLE_CERTIFICATE); put("application/x-x509-server-cert", DRAWABLE_CERTIFICATE); put("application/x-x509-user-cert", DRAWABLE_CERTIFICATE); put("application/ecmascript", DRAWABLE_CODE); put("application/javascript", DRAWABLE_CODE); put("application/json", DRAWABLE_CODE); put("application/toml", DRAWABLE_CODE); put("application/typescript", DRAWABLE_CODE); put("application/xml", DRAWABLE_CODE); put("application/x-csh", DRAWABLE_CODE); put("application/x-ecmascript", DRAWABLE_CODE); put("application/x-javascript", DRAWABLE_CODE); put("application/x-latex", DRAWABLE_CODE); put("application/x-perl", DRAWABLE_CODE); put("application/x-plist", DRAWABLE_CODE); put("application/x-python", DRAWABLE_CODE); put("application/x-ruby", DRAWABLE_CODE); put("application/x-sh", DRAWABLE_CODE); put("application/x-shellscript", DRAWABLE_CODE); put("application/x-smali", DRAWABLE_CODE); put("application/x-texinfo", DRAWABLE_CODE); put("application/x-yaml", DRAWABLE_CODE); put("text/css", DRAWABLE_CODE); put("text/html", DRAWABLE_CODE); put("text/ecmascript", DRAWABLE_CODE); put("text/javascript", DRAWABLE_CODE); put("text/jscript", DRAWABLE_CODE); put("text/livescript", DRAWABLE_CODE); put("text/xml", DRAWABLE_CODE); put("text/x-asm", DRAWABLE_CODE); put("text/x-c++hdr", DRAWABLE_CODE); put("text/x-c++src", DRAWABLE_CODE); put("text/x-chdr", DRAWABLE_CODE); put("text/x-csh", DRAWABLE_CODE); put("text/x-csharp", DRAWABLE_CODE); put("text/x-csrc", DRAWABLE_CODE); put("text/x-dsrc", DRAWABLE_CODE); put("text/x-ecmascript", DRAWABLE_CODE); put("text/x-haskell", DRAWABLE_CODE); put("text/x-java", DRAWABLE_CODE); put("text/x-java-source", DRAWABLE_CODE); put("text/x-javascript", DRAWABLE_CODE); put("text/x-kotlin", DRAWABLE_CODE); put("text/x-literate-haskell", DRAWABLE_CODE); put("text/x-lua", DRAWABLE_CODE); put("text/x-pascal", DRAWABLE_CODE); put("text/x-perl", DRAWABLE_CODE); put("text/x-php", DRAWABLE_CODE); put("text/x-python", DRAWABLE_CODE); put("text/x-ruby", DRAWABLE_CODE); put("text/x-shellscript", DRAWABLE_CODE); put("text/x-tcl", DRAWABLE_CODE); put("text/x-tex", DRAWABLE_CODE); put("text/x-yaml", DRAWABLE_CODE); put("text/vcard", DRAWABLE_CONTACT); put("text/x-vcard", DRAWABLE_CONTACT); put("application/vnd.sqlite3", DRAWABLE_DATABASE); put("application/x-sqlite3", DRAWABLE_DATABASE); put("inode/directory", DRAWABLE_DIRECTORY); put(DocumentsContract.Document.MIME_TYPE_DIR, DRAWABLE_DIRECTORY); put("resource/folder", DRAWABLE_DIRECTORY); put("application/rtf", DRAWABLE_DOCUMENT); put("application/vnd.oasis.opendocument.text", DRAWABLE_DOCUMENT); put("application/vnd.oasis.opendocument.text-master", DRAWABLE_DOCUMENT); put("application/vnd.oasis.opendocument.text-template", DRAWABLE_DOCUMENT); put("application/vnd.oasis.opendocument.text-web", DRAWABLE_DOCUMENT); put("application/vnd.stardivision.writer", DRAWABLE_DOCUMENT); put("application/vnd.stardivision.writer-global", DRAWABLE_DOCUMENT); put("application/vnd.sun.xml.writer", DRAWABLE_DOCUMENT); put("application/vnd.sun.xml.writer.global", DRAWABLE_DOCUMENT); put("application/vnd.sun.xml.writer.template", DRAWABLE_DOCUMENT); put("application/x-abiword", DRAWABLE_DOCUMENT); put("application/x-kword", DRAWABLE_DOCUMENT); put("text/rtf", DRAWABLE_DOCUMENT); put("application/epub+zip", DRAWABLE_EBOOK); put("application/vnd.amazon.ebook", DRAWABLE_EBOOK); put("application/x-cbr", DRAWABLE_EBOOK); put("application/x-cbz", DRAWABLE_EBOOK); put("application/x-ibooks+zip", DRAWABLE_EBOOK); put("application/x-mobipocket-ebook", DRAWABLE_EBOOK); put("application/vnd.ms-outlook", DRAWABLE_EMAIL); put("message/rfc822", DRAWABLE_EMAIL); put("application/font-cff", DRAWABLE_FONT); put("application/font-off", DRAWABLE_FONT); put("application/font-sfnt", DRAWABLE_FONT); put("application/font-ttf", DRAWABLE_FONT); put("application/font-woff", DRAWABLE_FONT); put("application/vnd.ms-fontobject", DRAWABLE_FONT); put("application/vnd.ms-opentype", DRAWABLE_FONT); put("application/x-font", DRAWABLE_FONT); put("application/x-font-otf", DRAWABLE_FONT); put("application/x-font-ttf", DRAWABLE_FONT); put("application/x-font-woff", DRAWABLE_FONT); put("application/vnd.oasis.opendocument.graphics", DRAWABLE_IMAGE); put("application/vnd.oasis.opendocument.graphics-template", DRAWABLE_IMAGE); put("application/vnd.oasis.opendocument.image", DRAWABLE_IMAGE); put("application/vnd.stardivision.draw", DRAWABLE_IMAGE); put("application/vnd.sun.xml.draw", DRAWABLE_IMAGE); put("application/vnd.sun.xml.draw.template", DRAWABLE_IMAGE); put("application/vnd.visio", DRAWABLE_IMAGE); put("application/pdf", DRAWABLE_PDF); put("application/vnd.oasis.opendocument.presentation", DRAWABLE_PRESENTATION); put("application/vnd.oasis.opendocument.presentation-template", DRAWABLE_PRESENTATION); put("application/vnd.stardivision.impress", DRAWABLE_PRESENTATION); put("application/vnd.sun.xml.impress", DRAWABLE_PRESENTATION); put("application/vnd.sun.xml.impress.template", DRAWABLE_PRESENTATION); put("application/x-kpresenter", DRAWABLE_PRESENTATION); put("application/vnd.oasis.opendocument.spreadsheet", DRAWABLE_SPREADSHEET); put("application/vnd.oasis.opendocument.spreadsheet-template", DRAWABLE_SPREADSHEET); put("application/vnd.stardivision.calc", DRAWABLE_SPREADSHEET); put("application/vnd.sun.xml.calc", DRAWABLE_SPREADSHEET); put("application/vnd.sun.xml.calc.template", DRAWABLE_SPREADSHEET); put("application/x-kspread", DRAWABLE_SPREADSHEET); put("application/x-quicktimeplayer", DRAWABLE_VIDEO); put("application/x-shockwave-flash", DRAWABLE_VIDEO); put("application/msword", DRAWABLE_WORD); put("application/vnd.openxmlformats-officedocument.wordprocessingml.document", DRAWABLE_WORD); put("application/vnd.openxmlformats-officedocument.wordprocessingml.template", DRAWABLE_WORD); put("application/vnd.ms-excel", DRAWABLE_EXCEL); put("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", DRAWABLE_EXCEL); put("application/vnd.openxmlformats-officedocument.spreadsheetml.template", DRAWABLE_EXCEL); put("application/vnd.ms-powerpoint", DRAWABLE_POWERPOINT); put("application/vnd.openxmlformats-officedocument.presentationml.presentation", DRAWABLE_POWERPOINT); put("application/vnd.openxmlformats-officedocument.presentationml.slideshow", DRAWABLE_POWERPOINT); put("application/vnd.openxmlformats-officedocument.presentationml.template", DRAWABLE_POWERPOINT); } }; private static final Map sTypeToIconMap = new HashMap() { { put("audio", DRAWABLE_AUDIO); put("font", DRAWABLE_FONT); put("image", DRAWABLE_IMAGE); put("text", DRAWABLE_TEXT); put("video", DRAWABLE_VIDEO); } }; @DrawableRes public static int getDrawableFromType(@Nullable String mimeType) { if (mimeType == null) { return DRAWABLE_GENERIC; } Integer drawable = sMimeTypeToIconMap.get(mimeType); if (drawable != null) { return drawable; } String firstPart = mimeType.split("/")[0]; drawable = sTypeToIconMap.get(firstPart); return drawable != null ? drawable : DRAWABLE_GENERIC; } public static boolean isApk(@DrawableRes int drawable) { return drawable == DRAWABLE_APK; } public static boolean isArchive(@DrawableRes int drawable) { return drawable == DRAWABLE_ARCHIVE; } public static boolean isImage(@DrawableRes int drawable) { return drawable == DRAWABLE_IMAGE; } public static boolean isAudio(@DrawableRes int drawable) { return drawable == DRAWABLE_AUDIO; } public static boolean isVideo(@DrawableRes int drawable) { return drawable == DRAWABLE_VIDEO; } public static boolean isMedia(@DrawableRes int drawable) { return drawable == DRAWABLE_AUDIO || drawable == DRAWABLE_VIDEO; } public static boolean isEbook(@DrawableRes int drawable) { return drawable == DRAWABLE_EBOOK; } public static boolean isFont(@DrawableRes int drawable) { return drawable == DRAWABLE_FONT; } public static boolean isPdf(@DrawableRes int drawable) { return drawable == DRAWABLE_PDF; } public static boolean isGeneric(@DrawableRes int drawable) { return drawable == DRAWABLE_GENERIC; } @Nullable public static Bitmap generateFontBitmap(@NonNull Path path) { String extension = path.getExtension(); String text = extension != null ? extension.substring(0, Math.min(extension.length(), 4)) .toUpperCase(Locale.ROOT) : "FONT"; Pair file = getUsableFile(path); if (file == null) { return null; } try { Typeface typeface = Typeface.createFromFile(file.first); return UIUtils.generateBitmapFromText(text, typeface); } finally { if (file.second) { file.first.delete(); } } } @Nullable public static Bitmap generatePdfBitmap(@NonNull Context context, @NonNull Uri uri) { PdfRenderer renderer; try { renderer = new PdfRenderer(FileUtils.getFdFromUri(context, uri, "r")); } catch (IOException e) { e.printStackTrace(); return null; } PdfRenderer.Page page; try { page = renderer.openPage(0); } catch (RuntimeException e) { e.printStackTrace(); return null; } int srcWidth = page.getWidth(); int srcHeight = page.getHeight(); if (srcWidth <= 0 || srcHeight <= 0) { return null; } Bitmap bitmap = Bitmap.createBitmap(srcWidth, srcHeight, Bitmap.Config.ARGB_8888); bitmap.eraseColor(Color.WHITE); page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY); return bitmap; } @Nullable public static Bitmap generateEpubCover(@NonNull Path path) { Pair file = getUsableFile(path); if (file == null) { return null; } try { return EpubCoverGenerator.generateFromFile(file.first); } finally { if (file.second) { file.first.delete(); } } } @Nullable public static Bitmap generateJ2meIcon(@NonNull Path path) { Pair file = getUsableFile(path); if (file == null) { return null; } try { return J2meIconExtractor.generateFromFile(file.first); } finally { if (file.second) { file.first.delete(); } } } @Nullable public static Bitmap getOpenDocumentThumbnail(@NonNull Path path) { Pair file = getUsableFile(path); if (file == null) { return null; } try (ZipFile zipFile = new ZipFile(file.first)) { ZipEntry coverEntry = zipFile.getEntry("Thumbnails/thumbnail.png"); if (coverEntry != null) { return BitmapFactory.decodeStream(zipFile.getInputStream(coverEntry)); } } catch (IOException e) { e.printStackTrace(); } finally { if (file.second) { file.first.delete(); } } return null; } @Nullable public static Bitmap generateApkIcon(@NonNull Path path) { Pair file = getUsableFile(path); if (file == null) { return null; } try { return getApkIcon(file.first); } finally { if (file.second) { file.first.delete(); } } } @Nullable public static Bitmap getApkmIcon(@NonNull Path path) { Pair file = getUsableFile(path); if (file == null) { return null; } try (ZipFile zipFile = new ZipFile(file.first)) { ZipEntry iconEntry = zipFile.getEntry("icon.png"); if (iconEntry != null) { return BitmapFactory.decodeStream(zipFile.getInputStream(iconEntry)); } } catch (IOException e) { e.printStackTrace(); } finally { if (file.second) { file.first.delete(); } } return null; } @Nullable public static Bitmap getApksIcon(@NonNull Path path) { Pair file = getUsableFile(path); if (file == null) { return null; } try (ZipFile zipFile = new ZipFile(file.first)) { ZipEntry iconEntry = zipFile.getEntry("icon.png"); if (iconEntry != null) { return BitmapFactory.decodeStream(zipFile.getInputStream(iconEntry)); } // Load as ApkFile UriApkSource apkSource = new UriApkSource(Uri.fromFile(file.first), path.getType()); try (ApkFile apkFile = apkSource.resolve()) { ApkFile.Entry baseEntry = apkFile.getBaseEntry(); return getApkIcon(baseEntry.getFile(false)); } } catch (IOException | ApkFile.ApkFileException e) { e.printStackTrace(); } finally { if (file.second) { file.first.delete(); } } return null; } @Nullable private static Bitmap getApkIcon(@NonNull File apkFile) { PackageManager pm = ContextUtils.getContext().getPackageManager(); String f = apkFile.getAbsolutePath(); PackageInfo packageInfo = pm.getPackageArchiveInfo(f, 0); if (packageInfo != null) { ApplicationInfo info = packageInfo.applicationInfo; info.sourceDir = info.publicSourceDir = f; if (info.icon != 0) { return UIUtils.getBitmapFromDrawable(info.loadIcon(pm)); } } return null; } @Nullable private static Pair getUsableFile(@NonNull Path path) { File f = path.getFile(); if (f == null) { try { return new Pair<>(FileCache.getGlobalFileCache().getCachedFile(path), true); } catch (IOException ignore) { return null; } } f = new File(f.getPath()); if (!f.canRead()) { try { return new Pair<>(FileCache.getGlobalFileCache().getCachedFile(path), true); } catch (IOException ignore) { return null; } } return new Pair<>(f, false); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/fm/icons/J2meIconExtractor.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.fm.icons; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.io.File; import java.io.IOException; import java.util.jar.Attributes; import java.util.jar.Manifest; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; class J2meIconExtractor { @Nullable public static Bitmap generateFromFile(@NonNull File file) { try(ZipFile zipFile = new ZipFile(file)) { String iconFile = getIconLocation(zipFile, zipFile.getEntry("META-INF/MANIFEST.MF")); if (iconFile == null) { // Not a J2ME JAR return null; } ZipEntry iconEntry = zipFile.getEntry(iconFile); if (iconEntry != null) { return BitmapFactory.decodeStream(zipFile.getInputStream(iconEntry)); } } catch (IOException e) { e.printStackTrace(); } return null; } @Nullable private static String getIconLocation(@NonNull ZipFile zipFile, @Nullable ZipEntry zipEntry) { if (zipEntry == null) { return null; } try { Manifest manifest = new Manifest(zipFile.getInputStream(zipEntry)); Attributes attributes = manifest.getMainAttributes(); // The logic is derived from J2ME Loader (ru.woesss.j2me.jar.Descriptor#getIcon()) String icon = attributes.getValue("MIDlet-Icon"); if (icon == null || icon.trim().isEmpty()) { String midlet = "MIDlet-" + 1; icon = attributes.getValue(midlet); if (icon == null) { return null; } int start = icon.indexOf(','); if (start != -1) { int end = icon.indexOf(',', ++start); if (end != -1) icon = icon.substring(start, end); } } icon = icon.trim(); if (icon.isEmpty()) { return null; } while (icon.charAt(0) == '/') { icon = icon.substring(1); } return icon; } catch (IOException ignore) { } return null; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/history/IJsonSerializer.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.history; import androidx.annotation.NonNull; import org.json.JSONException; import org.json.JSONObject; public interface IJsonSerializer { @NonNull JSONObject serializeToJson() throws JSONException; } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/history/JsonDeserializer.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.history; import androidx.annotation.NonNull; import org.json.JSONException; import org.json.JSONObject; public class JsonDeserializer { public interface Creator { @NonNull T deserialize(@NonNull JSONObject jsonObject) throws JSONException; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/history/ops/OpHistoryActivity.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.history.ops; import android.app.Application; import android.content.Intent; import android.os.Bundle; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModelProvider; import com.google.android.material.card.MaterialCardView; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.progressindicator.LinearProgressIndicator; import org.json.JSONException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.Future; import io.github.muntashirakon.AppManager.BaseActivity; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.db.entity.OpHistory; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.utils.DateUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.AppManager.utils.appearance.ColorCodes; import io.github.muntashirakon.util.AdapterUtils; import io.github.muntashirakon.util.UiUtils; import io.github.muntashirakon.widget.RecyclerView; public class OpHistoryActivity extends BaseActivity { private OpHistoryViewModel mViewModel; private OpHistoryAdapter mAdapter; private LinearProgressIndicator mProgressIndicator; @Override protected void onAuthenticated(@Nullable Bundle savedInstanceState) { setContentView(R.layout.activity_op_history); setSupportActionBar(findViewById(R.id.toolbar)); mViewModel = new ViewModelProvider(this).get(OpHistoryViewModel.class); mProgressIndicator = findViewById(R.id.progress_linear); mProgressIndicator.setVisibilityAfterHide(View.GONE); RecyclerView listView = findViewById(android.R.id.list); listView.setLayoutManager(UIUtils.getGridLayoutAt450Dp(this)); listView.setEmptyView(findViewById(android.R.id.empty)); UiUtils.applyWindowInsetsAsPaddingNoTop(listView); mAdapter = new OpHistoryAdapter(this); listView.setAdapter(mAdapter); FloatingActionButton fab = findViewById(R.id.floatingActionButton); UiUtils.applyWindowInsetsAsMargin(fab); fab.setOnClickListener(v -> new MaterialAlertDialogBuilder(this) .setTitle(R.string.clear_history) .setMessage(R.string.are_you_sure) .setNegativeButton(R.string.no, null) .setPositiveButton(R.string.yes, (dialog, which) -> { mProgressIndicator.show(); mViewModel.clearHistory(); }) .show()); mViewModel.getOpHistoriesLiveData().observe(this, opHistories -> { mProgressIndicator.hide(); mAdapter.setDefaultList(opHistories); }); mViewModel.getClearHistoryLiveData().observe(this, cleared -> UIUtils.displayShortToast(cleared ? R.string.done : R.string.failed)); mViewModel.getServiceLauncherIntentLiveData().observe(this, intent -> { if (intent != null) { ContextCompat.startForegroundService(this, intent); } else { UIUtils.displayShortToast(R.string.failed); } }); OpHistoryManager.getHistoryAddedLiveData().observe(this, opHistory -> { // New history added mProgressIndicator.show(); mViewModel.loadOpHistories(); }); mProgressIndicator.show(); mViewModel.loadOpHistories(); } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int id = item.getItemId(); if (id == android.R.id.home) { finish(); } else return super.onOptionsItemSelected(item); return true; } static class OpHistoryAdapter extends RecyclerView.Adapter { private final List mAdapterList = new ArrayList<>(); private final OpHistoryActivity mActivity; private final int mColorSuccess; private final int mColorFailure; static class ViewHolder extends RecyclerView.ViewHolder { MaterialCardView itemView; TextView type; TextView title; TextView execTime; Button execBtn; public ViewHolder(@NonNull View itemView) { super(itemView); this.itemView = (MaterialCardView) itemView; type = itemView.findViewById(R.id.type); title = itemView.findViewById(android.R.id.title); execTime = itemView.findViewById(android.R.id.summary); execBtn = itemView.findViewById(R.id.item_action); } } OpHistoryAdapter(@NonNull OpHistoryActivity activity) { mActivity = activity; mColorSuccess = ColorCodes.getSuccessColor(activity); mColorFailure = ColorCodes.getFailureColor(activity); } void setDefaultList(@NonNull List list) { synchronized (mAdapterList) { AdapterUtils.notifyDataSetChanged(this, mAdapterList, list); } } @Override public int getItemCount() { synchronized (mAdapterList) { return mAdapterList.size(); } } @Override public long getItemId(int position) { synchronized (mAdapterList) { return mAdapterList.get(position).hashCode(); } } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_op_history, parent, false); return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { OpHistoryItem history; synchronized (mAdapterList) { history = mAdapterList.get(position); } holder.itemView.setStrokeColor(history.getStatus() ? mColorSuccess : mColorFailure); holder.type.setText(history.getLocalizedType(mActivity)); holder.title.setText(history.getLabel(mActivity)); holder.execTime.setText(DateUtils.formatLongDateTime(mActivity, history.getTimestamp())); holder.itemView.setOnClickListener(v -> { // TODO: 1/26/25 Display history info }); holder.itemView.setOnLongClickListener(v -> { // TODO: 1/26/25 Possible long click options // 1. Apply // 2. Delete // 3. Add as a profile (for profile and batch op) // 4. Export (for profile) // 5. Create shortcut return true; }); holder.execBtn.setOnClickListener(v -> new MaterialAlertDialogBuilder(mActivity) .setTitle(R.string.title_confirm_execution) .setMessage(R.string.are_you_sure) .setNegativeButton(R.string.no, null) .setPositiveButton(R.string.yes, (dialog, which) -> mActivity.mViewModel.getServiceLauncherIntent(history)) .show()); } } public static class OpHistoryViewModel extends AndroidViewModel { private final MutableLiveData> mOpHistoriesLiveData = new MutableLiveData<>(); private final MutableLiveData mClearHistoryLiveData = new MutableLiveData<>(); private final MutableLiveData mServiceLauncherIntentLiveData = new MutableLiveData<>(); private Future mOpHistoriesResult; public OpHistoryViewModel(@NonNull Application application) { super(application); } public LiveData> getOpHistoriesLiveData() { return mOpHistoriesLiveData; } public LiveData getClearHistoryLiveData() { return mClearHistoryLiveData; } public MutableLiveData getServiceLauncherIntentLiveData() { return mServiceLauncherIntentLiveData; } public void loadOpHistories() { if (mOpHistoriesResult != null) { mOpHistoriesResult.cancel(true); } mOpHistoriesResult = ThreadUtils.postOnBackgroundThread(() -> { synchronized (mOpHistoriesLiveData) { List opHistories = OpHistoryManager.getAllHistoryItems(); Collections.sort(opHistories, (o1, o2) -> -Long.compare(o1.execTime, o2.execTime)); List opHistoryItems = new ArrayList<>(opHistories.size()); for (OpHistory history : opHistories) { try { opHistoryItems.add(new OpHistoryItem(history)); } catch (JSONException e) { Log.w(TAG, e.getMessage(), e); } } mOpHistoriesLiveData.postValue(opHistoryItems); } }); } public void clearHistory() { ThreadUtils.postOnBackgroundThread(() -> { synchronized (mOpHistoriesLiveData) { OpHistoryManager.clearAllHistory(); mClearHistoryLiveData.postValue(true); mOpHistoriesLiveData.postValue(Collections.emptyList()); } }); } public void getServiceLauncherIntent(@NonNull OpHistoryItem opHistoryItem) { ThreadUtils.postOnBackgroundThread(() -> { try { Intent intent = OpHistoryManager.getExecutableIntent(getApplication(), opHistoryItem); mServiceLauncherIntentLiveData.postValue(intent); } catch (JSONException e) { Log.w(TAG, e.getMessage(), e); mServiceLauncherIntentLiveData.postValue(null); } }); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/history/ops/OpHistoryItem.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.history.ops; import android.content.Context; import android.content.res.Resources; import androidx.annotation.NonNull; import org.json.JSONException; import org.json.JSONObject; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.batchops.BatchOpsService; import io.github.muntashirakon.AppManager.db.entity.OpHistory; import io.github.muntashirakon.AppManager.utils.JSONUtils; public class OpHistoryItem { private final OpHistory opHistory; public final JSONObject jsonData; public OpHistoryItem(@NonNull OpHistory opHistory) throws JSONException { this.opHistory = opHistory; jsonData = new JSONObject(opHistory.serializedData); } @OpHistoryManager.HistoryType public String getType() { return opHistory.type; } @NonNull public String getLocalizedType(@NonNull Context context) { switch (opHistory.type) { case OpHistoryManager.HISTORY_TYPE_BATCH_OPS: try { return context.getString(jsonData.getInt("title_res")); } catch (Resources.NotFoundException | JSONException e) { return context.getString(R.string.batch_ops); } case OpHistoryManager.HISTORY_TYPE_INSTALLER: return context.getString(R.string.installer); case OpHistoryManager.HISTORY_TYPE_PROFILE: return context.getString(R.string.profiles); } throw new IllegalStateException("Invalid type: " + opHistory.type); } @NonNull public String getLabel(@NonNull Context context) { switch (opHistory.type) { case OpHistoryManager.HISTORY_TYPE_BATCH_OPS: try { int op = jsonData.getInt("op"); return BatchOpsService.getDesiredOpTitle(context, op); } catch (JSONException e) { return context.getString(R.string.unknown_op); } case OpHistoryManager.HISTORY_TYPE_INSTALLER: { String label = JSONUtils.optString(jsonData, "app_label"); if (label != null) { return label; } return context.getString(R.string.state_unknown); } case OpHistoryManager.HISTORY_TYPE_PROFILE: { String label = JSONUtils.optString(jsonData, "profile_name"); if (label != null) { return label; } return context.getString(R.string.state_unknown); } } throw new IllegalStateException("Invalid type: " + opHistory.type); } public long getTimestamp() { return opHistory.execTime; } public boolean getStatus() { return opHistory.status.equals(OpHistoryManager.STATUS_SUCCESS); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/history/ops/OpHistoryManager.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.history.ops; import android.content.Context; import android.content.Intent; import androidx.annotation.NonNull; import androidx.annotation.StringDef; import androidx.annotation.WorkerThread; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import org.json.JSONException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; import io.github.muntashirakon.AppManager.apk.installer.ApkQueueItem; import io.github.muntashirakon.AppManager.apk.installer.PackageInstallerService; import io.github.muntashirakon.AppManager.batchops.BatchOpsService; import io.github.muntashirakon.AppManager.batchops.BatchQueueItem; import io.github.muntashirakon.AppManager.db.AppsDb; import io.github.muntashirakon.AppManager.db.entity.OpHistory; import io.github.muntashirakon.AppManager.history.IJsonSerializer; import io.github.muntashirakon.AppManager.intercept.IntentCompat; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.profiles.ProfileApplierService; import io.github.muntashirakon.AppManager.profiles.ProfileQueueItem; public final class OpHistoryManager { public static final String TAG = OpHistoryManager.class.getSimpleName(); public static final String HISTORY_TYPE_BATCH_OPS = "batch_ops"; public static final String HISTORY_TYPE_INSTALLER = "installer"; public static final String HISTORY_TYPE_PROFILE = "profile"; @Retention(RetentionPolicy.SOURCE) @StringDef({HISTORY_TYPE_BATCH_OPS, HISTORY_TYPE_INSTALLER, HISTORY_TYPE_PROFILE}) public @interface HistoryType { } public static final String STATUS_SUCCESS = "success"; public static final String STATUS_FAILURE = "failure"; @Retention(RetentionPolicy.SOURCE) @StringDef({STATUS_SUCCESS, STATUS_FAILURE}) public @interface Status { } private static final MutableLiveData sHistoryAddedLiveData = new MutableLiveData<>(); public static LiveData getHistoryAddedLiveData() { return sHistoryAddedLiveData; } @WorkerThread public static long addHistoryItem(@HistoryType String historyType, @NonNull IJsonSerializer item, boolean success) { try { OpHistory opHistory = new OpHistory(); opHistory.type = historyType; opHistory.execTime = System.currentTimeMillis(); opHistory.serializedData = item.serializeToJson().toString(); opHistory.status = success ? STATUS_SUCCESS : STATUS_FAILURE; opHistory.serializedExtra = null; long id = AppsDb.getInstance().opHistoryDao().insert(opHistory); opHistory.id = id; sHistoryAddedLiveData.postValue(opHistory); return id; } catch (JSONException e) { Log.e(TAG, "Could not serialize " + item.getClass(), e); return -1; } } @WorkerThread public static List getAllHistoryItems() { return AppsDb.getInstance().opHistoryDao().getAll(); } @WorkerThread public static void clearAllHistory() { AppsDb.getInstance().opHistoryDao().deleteAll(); } @NonNull public static Intent getExecutableIntent(@NonNull Context context, @NonNull OpHistoryItem item) throws JSONException { switch (item.getType()) { case HISTORY_TYPE_BATCH_OPS: { BatchQueueItem batchQueueItem = BatchQueueItem.DESERIALIZER.deserialize(item.jsonData); return BatchOpsService.getServiceIntent(context, batchQueueItem); } case HISTORY_TYPE_INSTALLER: { ApkQueueItem apkQueueItem = ApkQueueItem.DESERIALIZER.deserialize(item.jsonData); Intent intent = new Intent(context, PackageInstallerService.class); IntentCompat.putWrappedParcelableExtra(intent, PackageInstallerService.EXTRA_QUEUE_ITEM, apkQueueItem); return intent; } case HISTORY_TYPE_PROFILE: { ProfileQueueItem profileQueueItem = ProfileQueueItem.DESERIALIZER.deserialize(item.jsonData); return ProfileApplierService.getIntent(context, profileQueueItem, true); } } throw new IllegalStateException("Invalid type: " + item.getType()); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/intercept/ActivityInterceptor.java ================================================ // SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.intercept; import android.content.ComponentName; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.BadParcelableException; import android.os.Build; import android.os.Bundle; import android.os.RemoteException; import android.os.UserHandleHidden; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.ImageView; import android.widget.TextView; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.collection.LruCache; import androidx.collection.SimpleArrayMap; import androidx.collection.SparseArrayCompat; import androidx.core.content.ContextCompat; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.button.MaterialButton; import com.google.android.material.checkbox.MaterialCheckBox; import com.google.android.material.textfield.TextInputEditText; import com.google.android.material.textfield.TextInputLayout; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.StringTokenizer; import java.util.UUID; import io.github.muntashirakon.AppManager.BaseActivity; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.compat.ActivityManagerCompat; import io.github.muntashirakon.AppManager.compat.IntegerCompat; import io.github.muntashirakon.AppManager.compat.ManifestCompat; import io.github.muntashirakon.AppManager.compat.PackageManagerCompat; import io.github.muntashirakon.AppManager.crypto.auth.AuthManager; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.runner.RunnerUtils; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.self.imagecache.ImageLoader; import io.github.muntashirakon.AppManager.settings.Ops; import io.github.muntashirakon.AppManager.shortcut.CreateShortcutDialogFragment; import io.github.muntashirakon.AppManager.utils.ClipboardUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.util.AdapterUtils; import io.github.muntashirakon.AppManager.utils.PackageUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.AppManager.utils.Utils; import io.github.muntashirakon.dialog.TextInputDropdownDialogBuilder; import io.github.muntashirakon.util.UiUtils; import io.github.muntashirakon.widget.MaterialAutoCompleteTextView; // Copyright 2020 Muntashir Al-Islam // Copyright 2017 k3b // Copyright 2012 Intrications public class ActivityInterceptor extends BaseActivity { public static final String TAG = ActivityInterceptor.class.getSimpleName(); public static final String EXTRA_PACKAGE_NAME = BuildConfig.APPLICATION_ID + ".intent.extra.PACKAGE_NAME"; public static final String EXTRA_CLASS_NAME = BuildConfig.APPLICATION_ID + ".intent.extra.CLASS_NAME"; public static final String EXTRA_ACTION = BuildConfig.APPLICATION_ID + ".intent.extra.ACTION"; // TODO(29/8/21): Enable getting activity result for activities launched with root public static final String EXTRA_ROOT = BuildConfig.APPLICATION_ID + ".intent.extra.ROOT"; // Root only public static final String EXTRA_USER_HANDLE = BuildConfig.APPLICATION_ID + ".intent.extra.USER_HANDLE"; // Whether to trigger the Intent on startup, requires `auth` parameter to be set public static final String EXTRA_TRIGGER_ON_START = BuildConfig.APPLICATION_ID + ".intent.extra.TRIGGER_ON_START"; public static final String EXTRA_AUTH = BuildConfig.APPLICATION_ID + ".intent.extra.AUTH"; private static final String INTENT_EDITED = "intent_edited"; private static final SparseArrayCompat INTENT_FLAG_TO_STRING = new SparseArrayCompat() { { put(Intent.FLAG_GRANT_READ_URI_PERMISSION, "FLAG_GRANT_READ_URI_PERMISSION"); put(Intent.FLAG_GRANT_WRITE_URI_PERMISSION, "FLAG_GRANT_WRITE_URI_PERMISSION"); put(Intent.FLAG_FROM_BACKGROUND, "FLAG_FROM_BACKGROUND"); put(Intent.FLAG_DEBUG_LOG_RESOLUTION, "FLAG_DEBUG_LOG_RESOLUTION"); put(Intent.FLAG_EXCLUDE_STOPPED_PACKAGES, "FLAG_EXCLUDE_STOPPED_PACKAGES"); put(Intent.FLAG_INCLUDE_STOPPED_PACKAGES, "FLAG_INCLUDE_STOPPED_PACKAGES"); put(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION, "FLAG_GRANT_PERSISTABLE_URI_PERMISSION"); put(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION, "FLAG_GRANT_PREFIX_URI_PERMISSION"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { put(Intent.FLAG_DIRECT_BOOT_AUTO, "FLAG_DIRECT_BOOT_AUTO"); } put(0x00000200, "FLAG_IGNORE_EPHEMERAL"); put(Intent.FLAG_ACTIVITY_NO_HISTORY, "FLAG_ACTIVITY_NO_HISTORY"); put(Intent.FLAG_ACTIVITY_SINGLE_TOP, "FLAG_ACTIVITY_SINGLE_TOP"); put(Intent.FLAG_ACTIVITY_NEW_TASK, "FLAG_ACTIVITY_NEW_TASK"); put(Intent.FLAG_ACTIVITY_MULTIPLE_TASK, "FLAG_ACTIVITY_MULTIPLE_TASK"); put(Intent.FLAG_ACTIVITY_CLEAR_TOP, "FLAG_ACTIVITY_CLEAR_TOP"); put(Intent.FLAG_ACTIVITY_FORWARD_RESULT, "FLAG_ACTIVITY_FORWARD_RESULT"); put(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP, "FLAG_ACTIVITY_PREVIOUS_IS_TOP"); put(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS, "FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS"); put(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT, "FLAG_ACTIVITY_BROUGHT_TO_FRONT"); put(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED, "FLAG_ACTIVITY_RESET_TASK_IF_NEEDED"); put(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY, "FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY"); put(Intent.FLAG_ACTIVITY_NEW_DOCUMENT, "FLAG_ACTIVITY_NEW_DOCUMENT"); put(Intent.FLAG_ACTIVITY_NO_USER_ACTION, "FLAG_ACTIVITY_NO_USER_ACTION"); put(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT, "FLAG_ACTIVITY_REORDER_TO_FRONT"); put(Intent.FLAG_ACTIVITY_NO_ANIMATION, "FLAG_ACTIVITY_NO_ANIMATION"); put(Intent.FLAG_ACTIVITY_CLEAR_TASK, "FLAG_ACTIVITY_CLEAR_TASK"); put(Intent.FLAG_ACTIVITY_TASK_ON_HOME, "FLAG_ACTIVITY_TASK_ON_HOME"); put(Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS, "FLAG_ACTIVITY_RETAIN_IN_RECENTS"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { put(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT, "FLAG_ACTIVITY_LAUNCH_ADJACENT"); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { put(Intent.FLAG_ACTIVITY_MATCH_EXTERNAL, "FLAG_ACTIVITY_MATCH_EXTERNAL"); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { put(Intent.FLAG_ACTIVITY_REQUIRE_NON_BROWSER, "FLAG_ACTIVITY_REQUIRE_NON_BROWSER"); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { put(Intent.FLAG_ACTIVITY_REQUIRE_DEFAULT, "FLAG_ACTIVITY_REQUIRE_DEFAULT"); } } }; // TODO(25/1/21): Add support for receiver flags private static final SparseArrayCompat INTENT_RECEIVER_FLAG_TO_STRING = new SparseArrayCompat() { { put(Intent.FLAG_RECEIVER_REGISTERED_ONLY, "FLAG_RECEIVER_REGISTERED_ONLY"); put(Intent.FLAG_RECEIVER_REPLACE_PENDING, "FLAG_RECEIVER_REPLACE_PENDING"); put(Intent.FLAG_RECEIVER_FOREGROUND, "FLAG_RECEIVER_FOREGROUND"); put(0x80000000, "FLAG_RECEIVER_OFFLOAD"); put(Intent.FLAG_RECEIVER_NO_ABORT, "FLAG_RECEIVER_NO_ABORT"); put(0x04000000, "FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT"); put(0x02000000, "FLAG_RECEIVER_BOOT_UPGRADE"); put(0x01000000, "FLAG_RECEIVER_INCLUDE_BACKGROUND"); put(0x00800000, "FLAG_RECEIVER_EXCLUDE_BACKGROUND"); put(0x00400000, "FLAG_RECEIVER_FROM_SHELL"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { put(Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS, "FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS"); } } }; private static final List INTENT_CATEGORIES = new ArrayList() { { add(Intent.CATEGORY_DEFAULT); add(Intent.CATEGORY_BROWSABLE); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { add(Intent.CATEGORY_VOICE); } add(Intent.CATEGORY_ALTERNATIVE); add(Intent.CATEGORY_SELECTED_ALTERNATIVE); add(Intent.CATEGORY_TAB); add(Intent.CATEGORY_LAUNCHER); add(Intent.CATEGORY_LEANBACK_LAUNCHER); add("android.intent.category.CAR_LAUNCHER"); add("android.intent.category.LEANBACK_SETTINGS"); add(Intent.CATEGORY_INFO); add(Intent.CATEGORY_HOME); add("android.intent.category.HOME_MAIN"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { add(Intent.CATEGORY_SECONDARY_HOME); } add("android.intent.category.SETUP_WIZARD"); add("android.intent.category.LAUNCHER_APP"); add(Intent.CATEGORY_PREFERENCE); add(Intent.CATEGORY_DEVELOPMENT_PREFERENCE); add(Intent.CATEGORY_EMBED); add(Intent.CATEGORY_APP_MARKET); add(Intent.CATEGORY_MONKEY); add(Intent.CATEGORY_TEST); add(Intent.CATEGORY_UNIT_TEST); add(Intent.CATEGORY_SAMPLE_CODE); add(Intent.CATEGORY_OPENABLE); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { add(Intent.CATEGORY_TYPED_OPENABLE); } add(Intent.CATEGORY_FRAMEWORK_INSTRUMENTATION_TEST); add(Intent.CATEGORY_CAR_DOCK); add(Intent.CATEGORY_DESK_DOCK); add(Intent.CATEGORY_LE_DESK_DOCK); add(Intent.CATEGORY_HE_DESK_DOCK); add(Intent.CATEGORY_CAR_MODE); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { add(Intent.CATEGORY_VR_HOME); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { add(Intent.CATEGORY_ACCESSIBILITY_SHORTCUT_TARGET); } add(Intent.CATEGORY_APP_BROWSER); add(Intent.CATEGORY_APP_CALCULATOR); add(Intent.CATEGORY_APP_CALENDAR); add(Intent.CATEGORY_APP_CONTACTS); add(Intent.CATEGORY_APP_EMAIL); add(Intent.CATEGORY_APP_GALLERY); add(Intent.CATEGORY_APP_MAPS); add(Intent.CATEGORY_APP_MESSAGING); add(Intent.CATEGORY_APP_MUSIC); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { add(Intent.CATEGORY_APP_FILES); } } }; private abstract class IntentUpdateTextWatcher implements TextWatcher { private final TextView mTextView; IntentUpdateTextWatcher(TextView textView) { mTextView = textView; } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { if (mAreTextWatchersActive) { try { String modifiedContent = mTextView.getText().toString(); onUpdateIntent(modifiedContent); showTextViewIntentData(mTextView); showResetIntentButton(true); refreshUI(); } catch (Exception e) { UIUtils.displayShortToast(e.getMessage()); e.printStackTrace(); } } } abstract protected void onUpdateIntent(String modifiedContent); @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void afterTextChanged(Editable s) { } } private MaterialAutoCompleteTextView mActionView; private MaterialAutoCompleteTextView mDataView; private MaterialAutoCompleteTextView mTypeView; private MaterialAutoCompleteTextView mUriView; private MaterialAutoCompleteTextView mPackageNameView; private MaterialAutoCompleteTextView mClassNameView; private TextInputEditText mIdView; private TextInputEditText mUserIdEdit; @Nullable private HistoryEditText mHistory; private CategoriesRecyclerViewAdapter mCategoriesAdapter; private FlagsRecyclerViewAdapter mFlagsAdapter; private ExtrasRecyclerViewAdapter mExtrasAdapter; private MatchingActivitiesRecyclerViewAdapter mMatchingActivitiesAdapter; private TextView mActivitiesHeader; private Button mResendIntentButton; private Button mResetIntentButton; /** * String representation of intent as URI */ @Nullable private String mOriginalIntent; /** * Extras that are lost during intent to string conversion */ @Nullable private Bundle mAdditionalExtras; @Nullable private Intent mMutableIntent; @Nullable private ComponentName mRequestedComponent; private boolean mUseRoot; private int mUserHandle; @Nullable private Integer mLastResultCode = null; @Nullable private Intent mLastResultIntent = null; private final LruCache mPackageLabelMap = new LruCache<>(16); private volatile boolean mAreTextWatchersActive; private final ActivityResultLauncher mIntentLauncher = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { Intent data = result.getData(); mLastResultCode = result.getResultCode(); mLastResultIntent = result.getData(); // Forward the result of the activity to the caller activity setResult(result.getResultCode(), data); refreshUI(); Uri uri = data == null ? null : data.getData(); UIUtils.displayLongToast("%s: (%s)", getString(R.string.activity_result), uri); }); @Override public void onAuthenticated(Bundle savedInstanceState) { setContentView(R.layout.activity_interceptor); setSupportActionBar(findViewById(R.id.toolbar)); findViewById(R.id.progress_linear).setVisibility(View.GONE); // Get Intent Intent intent = new Intent(getIntent()); mUseRoot = Ops.isWorkingUidRoot() && intent.getBooleanExtra(EXTRA_ROOT, false); mUserHandle = intent.getIntExtra(EXTRA_USER_HANDLE, UserHandleHidden.myUserId()); intent.removeExtra(EXTRA_ROOT); intent.removeExtra(EXTRA_USER_HANDLE); intent.setPackage(null); intent.setComponent(null); // Get ComponentName if set String pkgName; if (intent.hasExtra(EXTRA_PACKAGE_NAME)) { pkgName = intent.getStringExtra(EXTRA_PACKAGE_NAME); intent.removeExtra(EXTRA_PACKAGE_NAME); intent.setPackage(pkgName); updateTitle(pkgName); } else pkgName = null; if (intent.hasExtra(EXTRA_CLASS_NAME)) { String className; className = intent.getStringExtra(EXTRA_CLASS_NAME); intent.removeExtra(EXTRA_CLASS_NAME); if (pkgName != null && className != null) { mRequestedComponent = new ComponentName(pkgName, className); intent.setComponent(mRequestedComponent); updateSubtitle(mRequestedComponent); } } String action = intent.getStringExtra(EXTRA_ACTION); if (action != null) { intent.setAction(action); } // For shortcut/startup trigger: Need authorization to prevent abuse if (intent.getBooleanExtra(EXTRA_TRIGGER_ON_START, false) && AuthManager.getKey().equals(intent.getStringExtra(EXTRA_AUTH))) { intent.removeExtra(EXTRA_TRIGGER_ON_START); intent.removeExtra(EXTRA_AUTH); // Trigger intent launchIntent(intent, mRequestedComponent == null); // Fall-through } // Whether the Intent was edited final boolean isVisible = savedInstanceState != null && savedInstanceState.getBoolean(INTENT_EDITED); init(intent, isVisible); } private void init(@NonNull Intent intent, boolean isEdited) { // Store the Intent storeOriginalIntent(intent); // Load Intent data showInitialIntent(isEdited); // Save Intent data to history if (mHistory != null && mRequestedComponent == null) { mHistory.saveHistory(); } } private void storeOriginalIntent(@NonNull Intent intent) { // Store original intent as URI string mOriginalIntent = getUri(intent); // Get a new intent from the URI Intent copyIntent = cloneIntent(mOriginalIntent); // Store extras that are not available in the URI Bundle originalExtras = intent.getExtras(); if (copyIntent == null || originalExtras == null) { return; } Bundle additionalExtrasBundle = new Bundle(originalExtras); for (String key : originalExtras.keySet()) { if (copyIntent.hasExtra(key)) { additionalExtrasBundle.remove(key); } } if (!additionalExtrasBundle.isEmpty()) { mAdditionalExtras = additionalExtrasBundle; } } /** * Create a clone of the original Intent and display it for editing */ private void showInitialIntent(boolean isVisible) { // Copy a mutable version of the intent mMutableIntent = cloneIntent(mOriginalIntent); // Setup views setupVariables(); // Setup watchers to watch and update changes setupTextWatchers(); // Display intent data showAllIntentData(null); // Display reset button if the intent was modified showResetIntentButton(isVisible); } /** * @param textViewToIgnore The {@link TextView} which should not be updated. */ private void showAllIntentData(@Nullable TextView textViewToIgnore) { showTextViewIntentData(textViewToIgnore); // Display categories mCategoriesAdapter.setDefaultList(mMutableIntent != null ? mMutableIntent.getCategories() : null); // Display flags mFlagsAdapter.setDefaultList(getFlags()); // Display extras mExtrasAdapter.setDefaultList(getExtras()); refreshUI(); } private void updateTitle(@Nullable String packageName) { ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { if (packageName != null) { CharSequence label = mPackageLabelMap.get(packageName); if (label != null) { actionBar.setTitle(label); } else { // Need to load the label ThreadUtils.postOnBackgroundThread(() -> { CharSequence appLabel = PackageUtils.getPackageLabel(getPackageManager(), packageName, mUserHandle); ThreadUtils.postOnMainThread(() -> { if (packageName.equals(appLabel.toString())) { // Ignore labels named after their package names actionBar.setTitle(R.string.interceptor); return; } actionBar.setTitle(appLabel); mPackageLabelMap.put(packageName, appLabel); }); }); } } else actionBar.setTitle(R.string.interceptor); } } private void updateSubtitle(@Nullable ComponentName cn) { ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { if (cn == null) { actionBar.setSubtitle(null); return; } PackageManager pm = getPackageManager(); try { // Load label for the given user ActivityInfo info = pm.getActivityInfo(cn, 0); actionBar.setSubtitle(info.loadLabel(pm)); } catch (PackageManager.NameNotFoundException e) { actionBar.setSubtitle(cn.getClassName()); } } } @NonNull private SimpleArrayMap getExtras() { Bundle intentBundle; if (mMutableIntent == null || (intentBundle = mMutableIntent.getExtras()) == null) { return new SimpleArrayMap<>(0); } SimpleArrayMap extras = new SimpleArrayMap<>(); for (String extraKey : intentBundle.keySet()) { Object extraValue = intentBundle.get(extraKey); if (extraValue == null) continue; extras.put(extraKey, extraValue); } return extras; } /** * @param textViewToIgnore The {@link TextView} which should not be updated. */ private void showTextViewIntentData(@Nullable TextView textViewToIgnore) { if (mMutableIntent == null) return; // Disable text watchers temporarily to prevent triggering modifications mAreTextWatchersActive = false; try { if (textViewToIgnore != mActionView) { mActionView.setText(mMutableIntent.getAction()); } if (textViewToIgnore != mDataView && mMutableIntent.getDataString() != null) { mDataView.setText(mMutableIntent.getDataString()); } if (textViewToIgnore != mTypeView) { mTypeView.setText(mMutableIntent.getType()); } if (textViewToIgnore != mPackageNameView) { mPackageNameView.setText(mMutableIntent.getPackage()); } if (textViewToIgnore != mClassNameView) { ComponentName cn = mMutableIntent.getComponent(); mClassNameView.setText(cn != null ? cn.getClassName() : null); } if (textViewToIgnore != mUriView) { mUriView.setText(getUri(mMutableIntent)); } if (textViewToIgnore != mIdView) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { mIdView.setText(mMutableIntent.getIdentifier()); } } } finally { mAreTextWatchersActive = true; } } @NonNull private List getFlags() { if (mMutableIntent == null) { return Collections.emptyList(); } int flags = mMutableIntent.getFlags(); ArrayList flagsStrings = new ArrayList<>(); for (int i = 0; i < INTENT_FLAG_TO_STRING.size(); ++i) { if ((flags & INTENT_FLAG_TO_STRING.keyAt(i)) != 0) { flagsStrings.add(INTENT_FLAG_TO_STRING.valueAt(i)); } } return flagsStrings; } private void checkAndShowMatchingActivities() { if (mMutableIntent == null) return; List resolveInfo = getMatchingActivities(); if (resolveInfo.isEmpty()) { mResendIntentButton.setEnabled(false); mActivitiesHeader.setVisibility(View.GONE); } else { mResendIntentButton.setEnabled(true); mActivitiesHeader.setVisibility(View.VISIBLE); } mActivitiesHeader.setText(getString(R.string.matching_activities)); mMatchingActivitiesAdapter.setDefaultList(resolveInfo); } @NonNull private List getMatchingActivities() { if (mMutableIntent == null) { return Collections.emptyList(); } if (mUseRoot || SelfPermissions.checkCrossUserPermission(mUserHandle, false)) { try { return PackageManagerCompat.queryIntentActivities(this, mMutableIntent, PackageManager.MATCH_ALL, mUserHandle); } catch (RemoteException e) { e.printStackTrace(); } } return getPackageManager().queryIntentActivities(mMutableIntent, 0); } private void setupVariables() { mActionView = findViewById(R.id.action_edit); mDataView = findViewById(R.id.data_edit); mTypeView = findViewById(R.id.type_edit); mUriView = findViewById(R.id.uri_edit); mPackageNameView = findViewById(R.id.package_edit); mClassNameView = findViewById(R.id.class_edit); mIdView = findViewById(R.id.type_id); mHistory = new HistoryEditText(this, mActionView, mDataView, mTypeView, mUriView, mPackageNameView, mClassNameView); // Setup user ID edit mUserIdEdit = findViewById(R.id.user_id_edit); mUserIdEdit.setText(String.valueOf(mUserHandle)); mUserIdEdit.setEnabled(SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.INTERACT_ACROSS_USERS) || SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.INTERACT_ACROSS_USERS_FULL)); // Setup root MaterialCheckBox useRootCheckBox = findViewById(R.id.use_root); useRootCheckBox.setChecked(mUseRoot); useRootCheckBox.setVisibility(Ops.isWorkingUidRoot() ? View.VISIBLE : View.GONE); useRootCheckBox.setOnCheckedChangeListener((buttonView, isChecked) -> { if (mUseRoot != isChecked) { mUseRoot = isChecked; refreshUI(); } }); // Setup identifier TextInputLayout idLayout = findViewById(R.id.type_id_layout); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { idLayout.setEndIconOnClickListener(v -> { mIdView.setText(UUID.randomUUID().toString()); mIdView.requestFocus(); }); } else idLayout.setVisibility(View.GONE); // Setup categories MaterialButton addCategoriesButton = findViewById(R.id.intent_categories_add_btn); addCategoriesButton.setOnClickListener(v -> { UiUtils.fixFocus(addCategoriesButton); new TextInputDropdownDialogBuilder(this, R.string.category) .setTitle(R.string.category) .setDropdownItems(INTENT_CATEGORIES, -1, true) .setNegativeButton(R.string.cancel, null) .setPositiveButton(R.string.ok, (dialog, which, inputText, isChecked) -> { if (!TextUtils.isEmpty(inputText)) { //noinspection ConstantConditions mMutableIntent.addCategory(inputText.toString().trim()); mCategoriesAdapter.setDefaultList(mMutableIntent.getCategories()); showTextViewIntentData(null); showResetIntentButton(true); } }) .show(); }); RecyclerView categoriesRecyclerView = findViewById(R.id.intent_categories); categoriesRecyclerView.setLayoutManager(UIUtils.getGridLayoutAt450Dp(this)); mCategoriesAdapter = new CategoriesRecyclerViewAdapter(this); categoriesRecyclerView.setAdapter(mCategoriesAdapter); // Setup flags MaterialButton addFlagsButton = findViewById(R.id.intent_flags_add_btn); addFlagsButton.setOnClickListener(v -> { UiUtils.fixFocus(addFlagsButton); new TextInputDropdownDialogBuilder(this, R.string.flags) .setTitle(R.string.flags) .setDropdownItems(getAllFlags(), -1, true) .setNegativeButton(R.string.cancel, null) .setPositiveButton(R.string.ok, (dialog, which, inputText, isChecked) -> { if (!TextUtils.isEmpty(inputText) && mMutableIntent != null) { int i = getFlagIndex(String.valueOf(inputText).trim()); if (i >= 0) { mMutableIntent.addFlags(INTENT_FLAG_TO_STRING.keyAt(i)); } else { try { int flag = IntegerCompat.decode(String.valueOf(inputText).trim()); mMutableIntent.addFlags(flag); } catch (NumberFormatException e) { return; } } mFlagsAdapter.setDefaultList(getFlags()); showTextViewIntentData(null); showResetIntentButton(true); } }) .show(); }); RecyclerView flagsRecyclerView = findViewById(R.id.intent_flags); flagsRecyclerView.setLayoutManager(UIUtils.getGridLayoutAt450Dp(this)); mFlagsAdapter = new FlagsRecyclerViewAdapter(this); flagsRecyclerView.setAdapter(mFlagsAdapter); // Setup extras MaterialButton addExtrasButton = findViewById(R.id.intent_extras_add_btn); addExtrasButton.setOnClickListener(v -> { UiUtils.fixFocus(addExtrasButton); AddIntentExtraFragment fragment = new AddIntentExtraFragment(); fragment.setOnSaveListener((mode, prefItem) -> { if (mMutableIntent != null) { IntentCompat.addToIntent(mMutableIntent, prefItem); mExtrasAdapter.setDefaultList(getExtras()); showResetIntentButton(true); } }); Bundle args = new Bundle(); args.putInt(AddIntentExtraFragment.ARG_MODE, AddIntentExtraFragment.MODE_CREATE); fragment.setArguments(args); fragment.show(getSupportFragmentManager(), AddIntentExtraFragment.TAG); }); RecyclerView extrasRecyclerView = findViewById(R.id.intent_extras); extrasRecyclerView.setLayoutManager(UIUtils.getGridLayoutAt450Dp(this)); mExtrasAdapter = new ExtrasRecyclerViewAdapter(this); extrasRecyclerView.setAdapter(mExtrasAdapter); // Setup matching activities mActivitiesHeader = findViewById(R.id.intent_matching_activities_header); if (mRequestedComponent != null) { // Hide matching activities since specific component requested mActivitiesHeader.setVisibility(View.GONE); } RecyclerView matchingActivitiesRecyclerView = findViewById(R.id.intent_matching_activities); matchingActivitiesRecyclerView.setLayoutManager(UIUtils.getGridLayoutAt450Dp(this)); mMatchingActivitiesAdapter = new MatchingActivitiesRecyclerViewAdapter(this); matchingActivitiesRecyclerView.setAdapter(mMatchingActivitiesAdapter); mResendIntentButton = findViewById(R.id.resend_intent_button); mResetIntentButton = findViewById(R.id.reset_intent_button); // Send Intent on clicking the resend intent button mResendIntentButton.setOnClickListener(v -> { UiUtils.fixFocus(mResendIntentButton); if (mMutableIntent == null) return; launchIntent(mMutableIntent, mRequestedComponent == null); }); // Reset Intent data on clicking the reset intent button mResetIntentButton.setOnClickListener(v -> { UiUtils.fixFocus(mResetIntentButton); mAreTextWatchersActive = false; showInitialIntent(false); mAreTextWatchersActive = true; refreshUI(); }); } private void setupTextWatchers() { mActionView.addTextChangedListener(new IntentUpdateTextWatcher(mActionView) { @Override protected void onUpdateIntent(String modifiedContent) { if (mMutableIntent != null) { mMutableIntent.setAction(modifiedContent); } } }); mDataView.addTextChangedListener(new IntentUpdateTextWatcher(mDataView) { @Override protected void onUpdateIntent(String modifiedContent) { if (mMutableIntent != null) { // setDataAndType clears type so we save it String savedType = mMutableIntent.getType(); mMutableIntent.setDataAndType(Uri.parse(modifiedContent), savedType); } } }); mTypeView.addTextChangedListener(new IntentUpdateTextWatcher(mTypeView) { @Override protected void onUpdateIntent(String modifiedContent) { if (mMutableIntent != null) { // setDataAndType clears data so we save it String dataString = mMutableIntent.getDataString(); mMutableIntent.setDataAndType(Uri.parse(dataString), modifiedContent); } } }); mPackageNameView.addTextChangedListener(new IntentUpdateTextWatcher(mPackageNameView) { @Override protected void onUpdateIntent(String modifiedContent) { if (mMutableIntent != null) { mMutableIntent.setPackage(TextUtils.isEmpty(modifiedContent) ? null : modifiedContent); } } }); mClassNameView.addTextChangedListener(new IntentUpdateTextWatcher(mClassNameView) { @Override protected void onUpdateIntent(String modifiedComponent) { if (mMutableIntent == null) return; if (TextUtils.isEmpty(modifiedComponent)) { mRequestedComponent = null; mMutableIntent.setComponent(null); return; } String packageName = mMutableIntent.getPackage(); if (packageName == null) { UIUtils.displayShortToast(R.string.set_package_name_first); mAreTextWatchersActive = false; mClassNameView.setText(null); mAreTextWatchersActive = true; return; } mRequestedComponent = new ComponentName(packageName, (modifiedComponent.startsWith(".") ? packageName : "") + modifiedComponent); mMutableIntent.setComponent(mRequestedComponent); } }); mUriView.addTextChangedListener(new IntentUpdateTextWatcher(mUriView) { @Override protected void onUpdateIntent(String modifiedContent) { // no error yet so continue mMutableIntent = cloneIntent(modifiedContent); // this time must update all content since extras/flags may have been changed showAllIntentData(mUriView); } }); mIdView.addTextChangedListener(new IntentUpdateTextWatcher(mIdView) { @Override protected void onUpdateIntent(String modifiedContent) { if (mMutableIntent != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { mMutableIntent.setIdentifier(modifiedContent); } } }); mUserIdEdit.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { if (s == null) return; try { mUserHandle = Integer.decode(s.toString()); refreshUI(); } catch (NumberFormatException ignore) { } } @Override public void afterTextChanged(Editable s) { } }); } private void showResetIntentButton(boolean visible) { mResendIntentButton.setText(R.string.send_edited_intent); mResetIntentButton.setVisibility((visible) ? View.VISIBLE : View.GONE); } private void copyIntentDetails() { Utils.copyToClipboard(this, "Intent Details", getIntentDetailsString()); } private void copyIntentAsCommand() { if (mMutableIntent == null) { return; } List args = IntentCompat.flattenToCommand(mMutableIntent); String command = String.format(Locale.ROOT, "%s start --user %d %s", RunnerUtils.CMD_AM, mUserHandle, TextUtils.join(" ", args)); Utils.copyToClipboard(this, "am command", command); } private void pasteIntentDetails() { CharSequence clipData = ClipboardUtils.readClipboard(this); if (clipData == null) return; String text = clipData.toString(); // Get ROOT and USER since they aren't part of IntentCompat.unflattenFromString() String[] lines = text.split("\n"); mUseRoot = false; mUserHandle = UserHandleHidden.myUserId(); int parseCount = 0; for (String line : lines) { if (TextUtils.isEmpty(line)) continue; StringTokenizer tokenizer = new StringTokenizer(line, "\t"); switch (tokenizer.nextToken()) { case "ROOT": mUseRoot = Ops.isWorkingUidRoot() && Boolean.parseBoolean(tokenizer.nextToken()); ++parseCount; break; case "USER": int userId = Integer.decode(tokenizer.nextToken()); if (SelfPermissions.checkCrossUserPermission(userId, false)) { mUserHandle = userId; } ++parseCount; break; } if (parseCount == 2) { // Got both ROOT and USER, no need to continue the loop break; } } // Rebuild Intent Intent intent = IntentCompat.unflattenFromString(text); if (intent != null) { // Requested component set to NULL in case it was set previously mRequestedComponent = null; init(intent, false); } } private void refreshUI() { if (mMutableIntent == null) return; if (mRequestedComponent == null) { // Since no explicit component requested, display matching activities checkAndShowMatchingActivities(); } else { // Hide matching activities since specific component requested mActivitiesHeader.setVisibility(View.GONE); mResendIntentButton.setEnabled(true); } updateTitle(mMutableIntent.getPackage()); updateSubtitle(mMutableIntent.getComponent()); } @NonNull private Intent createShareIntent() { Intent share = new Intent(Intent.ACTION_SEND); share.setType("text/plain"); share.putExtra(Intent.EXTRA_TEXT, getIntentDetailsString()); return share; } @NonNull private String getIntentDetailsString() { if (mMutableIntent == null) { return ""; } PackageManager pm = getPackageManager(); List resolveInfo = getMatchingActivities(); int numberOfMatchingActivities = resolveInfo.size(); StringBuilder result = new StringBuilder(); // NOTE: At least 1 tab have to be present in each non-empty line. Empty lines are ignored. // URI (unused) result.append("URI\t").append(getUri(mMutableIntent)).append("\n"); // ROOT if (mUseRoot) result.append("ROOT\t").append(mUseRoot).append("\n"); // USER if (mUserHandle != UserHandleHidden.myUserId()) { result.append("USER\t").append(mUserHandle).append("\n"); } result.append("\n"); // Convert the Intent to parsable string result.append(IntentCompat.flattenToString(mMutableIntent)).append("\n"); // MATCHING ACTIVITIES result.append("MATCHING ACTIVITIES\t").append(numberOfMatchingActivities).append("\n"); // Calculate the number of spaces needed in order to align activity items properly int spaceCount = String.valueOf(numberOfMatchingActivities).length(); StringBuilder spaces = new StringBuilder(); while ((spaceCount--) != 0) spaces.append(" "); for (int i = 0; i < numberOfMatchingActivities; ++i) { ActivityInfo activityinfo = resolveInfo.get(i).activityInfo; // Line 1: LABEL // Line 2: NAME // Line 3: PACKAGE result.append(i).append("\tLABEL \t").append(activityinfo.loadLabel(pm)).append("\n"); result.append(spaces).append("\tNAME \t").append(activityinfo.name).append("\n"); result.append(spaces).append("\tPACKAGE\t").append(activityinfo.packageName).append("\n"); } // Add activity results if (mLastResultCode != null) { result.append("\n"); // ACTIVITY RESULT result.append("ACTIVITY RESULT\t").append(mLastResultCode).append("\n"); if (mLastResultIntent != null) { // Print the last result intent with RESULT prefix so that it will not be parsed by the parser result.append(IntentCompat.describeIntent(mLastResultIntent, "RESULT")); } } return result.toString(); } public void launchIntent(@NonNull Intent intent, boolean createChooser) { boolean needPrivilege = mUseRoot || mUserHandle != UserHandleHidden.myUserId(); try { if (createChooser) { Intent chooserIntent = Intent.createChooser(intent, mResendIntentButton != null ? mResendIntentButton.getText() : getString(R.string.open)); if (needPrivilege) { // TODO: 4/2/22 Support sending activity result back to the original app ActivityManagerCompat.startActivity(chooserIntent, mUserHandle); } else { mIntentLauncher.launch(chooserIntent); } } else { // Launch a fixed component if (needPrivilege) { // TODO: 4/2/22 Support sending activity result back to the original app ActivityManagerCompat.startActivity(intent, mUserHandle); } else { try { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); mIntentLauncher.launch(intent); } catch (SecurityException e) { // TODO: 4/6/24 Support sending activity result back to the original app ActivityManagerCompat.startActivity(intent, mUserHandle); } } } } catch (Throwable th) { Log.e(TAG, th); UIUtils.displayLongToast(R.string.error_with_details, th.getClass().getName() + ": " + th.getMessage()); } } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.activity_activity_interceptor_actions, menu); return super.onCreateOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int id = item.getItemId(); if (id == android.R.id.home) { finish(); return true; } else if (id == R.id.action_copy_as_default) { copyIntentDetails(); return true; } else if (id == R.id.action_copy_as_command) { copyIntentAsCommand(); return true; } else if (id == R.id.action_paste) { pasteIntentDetails(); return true; } else if (id == R.id.action_shortcut) { try { ActionBar actionBar = getSupportActionBar(); CharSequence shortcutName = null; if (actionBar != null) { shortcutName = actionBar.getSubtitle(); } if (shortcutName == null) { shortcutName = Objects.requireNonNull(getTitle()); } Drawable icon = Objects.requireNonNull(ContextCompat.getDrawable(this, R.drawable.ic_launcher_foreground)); Intent intent = new Intent(mMutableIntent); // Add necessary extras intent.putExtra(EXTRA_AUTH, AuthManager.getKey()); intent.putExtra(EXTRA_TRIGGER_ON_START, true); intent.putExtra(EXTRA_ACTION, intent.getAction()); if (mUseRoot) { intent.putExtra(EXTRA_ROOT, true); } if (mUserHandle != UserHandleHidden.myUserId()) { intent.putExtra(EXTRA_USER_HANDLE, mUserHandle); } if (mRequestedComponent != null) { intent.putExtra(EXTRA_PACKAGE_NAME, mRequestedComponent.getPackageName()); intent.putExtra(EXTRA_CLASS_NAME, mRequestedComponent.getClassName()); } intent.setClass(getApplicationContext(), ActivityInterceptor.class); InterceptorShortcutInfo shortcutInfo = new InterceptorShortcutInfo(intent); shortcutInfo.setName(shortcutName); shortcutInfo.setIcon(UIUtils.getBitmapFromDrawable(icon)); CreateShortcutDialogFragment dialog = CreateShortcutDialogFragment.getInstance(shortcutInfo); dialog.show(getSupportFragmentManager(), CreateShortcutDialogFragment.TAG); } catch (Throwable th) { Log.e(TAG, th); UIUtils.displayLongToast(R.string.error_with_details, th.getClass().getName() + ": " + th.getMessage()); } return true; } return super.onOptionsItemSelected(item); } @Override protected void onPause() { super.onPause(); mAreTextWatchersActive = false; } @Override protected void onResume() { super.onResume(); // Inhibit new activity animation when resetting intent details overridePendingTransition(0, 0); mAreTextWatchersActive = true; } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); if (mResetIntentButton != null) { outState.putBoolean(INTENT_EDITED, mResetIntentButton.getVisibility() == View.VISIBLE); } if (mHistory != null) { mHistory.saveHistory(); } } @Nullable private static String getUri(@Nullable Intent src) { try { return (src != null) ? IntentCompat.toUri(src, Intent.URI_INTENT_SCHEME) : null; } catch (BadParcelableException e) { // TODO: 4/2/22 Add support for invalid classes. This could be done in the following way: // 1. Upon detecting a BPE (and the class name), ask the user to select the source application // 2. Load the source application via the DexClassLoader // 3. Use Class.forName() to load the class and it's class loader to recognize the Parcelable // The other option is to skip the problematic classes. e.printStackTrace(); return null; } } @Nullable private Intent cloneIntent(@Nullable String intentUri) { if (intentUri == null) return null; try { Intent clone; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { clone = Intent.parseUri(intentUri, Intent.URI_INTENT_SCHEME | Intent.URI_ANDROID_APP_SCHEME | Intent.URI_ALLOW_UNSAFE); } else clone = Intent.parseUri(intentUri, Intent.URI_INTENT_SCHEME); // Restore extras that are lost in the intent to string conversion if (mAdditionalExtras != null) { clone.putExtras(mAdditionalExtras); } return clone; } catch (URISyntaxException e) { e.printStackTrace(); } return null; } @NonNull private List getAllFlags() { List allFlags = new ArrayList<>(); for (int i = 0; i < INTENT_FLAG_TO_STRING.size(); ++i) { allFlags.add(INTENT_FLAG_TO_STRING.valueAt(i)); } return allFlags; } private int getFlagIndex(String flagStr) { for (int i = 0; i < INTENT_FLAG_TO_STRING.size(); ++i) { if (INTENT_FLAG_TO_STRING.valueAt(i).equals(flagStr)) return i; } return -1; } private static class CategoriesRecyclerViewAdapter extends RecyclerView.Adapter { private final List mCategories = new ArrayList<>(); private final ActivityInterceptor mActivity; public CategoriesRecyclerViewAdapter(ActivityInterceptor activity) { mActivity = activity; } public void setDefaultList(@Nullable Collection categories) { AdapterUtils.notifyDataSetChanged(this, mCategories, categories != null ? new ArrayList<>(categories) : null); } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_title_action, parent, false); return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { String category = mCategories.get(position); holder.title.setText(category); holder.title.setTextIsSelectable(true); holder.actionIcon.setOnClickListener(v -> { UiUtils.fixFocus(holder.actionIcon); if (mActivity.mMutableIntent != null) { mActivity.mMutableIntent.removeCategory(category); setDefaultList(mActivity.mMutableIntent.getCategories()); mActivity.showTextViewIntentData(null); mActivity.showResetIntentButton(true); } }); } @Override public int getItemCount() { return mCategories.size(); } static class ViewHolder extends RecyclerView.ViewHolder { TextView title; MaterialButton actionIcon; public ViewHolder(@NonNull View itemView) { super(itemView); title = itemView.findViewById(R.id.item_title); actionIcon = itemView.findViewById(R.id.item_action); actionIcon.setContentDescription(itemView.getContext().getString(R.string.item_remove)); } } } private static class FlagsRecyclerViewAdapter extends RecyclerView.Adapter { private final List mFlags = new ArrayList<>(); private final ActivityInterceptor mActivity; public FlagsRecyclerViewAdapter(ActivityInterceptor activity) { mActivity = activity; } public void setDefaultList(@Nullable List flags) { AdapterUtils.notifyDataSetChanged(this, mFlags, flags); } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_title_action, parent, false); return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { String flagName = mFlags.get(position); holder.title.setText(flagName); holder.title.setTextIsSelectable(true); holder.actionIcon.setOnClickListener(v -> { UiUtils.fixFocus(holder.actionIcon); int i = INTENT_FLAG_TO_STRING.indexOfValue(flagName); if (i >= 0 && mActivity.mMutableIntent != null) { IntentCompat.removeFlags(mActivity.mMutableIntent, INTENT_FLAG_TO_STRING.keyAt(i)); setDefaultList(mActivity.getFlags()); mActivity.showTextViewIntentData(null); mActivity.showResetIntentButton(true); } }); } @Override public int getItemCount() { return mFlags.size(); } static class ViewHolder extends RecyclerView.ViewHolder { TextView title; MaterialButton actionIcon; public ViewHolder(@NonNull View itemView) { super(itemView); title = itemView.findViewById(R.id.item_title); actionIcon = itemView.findViewById(R.id.item_action); actionIcon.setContentDescription(itemView.getContext().getString(R.string.item_remove)); } } } private static class ExtrasRecyclerViewAdapter extends RecyclerView.Adapter { private final SimpleArrayMap mExtras = new SimpleArrayMap<>(0); private final ActivityInterceptor mActivity; public ExtrasRecyclerViewAdapter(ActivityInterceptor activity) { mActivity = activity; } public void setDefaultList(@Nullable SimpleArrayMap extras) { AdapterUtils.notifyDataSetChanged(this, mExtras, extras); } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_icon_title_subtitle, parent, false); return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { String key = mExtras.keyAt(position); Object value = mExtras.valueAt(position); holder.title.setText(key); holder.title.setTextIsSelectable(true); holder.subtitle.setText(value.toString()); holder.subtitle.setTextIsSelectable(true); holder.actionIcon.setOnClickListener(v -> { UiUtils.fixFocus(holder.actionIcon); if (mActivity.mMutableIntent != null) { mActivity.mMutableIntent.removeExtra(key); mActivity.showTextViewIntentData(null); int pos = mExtras.indexOfKey(key); if (pos >= 0) { mExtras.removeAt(pos); notifyItemRemoved(pos); } mActivity.showResetIntentButton(true); } }); } @Override public int getItemCount() { return mExtras.size(); } static class ViewHolder extends RecyclerView.ViewHolder { TextView title; TextView subtitle; ImageView icon; MaterialButton actionIcon; public ViewHolder(@NonNull View itemView) { super(itemView); title = itemView.findViewById(R.id.item_title); subtitle = itemView.findViewById(R.id.item_subtitle); actionIcon = itemView.findViewById(R.id.item_open); actionIcon.setIconResource(R.drawable.ic_trash_can); actionIcon.setContentDescription(itemView.getContext().getString(R.string.item_remove)); icon = itemView.findViewById(R.id.item_icon); icon.setVisibility(View.GONE); } } } private static class MatchingActivitiesRecyclerViewAdapter extends RecyclerView.Adapter { private final List mMatchingActivities = new ArrayList<>(); private final PackageManager mPm; private final ActivityInterceptor mActivity; public MatchingActivitiesRecyclerViewAdapter(ActivityInterceptor activity) { mActivity = activity; mPm = activity.getPackageManager(); } public void setDefaultList(@Nullable List matchingActivities) { AdapterUtils.notifyDataSetChanged(this, mMatchingActivities, matchingActivities); } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_icon_title_subtitle, parent, false); return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { ResolveInfo resolveInfo = mMatchingActivities.get(position); ActivityInfo info = resolveInfo.activityInfo; holder.title.setText(info.loadLabel(mPm)); String activityName = info.name; String name = info.packageName + "\n" + activityName; holder.subtitle.setText(name); holder.subtitle.setTextIsSelectable(true); String tag = info.packageName + "_" + activityName; holder.icon.setTag(tag); ImageLoader.getInstance().displayImage(tag, info, holder.icon); holder.actionIcon.setOnClickListener(v -> { UiUtils.fixFocus(holder.actionIcon); Intent intent = new Intent(mActivity.mMutableIntent); intent.setClassName(info.packageName, activityName); IntentCompat.removeFlags(intent, Intent.FLAG_ACTIVITY_FORWARD_RESULT); mActivity.launchIntent(intent, false); }); } @Override public int getItemCount() { return mMatchingActivities.size(); } static class ViewHolder extends RecyclerView.ViewHolder { TextView title; TextView subtitle; ImageView icon; MaterialButton actionIcon; public ViewHolder(@NonNull View itemView) { super(itemView); title = itemView.findViewById(R.id.item_title); subtitle = itemView.findViewById(R.id.item_subtitle); actionIcon = itemView.findViewById(R.id.item_open); actionIcon.setContentDescription(itemView.getContext().getString(R.string.open)); icon = itemView.findViewById(R.id.item_icon); } } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/intercept/AddIntentExtraFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.intercept; import static io.github.muntashirakon.AppManager.intercept.IntentCompat.parseExtraValue; import android.app.Dialog; import android.os.Bundle; import android.text.TextUtils; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.TextView; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.FragmentActivity; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.materialswitch.MaterialSwitch; import com.google.android.material.textfield.TextInputEditText; import com.google.android.material.textfield.TextInputLayout; import java.io.Serializable; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.adapters.SelectedArrayAdapter; import io.github.muntashirakon.widget.MaterialSpinner; public class AddIntentExtraFragment extends DialogFragment { public static final String TAG = "AddIntentExtraFragment"; public static final String ARG_PREF_ITEM = "ARG_PREF_ITEM"; public static final String ARG_MODE = "ARG_MODE"; @IntDef(value = { MODE_EDIT, MODE_CREATE, MODE_DELETE }) public @interface Mode { } public static final int MODE_EDIT = 1; // Key name is disabled public static final int MODE_CREATE = 2; // Key name is not disabled public static final int MODE_DELETE = 3; @IntDef(value = { TYPE_BOOLEAN, TYPE_COMPONENT_NAME, TYPE_FLOAT, TYPE_FLOAT_ARR, TYPE_FLOAT_AL, TYPE_INTEGER, TYPE_INT_ARR, TYPE_INT_AL, TYPE_LONG, TYPE_LONG_ARR, TYPE_LONG_AL, TYPE_NULL, TYPE_STRING, TYPE_STRING_ARR, TYPE_STRING_AL, TYPE_URI, TYPE_URI_ARR, TYPE_URI_AL, }) public @interface Type { } public static final int TYPE_BOOLEAN = 0; public static final int TYPE_COMPONENT_NAME = 1; public static final int TYPE_FLOAT = 2; public static final int TYPE_FLOAT_ARR = 3; public static final int TYPE_FLOAT_AL = 4; public static final int TYPE_INTEGER = 5; public static final int TYPE_INT_ARR = 6; public static final int TYPE_INT_AL = 7; public static final int TYPE_LONG = 8; public static final int TYPE_LONG_ARR = 9; public static final int TYPE_LONG_AL = 10; public static final int TYPE_NULL = 11; public static final int TYPE_STRING = 12; public static final int TYPE_STRING_ARR = 13; public static final int TYPE_STRING_AL = 14; public static final int TYPE_URI = 15; public static final int TYPE_URI_ARR = 16; public static final int TYPE_URI_AL = 17; private static final int TYPE_COUNT = 18; @Nullable private OnSaveListener mOnSaveListener; public interface OnSaveListener { void onSave(@Mode int mode, ExtraItem extraItem); } public static class ExtraItem implements Serializable { private static final long serialVersionUID = 4815162342L; @Type public int type; public String keyName; @Nullable public Object keyValue; public ExtraItem() { } @Override @NonNull public String toString() { return "PrefItem{" + "type=" + type + ", keyName='" + keyName + '\'' + ", keyValue=" + keyValue + '}'; } } private final ViewGroup[] mLayoutTypes = new ViewGroup[TYPE_COUNT]; private final TextView[] mValues = new TextView[TYPE_COUNT]; @Type private int mCurrentType; public void setOnSaveListener(@Nullable OnSaveListener onSaveListener) { mOnSaveListener = onSaveListener; } @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { FragmentActivity activity = requireActivity(); Bundle args = requireArguments(); ExtraItem extraItem = (ExtraItem) args.getSerializable(ARG_PREF_ITEM); @Mode int mode = args.getInt(ARG_MODE, MODE_CREATE); View view = View.inflate(activity, R.layout.dialog_edit_pref_item, null); MaterialSpinner spinner = view.findViewById(R.id.type_selector_spinner); ArrayAdapter spinnerAdapter = SelectedArrayAdapter.createFromResource(activity, R.array.extras_types, io.github.muntashirakon.ui.R.layout.auto_complete_dropdown_item); spinner.setAdapter(spinnerAdapter); spinner.setOnItemClickListener((parent, view1, position, id) -> { for (ViewGroup layout : mLayoutTypes) layout.setVisibility(View.GONE); if (position != TYPE_NULL) { // We don't need a value for null ViewGroup viewGroup = mLayoutTypes[position]; viewGroup.setVisibility(View.VISIBLE); if (viewGroup instanceof TextInputLayout) { ((TextInputLayout) viewGroup).setHint(spinnerAdapter.getItem(position)); } } mCurrentType = position; }); // Set layouts mLayoutTypes[TYPE_BOOLEAN] = view.findViewById(R.id.layout_bool); mLayoutTypes[TYPE_COMPONENT_NAME] = view.findViewById(R.id.layout_string); mLayoutTypes[TYPE_FLOAT] = view.findViewById(R.id.layout_float); mLayoutTypes[TYPE_FLOAT_ARR] = view.findViewById(R.id.layout_string); mLayoutTypes[TYPE_FLOAT_AL] = view.findViewById(R.id.layout_string); mLayoutTypes[TYPE_INTEGER] = view.findViewById(R.id.layout_int); mLayoutTypes[TYPE_INT_ARR] = view.findViewById(R.id.layout_string); mLayoutTypes[TYPE_INT_AL] = view.findViewById(R.id.layout_string); mLayoutTypes[TYPE_LONG] = view.findViewById(R.id.layout_long); mLayoutTypes[TYPE_LONG_ARR] = view.findViewById(R.id.layout_string); mLayoutTypes[TYPE_LONG_AL] = view.findViewById(R.id.layout_string); mLayoutTypes[TYPE_NULL] = view.findViewById(R.id.layout_string); mLayoutTypes[TYPE_STRING] = view.findViewById(R.id.layout_string); mLayoutTypes[TYPE_STRING_ARR] = view.findViewById(R.id.layout_string); mLayoutTypes[TYPE_STRING_AL] = view.findViewById(R.id.layout_string); mLayoutTypes[TYPE_URI] = view.findViewById(R.id.layout_string); mLayoutTypes[TYPE_URI_ARR] = view.findViewById(R.id.layout_string); mLayoutTypes[TYPE_URI_AL] = view.findViewById(R.id.layout_string); // Set views mValues[TYPE_BOOLEAN] = view.findViewById(R.id.input_bool); mValues[TYPE_COMPONENT_NAME] = view.findViewById(R.id.input_string); mValues[TYPE_FLOAT] = view.findViewById(R.id.input_float); mValues[TYPE_FLOAT_ARR] = view.findViewById(R.id.input_string); mValues[TYPE_FLOAT_AL] = view.findViewById(R.id.input_string); mValues[TYPE_INTEGER] = view.findViewById(R.id.input_int); mValues[TYPE_INT_ARR] = view.findViewById(R.id.input_string); mValues[TYPE_INT_AL] = view.findViewById(R.id.input_string); mValues[TYPE_LONG] = view.findViewById(R.id.input_long); mValues[TYPE_LONG_ARR] = view.findViewById(R.id.input_string); mValues[TYPE_LONG_AL] = view.findViewById(R.id.input_string); mValues[TYPE_NULL] = view.findViewById(R.id.input_string); mValues[TYPE_STRING] = view.findViewById(R.id.input_string); mValues[TYPE_STRING_ARR] = view.findViewById(R.id.input_string); mValues[TYPE_STRING_AL] = view.findViewById(R.id.input_string); mValues[TYPE_URI] = view.findViewById(R.id.input_string); mValues[TYPE_URI_ARR] = view.findViewById(R.id.input_string); mValues[TYPE_URI_AL] = view.findViewById(R.id.input_string); // Key name TextInputEditText editKeyName = view.findViewById(R.id.key_name); if (extraItem != null) { // Extra is already set mCurrentType = extraItem.type; String keyName = extraItem.keyName; Object keyValue = extraItem.keyValue; editKeyName.setText(keyName); if (mode == MODE_EDIT) editKeyName.setEnabled(false); for (ViewGroup layout : mLayoutTypes) layout.setVisibility(View.GONE); if (mCurrentType != TYPE_NULL) { // We don't need a value for null ViewGroup viewGroup = mLayoutTypes[mCurrentType]; viewGroup.setVisibility(View.VISIBLE); if (viewGroup instanceof TextInputLayout) { ((TextInputLayout) viewGroup).setHint(spinnerAdapter.getItem(mCurrentType)); } if (keyValue != null) { // FIXME: 25/1/21 Reformat the string to support parsing TextView tv = mValues[TYPE_FLOAT]; if (tv instanceof MaterialSwitch) { ((MaterialSwitch) tv).setChecked((boolean) keyValue); } else tv.setText(keyValue.toString()); } } } MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(activity); builder.setView(view) .setPositiveButton(mode == MODE_CREATE ? R.string.add : R.string.done, (dialog, which) -> { if (mOnSaveListener == null) return; if (editKeyName.getText() == null) { UIUtils.displayLongToast(R.string.key_name_cannot_be_null); return; } String keyName = editKeyName.getText().toString().trim(); ExtraItem newExtraItem; if (extraItem != null) newExtraItem = extraItem; else { newExtraItem = new ExtraItem(); newExtraItem.keyName = keyName; } newExtraItem.type = mCurrentType; if (TextUtils.isEmpty(newExtraItem.keyName)) { UIUtils.displayLongToast(R.string.key_name_cannot_be_null); return; } try { if (mCurrentType == TYPE_BOOLEAN) { newExtraItem.keyValue = ((MaterialSwitch) mValues[mCurrentType]).isChecked(); } else { newExtraItem.keyValue = parseExtraValue(mCurrentType, mValues[mCurrentType].getText().toString().trim()); } } catch (Exception e) { e.printStackTrace(); UIUtils.displayLongToast(R.string.error_evaluating_input); return; } mOnSaveListener.onSave(mode, newExtraItem); }) .setNegativeButton(R.string.cancel, (dialog, which) -> { if (getDialog() != null) getDialog().cancel(); }); if (mode == MODE_EDIT) { builder.setNeutralButton(R.string.delete, (dialog, which) -> { if (mOnSaveListener != null) mOnSaveListener.onSave(MODE_DELETE, extraItem); }); } return builder.create(); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/intercept/HistoryEditText.java ================================================ // SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.intercept; import android.app.Activity; import android.content.SharedPreferences; import android.widget.ArrayAdapter; import androidx.annotation.NonNull; import androidx.preference.PreferenceManager; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import io.github.muntashirakon.adapters.NoFilterArrayAdapter; import io.github.muntashirakon.widget.MaterialAutoCompleteTextView; public class HistoryEditText { private static final String DELIMITER = "';'"; private static final int MAX_HISTORY_SIZE = 8; private static final String HISTORY_PREFIX = "ActivityInterceptor_history_"; private final Activity mContext; private final EditorHandler[] mEditorHandlers; /** * ContextActionBar for one EditText */ protected class EditorHandler { private final MaterialAutoCompleteTextView mEditor; private final String mId; public EditorHandler(String id, MaterialAutoCompleteTextView editor) { mId = id; mEditor = editor; showHistory(); } public String toString(SharedPreferences pref) { return mId + " : '" + getHistory(pref) + "'"; } protected void showHistory() { List items = getHistoryItems(); ArrayAdapter adapter = new NoFilterArrayAdapter<>(mContext, io.github.muntashirakon.ui.R.layout.auto_complete_dropdown_item, items); mEditor.setAdapter(adapter); } @NonNull private List getHistoryItems() { SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(mContext); return getHistory(sharedPref); } @NonNull private List getHistory(@NonNull SharedPreferences sharedPref) { String history = sharedPref.getString(mId, ""); return asList(history == null ? "" : history); } protected void saveHistory(@NonNull SharedPreferences sharedPref, @NonNull SharedPreferences.Editor edit) { List history = getHistory(sharedPref); history = include(history, mEditor.getText().toString().trim()); String result = toString(history); edit.putString(mId, result); } @NonNull private List asList(@NonNull String serialistedListElements) { String[] items = serialistedListElements.split(DELIMITER); return Arrays.asList(items); } @NonNull private String toString(List list) { StringBuilder result = new StringBuilder(); if (list != null) { String nextDelim = ""; for (Object instance : list) { if (instance != null) { String instanceString = instance.toString().trim(); if (instanceString.length() > 0) { result.append(nextDelim).append(instanceString); nextDelim = DELIMITER; } } } } return result.toString(); } @NonNull private List include(List history_, String newValue) { List history = new ArrayList<>(history_); if ((newValue != null) && (newValue.length() > 0)) { history.remove(newValue); history.add(0, newValue); } int len = history.size(); // forget oldest entries if maxHisotrySize is reached while (len > MAX_HISTORY_SIZE) { len--; history.remove(len); } return history; } } /** * define history function for these editors */ public HistoryEditText(@NonNull Activity context, @NonNull MaterialAutoCompleteTextView... editors) { mContext = context; mEditorHandlers = new EditorHandler[editors.length]; for (int i = 0; i < editors.length; i++) { mEditorHandlers[i] = createHandler(HISTORY_PREFIX + i, editors[i]); } } protected EditorHandler createHandler(String id, MaterialAutoCompleteTextView editor) { return new EditorHandler(id, editor); } /** * include current editor-content to history and save to settings */ public void saveHistory() { SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(mContext); SharedPreferences.Editor edit = sharedPref.edit(); for (EditorHandler instance : mEditorHandlers) { instance.saveHistory(sharedPref, edit); } edit.apply(); } @NonNull @Override public String toString() { SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(mContext); StringBuilder result = new StringBuilder(); for (EditorHandler instance : mEditorHandlers) { result.append(instance.toString(sharedPref)).append("\n"); } return result.toString(); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/intercept/IntentCompat.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.intercept; import static io.github.muntashirakon.AppManager.intercept.AddIntentExtraFragment.ExtraItem; import static io.github.muntashirakon.AppManager.intercept.AddIntentExtraFragment.TYPE_BOOLEAN; import static io.github.muntashirakon.AppManager.intercept.AddIntentExtraFragment.TYPE_COMPONENT_NAME; import static io.github.muntashirakon.AppManager.intercept.AddIntentExtraFragment.TYPE_FLOAT; import static io.github.muntashirakon.AppManager.intercept.AddIntentExtraFragment.TYPE_FLOAT_AL; import static io.github.muntashirakon.AppManager.intercept.AddIntentExtraFragment.TYPE_FLOAT_ARR; import static io.github.muntashirakon.AppManager.intercept.AddIntentExtraFragment.TYPE_INTEGER; import static io.github.muntashirakon.AppManager.intercept.AddIntentExtraFragment.TYPE_INT_AL; import static io.github.muntashirakon.AppManager.intercept.AddIntentExtraFragment.TYPE_INT_ARR; import static io.github.muntashirakon.AppManager.intercept.AddIntentExtraFragment.TYPE_LONG; import static io.github.muntashirakon.AppManager.intercept.AddIntentExtraFragment.TYPE_LONG_AL; import static io.github.muntashirakon.AppManager.intercept.AddIntentExtraFragment.TYPE_LONG_ARR; import static io.github.muntashirakon.AppManager.intercept.AddIntentExtraFragment.TYPE_NULL; import static io.github.muntashirakon.AppManager.intercept.AddIntentExtraFragment.TYPE_STRING; import static io.github.muntashirakon.AppManager.intercept.AddIntentExtraFragment.TYPE_STRING_AL; import static io.github.muntashirakon.AppManager.intercept.AddIntentExtraFragment.TYPE_STRING_ARR; import static io.github.muntashirakon.AppManager.intercept.AddIntentExtraFragment.TYPE_URI; import static io.github.muntashirakon.AppManager.intercept.AddIntentExtraFragment.TYPE_URI_AL; import static io.github.muntashirakon.AppManager.intercept.AddIntentExtraFragment.TYPE_URI_ARR; import static io.github.muntashirakon.AppManager.intercept.AddIntentExtraFragment.Type; import android.content.ComponentName; import android.content.Intent; import android.content.IntentHidden; import android.graphics.Rect; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Parcelable; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.os.BundleCompat; import androidx.core.util.Pair; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.StringTokenizer; import dev.rikka.tools.refine.Refine; import io.github.muntashirakon.AppManager.compat.IntegerCompat; import io.github.muntashirakon.AppManager.compat.UriCompat; import io.github.muntashirakon.AppManager.fm.FmUtils; import io.github.muntashirakon.AppManager.utils.ExUtils; public final class IntentCompat { public static void putWrappedParcelableExtra(@NonNull Intent intent, @Nullable String name, @Nullable Parcelable parcelable) { Bundle bundle = new Bundle(); bundle.putParcelable(name, parcelable); intent.putExtra(name, bundle); } @Nullable public static T getUnwrappedParcelableExtra(@NonNull Intent intent, @Nullable String name, @NonNull Class clazz) { Bundle bundle = intent.getBundleExtra(name); if (bundle == null) { return null; } return BundleCompat.getParcelable(bundle, name, clazz); } /** * Retrieve extended data from the intent. * * @param name The name of the desired item. * @param clazz The type of the object expected. * @return the value of an item previously added with putExtra(), * or null if no Parcelable value was found. * @see Intent#putExtra(String, Parcelable) */ @Nullable public static T getParcelableExtra(@NonNull Intent intent, @Nullable String name, @NonNull Class clazz) { return androidx.core.content.IntentCompat.getParcelableExtra(intent, name, clazz); } /** * Retrieve extended data from the intent. * * @param name The name of the desired item. * @param clazz The type of the items inside the array list. This is only verified when * parcelling. * @return the value of an item previously added with * putParcelableArrayListExtra(), or null if no * ArrayList value was found. * @see Intent#putParcelableArrayListExtra(String, ArrayList) */ @Nullable public static ArrayList getParcelableArrayListExtra(@NonNull Intent intent, @Nullable String name, @NonNull Class clazz) { return androidx.core.content.IntentCompat.getParcelableArrayListExtra(intent, name, clazz); } @Nullable public static Uri getDataUri(@Nullable Intent intent) { if (intent == null) { return null; } if (Intent.ACTION_SEND.equals(intent.getAction())) { return FmUtils.sanitizeContentInput(getParcelableExtra(intent, Intent.EXTRA_STREAM, Uri.class)); } return FmUtils.sanitizeContentInput(intent.getData()); } @Nullable public static List getDataUris(@NonNull Intent intent) { if (Intent.ACTION_SEND_MULTIPLE.equals(intent.getAction())) { List inputUris = getParcelableArrayListExtra(intent, Intent.EXTRA_STREAM, Uri.class); if (inputUris == null) { return null; } List filteredUris = new ArrayList<>(inputUris.size()); for (Uri uri : inputUris) { Uri fixedUri = FmUtils.sanitizeContentInput(uri); if (fixedUri != null) { filteredUris.add(fixedUri); } } return filteredUris.isEmpty() ? null : filteredUris; } Uri uri = getDataUri(intent); return uri != null ? Collections.singletonList(uri) : null; } public static void removeFlags(@NonNull Intent intent, int flags) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { intent.removeFlags(flags); } else { intent.setFlags(intent.getFlags() & ~flags); } } @Nullable public static Object parseExtraValue(@Type int type, String rawValue) { switch (type) { case TYPE_STRING: return rawValue; case TYPE_NULL: return null; case TYPE_INTEGER: return IntegerCompat.decode(rawValue); case TYPE_URI: return Uri.parse(rawValue); case TYPE_URI_ARR: { // Split on commas unless they are preceded by an escape. // The escape character must be escaped for the string and // again for the regex, thus four escape characters become one. String[] strings = rawValue.split("(? list = new ArrayList<>(strings.length); for (String s : strings) { list.add(Uri.parse(s)); } return list; } case TYPE_COMPONENT_NAME: ComponentName cn = ComponentName.unflattenFromString(rawValue); if (cn == null) { throw new IllegalArgumentException("Bad component name: " + rawValue); } return cn; case TYPE_INT_ARR: { String[] strings = rawValue.split(","); int[] list = new int[strings.length]; for (int i = 0; i < strings.length; i++) { list[i] = IntegerCompat.decode(strings[i].trim()); } return list; } case TYPE_INT_AL: { String[] strings = rawValue.split(","); ArrayList list = new ArrayList<>(strings.length); for (String string : strings) { list.add(IntegerCompat.decode(string.trim())); } return list; } case TYPE_LONG: return Long.parseLong(rawValue); case TYPE_LONG_ARR: { String[] strings = rawValue.split(","); long[] list = new long[strings.length]; for (int i = 0; i < strings.length; i++) { list[i] = Long.decode(strings[i].trim()); } return list; } case TYPE_LONG_AL: { String[] strings = rawValue.split(","); ArrayList list = new ArrayList<>(strings.length); for (String string : strings) { list.add(Long.decode(string.trim())); } return list; } case TYPE_FLOAT: return Float.parseFloat(rawValue); case TYPE_FLOAT_ARR: { String[] strings = rawValue.split(","); float[] list = new float[strings.length]; for (int i = 0; i < strings.length; i++) { list[i] = Float.parseFloat(strings[i]); } return list; } case TYPE_FLOAT_AL: { String[] strings = rawValue.split(","); ArrayList list = new ArrayList<>(strings.length); for (String string : strings) { list.add(Float.parseFloat(string)); } return list; } case TYPE_STRING_ARR: // Split on commas unless they are preceded by an escape. // The escape character must be escaped for the string and // again for the regex, thus four escape characters become one. return rawValue.split("(? list = new ArrayList<>(strings.length); Collections.addAll(list, strings); return list; } case TYPE_BOOLEAN: { // Boolean.valueOf() results in false for anything that is not "true", which is // error-prone in shell commands boolean boolValue; if ("true".equals(rawValue) || "t".equals(rawValue)) { boolValue = true; } else if ("false".equals(rawValue) || "f".equals(rawValue)) { boolValue = false; } else { try { boolValue = IntegerCompat.decode(rawValue) != 0; } catch (NumberFormatException ex) { throw new IllegalArgumentException("Invalid boolean value: " + rawValue); } } return boolValue; } default: throw new IllegalArgumentException("Unknown type: " + type); } } @Nullable private static Pair valueToParsableStringAndType(@Nullable Object object) { if (object == null) { return new Pair<>(TYPE_NULL, null); } else if (object instanceof String) { return new Pair<>(TYPE_STRING, (String) object); } else if (object instanceof Integer) { return new Pair<>(TYPE_INTEGER, String.valueOf((int) object)); } else if (object instanceof Long) { return new Pair<>(TYPE_LONG, String.valueOf((long) object)); } else if (object instanceof Float) { return new Pair<>(TYPE_FLOAT, String.valueOf((float) object)); } else if (object instanceof Boolean) { return new Pair<>(TYPE_BOOLEAN, String.valueOf((boolean) object)); } else if (object instanceof Uri) { return new Pair<>(TYPE_URI, object.toString()); } else if (object instanceof ComponentName) { return new Pair<>(TYPE_COMPONENT_NAME, ((ComponentName) object).flattenToString()); } else if (object instanceof int[]) { StringBuilder sb = new StringBuilder(); int[] list = (int[]) object; if (list.length >= 1) sb.append(list[0]); for (int i = 1; i < list.length; ++i) { sb.append(",").append(list[i]); } return new Pair<>(TYPE_INT_ARR, sb.toString()); } else if (object instanceof long[]) { StringBuilder sb = new StringBuilder(); long[] list = (long[]) object; if (list.length >= 1) sb.append(list[0]); for (int i = 1; i < list.length; ++i) { sb.append(",").append(list[i]); } return new Pair<>(TYPE_LONG_ARR, sb.toString()); } else if (object instanceof float[]) { StringBuilder sb = new StringBuilder(); float[] list = (float[]) object; if (list.length >= 1) sb.append(list[0]); for (int i = 1; i < list.length; ++i) { sb.append(",").append(list[i]); } return new Pair<>(TYPE_FLOAT_ARR, sb.toString()); } else if (object instanceof String[]) { StringBuilder sb = new StringBuilder(); String[] list = (String[]) object; if (list.length >= 1) sb.append(list[0].replace(",", "\\,")); for (int i = 1; i < list.length; ++i) { sb.append(",").append(list[i].replace(",", "\\,")); } return new Pair<>(TYPE_STRING_ARR, sb.toString()); } else if (object instanceof Uri[]) { StringBuilder sb = new StringBuilder(); Uri[] list = (Uri[]) object; if (list.length >= 1) sb.append(list[0].toString().replace(",", "\\,")); for (int i = 1; i < list.length; ++i) { sb.append(",").append(list[i].toString().replace(",", "\\,")); } return new Pair<>(TYPE_URI_ARR, sb.toString()); } else if (object instanceof List) { @SuppressWarnings("rawtypes") List list = (List) object; if (list.isEmpty()) { // Type is lost forever, return null // FIXME: Try to infer type using reflection return new Pair<>(TYPE_NULL, null); } Object item = list.get(0); if (item instanceof Integer) { StringBuilder sb = new StringBuilder(); sb.append(item); for (int i = 1; i < list.size(); ++i) { sb.append(",").append(list.get(i)); } return new Pair<>(TYPE_INT_AL, sb.toString()); } else if (item instanceof Long) { StringBuilder sb = new StringBuilder(); sb.append(item); for (int i = 1; i < list.size(); ++i) { sb.append(",").append(list.get(i)); } return new Pair<>(TYPE_LONG_AL, sb.toString()); } else if (item instanceof Float) { StringBuilder sb = new StringBuilder(); sb.append(item); for (int i = 1; i < list.size(); ++i) { sb.append(",").append(list.get(i)); } return new Pair<>(TYPE_FLOAT_AL, sb.toString()); } else if (item instanceof String) { StringBuilder sb = new StringBuilder(); sb.append(((String) item).replace(",", "\\,")); for (int i = 1; i < list.size(); ++i) { sb.append(",").append(((String) list.get(i)).replace(",", "\\,")); } return new Pair<>(TYPE_STRING_AL, sb.toString()); } else if (item instanceof Uri) { StringBuilder sb = new StringBuilder(); sb.append(item.toString().replace(",", "\\,")); for (int i = 1; i < list.size(); ++i) { sb.append(",").append(list.get(i).toString().replace(",", "\\,")); } return new Pair<>(TYPE_URI_AL, sb.toString()); } } return null; } public static void addToIntent(@NonNull Intent intent, @NonNull ExtraItem extraItem) { if (extraItem.keyValue == null && extraItem.type != TYPE_NULL) { return; } switch (extraItem.type) { case TYPE_BOOLEAN: intent.putExtra(extraItem.keyName, (boolean) extraItem.keyValue); break; case TYPE_FLOAT: intent.putExtra(extraItem.keyName, (float) extraItem.keyValue); break; case TYPE_FLOAT_AL: case TYPE_STRING_AL: case TYPE_LONG_AL: case TYPE_INT_AL: case TYPE_URI_AL: intent.putExtra(extraItem.keyName, (ArrayList) extraItem.keyValue); break; case TYPE_FLOAT_ARR: intent.putExtra(extraItem.keyName, (float[]) extraItem.keyValue); break; case TYPE_INTEGER: intent.putExtra(extraItem.keyName, (int) extraItem.keyValue); break; case TYPE_INT_ARR: intent.putExtra(extraItem.keyName, (int[]) extraItem.keyValue); break; case TYPE_LONG: intent.putExtra(extraItem.keyName, (long) extraItem.keyValue); break; case TYPE_LONG_ARR: intent.putExtra(extraItem.keyName, (long[]) extraItem.keyValue); break; case TYPE_NULL: intent.putExtra(extraItem.keyName, (String) null); break; case TYPE_STRING: intent.putExtra(extraItem.keyName, (String) extraItem.keyValue); break; case TYPE_STRING_ARR: intent.putExtra(extraItem.keyName, (String[]) extraItem.keyValue); break; case TYPE_COMPONENT_NAME: case TYPE_URI: intent.putExtra(extraItem.keyName, (Parcelable) extraItem.keyValue); break; case TYPE_URI_ARR: intent.putExtra(extraItem.keyName, (Parcelable[]) extraItem.keyValue); break; } } @NonNull public static List flattenToCommand(@NonNull Intent intent) { List args = new ArrayList<>(); String action = intent.getAction(); String data = intent.getDataString(); String type = intent.getType(); Set categories = intent.getCategories(); ComponentName cn = intent.getComponent(); String packageName = intent.getPackage(); int flags = intent.getFlags(); Bundle extras = intent.getExtras(); if (action != null) { args.add("-a"); args.add(action); } if (data != null) { args.add("-d"); args.add(data); } if (type != null) { args.add("-t"); args.add(type); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { String id = intent.getIdentifier(); if (id != null) { args.add("-i"); args.add(id); } } if (categories != null) { for (String category : categories) { args.add("-c"); args.add(category); } } if (cn != null) { args.add("-n"); args.add(cn.flattenToString()); } if (extras != null) { for (String key : extras.keySet()) { Pair typeAndString = valueToParsableStringAndType(extras.get(key)); if (typeAndString == null) { // else unsupported bundle item, ignore continue; } switch (typeAndString.first) { case TYPE_STRING: args.add("--es"); break; case TYPE_NULL: args.add("--esn"); break; case TYPE_BOOLEAN: args.add("--ez"); break; case TYPE_INTEGER: args.add("--ei"); break; case TYPE_LONG: args.add("--el"); break; case TYPE_FLOAT: args.add("--ef"); break; case TYPE_URI: args.add("--eu"); break; case TYPE_COMPONENT_NAME: args.add("--ecn"); break; case TYPE_INT_ARR: args.add("--eia"); break; case TYPE_INT_AL: args.add("--eial"); break; case TYPE_LONG_ARR: args.add("--ela"); break; case TYPE_LONG_AL: args.add("--elal"); break; case TYPE_FLOAT_ARR: args.add("--efa"); break; case TYPE_FLOAT_AL: args.add("--efal"); break; case TYPE_STRING_ARR: args.add("--esa"); break; case TYPE_STRING_AL: args.add("--esal"); break; default: // Unsupported continue; } // Add key args.add(key); if (typeAndString.first != TYPE_NULL) { // All except NULL has a value args.add(typeAndString.second); } } } args.add("-f"); args.add(String.valueOf(flags)); if (packageName != null) { args.add(packageName); } return args; } @NonNull public static String flattenToString(@NonNull Intent intent) { String action = intent.getAction(); String data = intent.getDataString(); String type = intent.getType(); Set categories = intent.getCategories(); ComponentName cn = intent.getComponent(); String packageName = intent.getPackage(); int flags = intent.getFlags(); Bundle extras = intent.getExtras(); StringBuilder sb = new StringBuilder("VERSION\t").append(1).append("\n"); if (action != null) sb.append("ACTION\t").append(action).append("\n"); if (data != null) sb.append("DATA\t").append(data).append("\n"); if (type != null) sb.append("TYPE\t").append(type).append("\n"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { String id = intent.getIdentifier(); if (id != null) sb.append("IDENTIFIER\t").append(id).append("\n"); } if (categories != null) { for (String category : categories) { sb.append("CATEGORY\t").append(category).append("\n"); } } if (cn != null) sb.append("COMPONENT\t").append(cn.flattenToString()).append("\n"); if (packageName != null) sb.append("PACKAGE\t").append(packageName).append("\n"); if (flags != 0) sb.append("FLAGS\t0x").append(Integer.toHexString(flags)).append("\n"); if (extras != null) { for (String key : extras.keySet()) { Pair typeAndString = valueToParsableStringAndType(extras.get(key)); if (typeAndString != null) { sb.append("EXTRA\t").append(key).append("\t").append(typeAndString.first); if (typeAndString.first != TYPE_NULL) { sb.append("\t").append(typeAndString.second); } sb.append("\n"); } // else unsupported bundle item, ignore // TODO: Add support for more items } } return sb.toString(); } @NonNull public static String describeIntent(@NonNull Intent intent, String prefix) { String action = intent.getAction(); String data = intent.getDataString(); String type = intent.getType(); Set categories = intent.getCategories(); ComponentName cn = intent.getComponent(); String packageName = intent.getPackage(); int flags = intent.getFlags(); Bundle extras = intent.getExtras(); StringBuilder sb = new StringBuilder(); if (action != null) sb.append(prefix).append(" ACTION\t").append(action).append("\n"); if (data != null) sb.append(prefix).append(" DATA\t").append(data).append("\n"); if (type != null) sb.append(prefix).append(" TYPE\t").append(type).append("\n"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { String id = intent.getIdentifier(); if (id != null) sb.append(prefix).append(" IDENTIFIER\t").append(id).append("\n"); } if (categories != null) { for (String category : categories) { sb.append(prefix).append(" CATEGORY\t").append(category).append("\n"); } } if (cn != null) sb.append(prefix).append(" COMPONENT\t").append(cn.flattenToString()).append("\n"); if (packageName != null) sb.append(prefix).append(" PACKAGE\t").append(packageName).append("\n"); if (flags != 0) sb.append(prefix).append(" FLAGS\t0x").append(Integer.toHexString(flags)).append("\n"); if (extras != null) { for (String key : extras.keySet()) { Pair typeAndString = valueToParsableStringAndType(extras.get(key)); if (typeAndString != null) { sb.append(prefix).append(" EXTRA\t").append(key).append("\t").append(typeAndString.first); if (typeAndString.first != TYPE_NULL) { sb.append("\t").append(typeAndString.second); } sb.append("\n"); } // else unsupported bundle item, ignore // TODO: Add support for more items } } return sb.toString(); } @Nullable public static Intent unflattenFromString(@NonNull String intentString) { Intent intent = new Intent(); String[] lines = intentString.split("\n"); Uri data = null; String type = null; for (String line : lines) { if (TextUtils.isEmpty(line)) continue; StringTokenizer tokenizer = new StringTokenizer(line, "\t"); if (tokenizer.countTokens() < 2) { // Invalid line return null; } switch (tokenizer.nextToken()) { case "VERSION": { int version = IntegerCompat.decode(tokenizer.nextToken()); if (version != 1) { // Unsupported version return null; } break; } case "ACTION": intent.setAction(tokenizer.nextToken()); break; case "DATA": data = Uri.parse(tokenizer.nextToken()); break; case "TYPE": type = tokenizer.nextToken(); break; case "IDENTIFIER": if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { intent.setIdentifier(tokenizer.nextToken()); } break; case "CATEGORY": intent.addCategory(tokenizer.nextToken()); break; case "COMPONENT": intent.setComponent(ComponentName.unflattenFromString(tokenizer.nextToken())); break; case "PACKAGE": intent.setPackage(tokenizer.nextToken()); break; case "FLAGS": intent.setFlags(IntegerCompat.decode(tokenizer.nextToken())); break; case "EXTRA": { ExtraItem item = new ExtraItem(); item.keyName = tokenizer.nextToken(); item.type = IntegerCompat.decode(tokenizer.nextToken()); item.keyValue = parseExtraValue(item.type, tokenizer.nextToken()); addToIntent(intent, item); } } } if (data != null) { intent.setDataAndType(data, type); } else if (type != null) { intent.setType(type); } return intent; } public static int getExtendedFlags(@NonNull Intent intent) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { // Added in Android 14 r50 return ExUtils.requireNonNullElse(() -> Refine.unsafeCast(intent).getExtendedFlags(), 0); } return 0; } /** * Convert this Intent into a String holding a URI representation of it. * The returned URI string has been properly URI encoded, so it can be * used with {@link Uri#parse Uri.parse(String)}. The URI contains the * Intent's data as the base URI, with an additional fragment describing * the action, categories, type, flags, package, component, and extras. * *

You can convert the returned string back to an Intent with * {@link Intent#getIntent(String)}. * * @param flags Additional operating flags. * @return Returns a URI encoding URI string describing the entire contents * of the Intent. * @see Intent#toUri(int) */ @NonNull public static String toUri(@NonNull Intent intent, int flags) { long flagsLong = intent.getFlags() & 0xFFFFFFFFL; if (flagsLong < 0x80000000L || Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { return intent.toUri(flags); } // Special workaround for Motorola and MIUI (> A10 and < A15) StringBuilder uri = new StringBuilder(128); if ((flags & Intent.URI_ANDROID_APP_SCHEME) != 0) { if (intent.getPackage() == null) { throw new IllegalArgumentException( "Intent must include an explicit package name to build an android-app: " + intent); } uri.append("android-app://"); uri.append(Uri.encode(intent.getPackage())); String scheme = null; Uri data = intent.getData(); if (data != null) { // All values here must be wrapped with Uri#encodeIfNotEncoded because it is // possible to exploit the Uri API to return a raw unencoded value, which will // not deserialize properly and may cause the resulting Intent to be transformed // to a malicious value. scheme = UriCompat.encodeIfNotEncoded(data.getScheme(), null); if (scheme != null) { uri.append('/'); uri.append(scheme); String authority = UriCompat.encodeIfNotEncoded(data.getEncodedAuthority(), null); if (authority != null) { uri.append('/'); uri.append(authority); // Multiple path segments are allowed, don't encode the path / separator String path = UriCompat.encodeIfNotEncoded(data.getEncodedPath(), "/"); if (path != null) { uri.append(path); } String queryParams = UriCompat.encodeIfNotEncoded(data.getEncodedQuery(), null); if (queryParams != null) { uri.append('?'); uri.append(queryParams); } String fragment = UriCompat.encodeIfNotEncoded(data.getEncodedFragment(), null); if (fragment != null) { uri.append('#'); uri.append(fragment); } } } } toUriFragment(intent, uri, null, scheme == null ? Intent.ACTION_MAIN : Intent.ACTION_VIEW, intent.getPackage(), flags); return uri.toString(); } String scheme = null; Uri dataUri = intent.getData(); if (dataUri != null) { String data = dataUri.toString(); if ((flags & Intent.URI_INTENT_SCHEME) != 0) { final int N = data.length(); for (int i = 0; i < N; i++) { char c = data.charAt(i); if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '.' || c == '-' || c == '+') { continue; } if (c == ':' && i > 0) { // Valid scheme. scheme = data.substring(0, i); uri.append("intent:"); data = data.substring(i + 1); break; } // No scheme. break; } } uri.append(data); } else if ((flags & Intent.URI_INTENT_SCHEME) != 0) { uri.append("intent:"); } toUriFragment(intent, uri, scheme, Intent.ACTION_VIEW, null, flags); return uri.toString(); } private static void toUriFragment(Intent intent, StringBuilder uri, @Nullable String scheme, String defAction, @Nullable String defPackage, int flags) { StringBuilder frag = new StringBuilder(128); toUriInner(intent, frag, scheme, defAction, defPackage, flags); Intent selector = intent.getSelector(); if (selector != null) { frag.append("SEL;"); // Note that for now we are not going to try to handle the // data part; not clear how to represent this as a URI, and // not much utility in it. toUriInner(selector, frag, selector.getData() != null ? selector.getData().getScheme() : null, null, null, flags); } if (frag.length() > 0) { uri.append("#Intent;"); uri.append(frag); uri.append("end"); } } private static void toUriInner(Intent intent, StringBuilder uri, @Nullable String scheme, @Nullable String defAction, @Nullable String defPackage, int flags) { if (scheme != null) { uri.append("scheme=").append(Uri.encode(scheme)).append(';'); } String action = intent.getAction(); if (action != null && !action.equals(defAction)) { uri.append("action=").append(Uri.encode(action)).append(';'); } Set categories = intent.getCategories(); if (categories != null) { for (String category : categories) { uri.append("category=").append(Uri.encode(category)).append(';'); } } String type = intent.getType(); if (type != null) { uri.append("type=").append(Uri.encode(type, "/")).append(';'); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { String mIdentifier = intent.getIdentifier(); if (mIdentifier != null) { uri.append("identifier=").append(Uri.encode(mIdentifier, "/")).append(';'); } } int intentFlags = intent.getFlags(); if (intentFlags != 0) { uri.append("launchFlags=").append(IntegerCompat.toSignedHex(intent.getFlags())).append(';'); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { int extendedFlags = getExtendedFlags(intent); if (extendedFlags != 0) { uri.append("extendedLaunchFlags=0x").append(Integer.toHexString(extendedFlags)) .append(';'); } } String intentPackage = intent.getPackage(); if (intentPackage != null && !intentPackage.equals(defPackage)) { uri.append("package=").append(Uri.encode(intentPackage)).append(';'); } ComponentName component = intent.getComponent(); if (component != null) { uri.append("component=").append(Uri.encode( component.flattenToShortString(), "/")).append(';'); } Rect sourceBounds = intent.getSourceBounds(); if (sourceBounds != null) { uri.append("sourceBounds=") .append(Uri.encode(sourceBounds.flattenToString())) .append(';'); } Bundle extras = intent.getExtras(); if (extras != null) { for (String key : extras.keySet()) { final Object value = extras.get(key); char entryType = value instanceof String ? 'S' : value instanceof Boolean ? 'B' : value instanceof Byte ? 'b' : value instanceof Character ? 'c' : value instanceof Double ? 'd' : value instanceof Float ? 'f' : value instanceof Integer ? 'i' : value instanceof Long ? 'l' : value instanceof Short ? 's' : '\0'; if (entryType != '\0') { uri.append(entryType); uri.append('.'); uri.append(Uri.encode(key)); uri.append('='); uri.append(Uri.encode(value.toString())); uri.append(';'); } } } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/intercept/InterceptorShortcutInfo.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.intercept; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.Parcel; import android.os.PersistableBundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.os.ParcelCompat; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.shortcut.ShortcutInfo; public class InterceptorShortcutInfo extends ShortcutInfo { public static final String TAG = InterceptorShortcutInfo.class.getSimpleName(); private final Intent intent; public InterceptorShortcutInfo(@NonNull Intent intent) { this.intent = new Intent(intent); fixIntent(this.intent); } protected InterceptorShortcutInfo(Parcel in) { super(in); intent = ParcelCompat.readParcelable(in, Intent.class.getClassLoader(), Intent.class); } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeParcelable(intent, flags); } @Override public Intent toShortcutIntent(@NonNull Context context) { return intent; } public static final Creator CREATOR = new Creator() { @Override public InterceptorShortcutInfo createFromParcel(Parcel source) { return new InterceptorShortcutInfo(source); } @Override public InterceptorShortcutInfo[] newArray(int size) { return new InterceptorShortcutInfo[size]; } }; @SuppressWarnings("deprecation") private static void fixIntent(@NonNull Intent intent) { Bundle extras = intent.getExtras(); if (extras == null) { // Nothing to do return; } // Shortcuts use PersistableBundle for extras which only support 12 types for (String key : extras.keySet()) { Object value = extras.get(key); if (!isValidType(value)) { // Not a valid type, remove it from intent Log.w(TAG, "Removing unsupported key %s (class: %s, value: %s)", key, value.getClass().getName(), value); intent.removeExtra(key); } } } /** * @see PersistableBundle#isValidType(Object) */ private static boolean isValidType(@Nullable Object value) { return (value instanceof Integer) || (value instanceof Long) || (value instanceof Double) || (value instanceof String) || (value instanceof int[]) || (value instanceof long[]) || (value instanceof double[]) || (value instanceof String[]) || (value instanceof PersistableBundle) || (value == null) || (value instanceof Boolean) || (value instanceof boolean[]); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/ipc/AMService.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.ipc; import android.content.Intent; import android.os.Binder; import android.os.IBinder; import android.os.Parcel; import android.os.RemoteException; import android.os.ServiceManager; import android.system.ErrnoException; import android.system.Os; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.io.File; import aosp.android.content.pm.ParceledListSlice; import io.github.muntashirakon.AppManager.IAMService; import io.github.muntashirakon.AppManager.IRemoteProcess; import io.github.muntashirakon.AppManager.IRemoteShell; import io.github.muntashirakon.AppManager.ipc.ps.ProcessEntry; import io.github.muntashirakon.AppManager.ipc.ps.Ps; import io.github.muntashirakon.AppManager.server.common.IRootServiceManager; import io.github.muntashirakon.compat.os.ParcelCompat2; public class AMService extends RootService { static class IAMServiceImpl extends IAMService.Stub { /** * To get {@link Process}, wrap it using {@link RemoteProcess}. Since the streams are piped, * I/O operations may have to be done in different threads. */ @Override public IRemoteProcess newProcess(String[] cmd, String[] env, String dir) throws RemoteException { Process process; try { process = Runtime.getRuntime().exec(cmd, env, dir != null ? new File(dir) : null); } catch (Exception e) { throw new RemoteException(e.getMessage()); } return new RemoteProcessImpl(process); } @Override public IRemoteShell getShell(String[] cmd) { return new RemoteShellImpl(cmd); } @Override public ParceledListSlice getRunningProcesses() { Ps ps = new Ps(); ps.loadProcesses(); return new ParceledListSlice<>(ps.getProcesses()); } @Override public int getUid() { return android.os.Process.myUid(); } @Override public void symlink(String file, String link) throws RemoteException { try { Os.symlink(file, link); } catch (ErrnoException e) { throw new RemoteException(e.getMessage()); } } @Override public IBinder getService(String serviceName) throws RemoteException { return ServiceManager.getService(serviceName); } @Override public boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags) throws RemoteException { if (code == ProxyBinder.PROXY_BINDER_TRANSACTION) { data.enforceInterface(IRootServiceManager.class.getName()); transactRemote(data, reply); return true; } return super.onTransact(code, data, reply, flags); } /** * Call target Binder received through {@link ProxyBinder}. * * @author Rikka */ private void transactRemote(@NonNull Parcel data, @Nullable Parcel reply) throws RemoteException { IBinder targetBinder = data.readStrongBinder(); int targetCode = data.readInt(); int targetFlags = data.readInt(); Parcel newData = ParcelCompat2.obtain(targetBinder); try { newData.appendFrom(data, data.dataPosition(), data.dataAvail()); long id = Binder.clearCallingIdentity(); targetBinder.transact(targetCode, newData, reply, targetFlags); Binder.restoreCallingIdentity(id); } catch (RemoteException e) { throw e; } catch (Throwable th) { throw (RemoteException) new RemoteException(th.getMessage()).initCause(th); } finally { newData.recycle(); } } } @Override public IBinder onBind(@NonNull Intent intent) { Log.d(TAG, "AMService: onBind"); return new IAMServiceImpl(); } @Override public void onRebind(@NonNull Intent intent) { super.onRebind(intent); } @Override public boolean onUnbind(@NonNull Intent intent) { return true; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/ipc/BinderHolder.java ================================================ // SPDX-License-Identifier: Apache-2.0 package io.github.muntashirakon.AppManager.ipc; import android.os.IBinder; import android.os.RemoteException; import com.topjohnwu.superuser.internal.UiThreadHandler; // Copyright 2022 John "topjohnwu" Wu abstract class BinderHolder implements IBinder.DeathRecipient { private final IBinder mBinder; BinderHolder(IBinder b) throws RemoteException { mBinder = b; mBinder.linkToDeath(this, 0); } @Override public final void binderDied() { mBinder.unlinkToDeath(this, 0); UiThreadHandler.run(this::onBinderDied); } protected abstract void onBinderDied(); } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/ipc/Container.java ================================================ // SPDX-License-Identifier: Apache-2.0 package io.github.muntashirakon.AppManager.ipc; // Copyright 2020 John "topjohnwu" Wu class Container { public T obj; } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/ipc/FileSystemService.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.ipc; import android.content.Intent; import android.os.IBinder; import androidx.annotation.NonNull; import io.github.muntashirakon.io.FileSystemManager; public class FileSystemService extends RootService { @Override public IBinder onBind(@NonNull Intent intent) { return FileSystemManager.getService(); } @Override public void onRebind(@NonNull Intent intent) { super.onRebind(intent); } @Override public boolean onUnbind(@NonNull Intent intent) { return true; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/ipc/HiddenAPIs.java ================================================ // SPDX-License-Identifier: Apache-2.0 package io.github.muntashirakon.AppManager.ipc; import android.annotation.SuppressLint; import android.content.Context; import android.content.ContextWrapper; import android.os.Build; import android.os.IBinder; import android.util.Log; import java.lang.reflect.Method; /** * All hidden Android framework APIs used here are very stable. *

* These methods should only be accessed in the root process, since under normal circumstances * accessing these internal APIs through reflection will be blocked. */ // Copyright 2020 John "topjohnwu" Wu @SuppressLint({"PrivateApi,DiscouragedPrivateApi,SoonBlockedPrivateApi", "RestrictedApi"}) class HiddenAPIs { public static final String TAG = HiddenAPIs.class.getSimpleName(); private static Method sAddService; private static Method sAttachBaseContext; private static Method sSetAppName; // Set this flag to silence AMS's complaints. Only exist on Android 8.0+ public static final int FLAG_RECEIVER_FROM_SHELL = Build.VERSION.SDK_INT >= 26 ? 0x00400000 : 0; static { try { Class sm = Class.forName("android.os.ServiceManager"); if (Build.VERSION.SDK_INT >= 28) { try { sAddService = sm.getDeclaredMethod("addService", String.class, IBinder.class, boolean.class, int.class); } catch (NoSuchMethodException ignored) { // Fallback to the 2 argument version } } if (sAddService == null) { sAddService = sm.getDeclaredMethod("addService", String.class, IBinder.class); } sAttachBaseContext = ContextWrapper.class.getDeclaredMethod("attachBaseContext", Context.class); sAttachBaseContext.setAccessible(true); Class ddm = Class.forName("android.ddm.DdmHandleAppName"); sSetAppName = ddm.getDeclaredMethod("setAppName", String.class, int.class); } catch (ReflectiveOperationException e) { Log.e(TAG, e.getMessage(), e); } } static void setAppName(String name) { try { sSetAppName.invoke(null, name, 0); } catch (ReflectiveOperationException e) { throw new RuntimeException(e); } } static void addService(String name, IBinder service) { try { if (sAddService.getParameterTypes().length == 4) { // Set dumpPriority to 0 so the service cannot be listed sAddService.invoke(null, name, service, false, 0); } else { sAddService.invoke(null, name, service); } } catch (ReflectiveOperationException e) { Log.e(TAG, e.getMessage(), e); } } static void attachBaseContext(Object wrapper, Context context) { if (wrapper instanceof ContextWrapper) { try { sAttachBaseContext.invoke(wrapper, context); } catch (ReflectiveOperationException ignored) { /* Impossible */ } } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/ipc/LocalServices.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.ipc; import android.os.Process; import android.os.RemoteException; import androidx.annotation.AnyThread; import androidx.annotation.MainThread; import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.IAMService; import io.github.muntashirakon.AppManager.misc.NoOps; import io.github.muntashirakon.AppManager.settings.Ops; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.io.FileSystemManager; public class LocalServices { private static final Object sBindLock = new Object(); @NonNull private static final ServiceConnectionWrapper sFileSystemServiceConnectionWrapper = new ServiceConnectionWrapper(BuildConfig.APPLICATION_ID, FileSystemService.class.getName()); @WorkerThread public static void bindServicesIfNotAlready() throws RemoteException { if (!alive()) { bindServices(); } } @WorkerThread public static void bindServices() throws RemoteException { synchronized (sBindLock) { unbindServicesIfRunning(); bindAmService(); bindFileSystemManager(); // Verify binding if (!getAmService().asBinder().pingBinder()) { throw new RemoteException("IAmService not running."); } getFileSystemManager(); // Update UID Ops.setWorkingUid(getAmService().getUid()); } } public static boolean alive() { synchronized (sAMServiceConnectionWrapper) { return sAMServiceConnectionWrapper.isBinderActive(); } } @WorkerThread @NoOps(used = true) private static void bindFileSystemManager() throws RemoteException { synchronized (sFileSystemServiceConnectionWrapper) { try { sFileSystemServiceConnectionWrapper.bindService(); } finally { sFileSystemServiceConnectionWrapper.notifyAll(); } } } @AnyThread @NonNull @NoOps public static FileSystemManager getFileSystemManager() throws RemoteException { synchronized (sFileSystemServiceConnectionWrapper) { try { return FileSystemManager.getRemote(sFileSystemServiceConnectionWrapper.getService()); } finally { sFileSystemServiceConnectionWrapper.notifyAll(); } } } @NonNull private static final ServiceConnectionWrapper sAMServiceConnectionWrapper = new ServiceConnectionWrapper(BuildConfig.APPLICATION_ID, AMService.class.getName()); @WorkerThread @NoOps(used = true) private static void bindAmService() throws RemoteException { synchronized (sAMServiceConnectionWrapper) { try { sAMServiceConnectionWrapper.bindService(); } finally { sAMServiceConnectionWrapper.notifyAll(); } } } @AnyThread @NonNull @NoOps public static IAMService getAmService() throws RemoteException { synchronized (sAMServiceConnectionWrapper) { try { return IAMService.Stub.asInterface(sAMServiceConnectionWrapper.getService()); } finally { sAMServiceConnectionWrapper.notifyAll(); } } } @WorkerThread @NoOps public static void stopServices() { synchronized (sAMServiceConnectionWrapper) { sAMServiceConnectionWrapper.stopDaemon(); } synchronized (sFileSystemServiceConnectionWrapper) { sFileSystemServiceConnectionWrapper.stopDaemon(); } Ops.setWorkingUid(Process.myUid()); } @MainThread public static void unbindServices() { synchronized (sAMServiceConnectionWrapper) { sAMServiceConnectionWrapper.unbindService(); } synchronized (sFileSystemServiceConnectionWrapper) { sFileSystemServiceConnectionWrapper.unbindService(); } Ops.setWorkingUid(Process.myUid()); } @WorkerThread private static void unbindServicesIfRunning() { // Basically unregister the services so that we can open another connection CountDownLatch unbindWatcher = new CountDownLatch(1); ThreadUtils.postOnMainThread(() -> { try { unbindServices(); } finally { unbindWatcher.countDown(); } }); try { unbindWatcher.await(30, TimeUnit.SECONDS); } catch (InterruptedException ignore) { } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/ipc/ProxyBinder.java ================================================ // SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.ipc; import android.os.Build; import android.os.IBinder; import android.os.IInterface; import android.os.Parcel; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ServiceManager; import android.os.ShellCallback; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.collection.ArrayMap; import org.jetbrains.annotations.NotNull; import java.io.FileDescriptor; import java.util.Collections; import java.util.Map; import java.util.Objects; import io.github.muntashirakon.AppManager.compat.BinderCompat; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.server.common.IRootServiceManager; import io.github.muntashirakon.compat.os.ParcelCompat2; // Copyright 2020 Rikka public class ProxyBinder implements IBinder { private static final String TAG = ProxyBinder.class.getSimpleName(); public static final int PROXY_BINDER_TRANSACTION = 2; /** * IBinder protocol transaction code: execute a shell command. */ public static final int SHELL_COMMAND_TRANSACTION = ('_' << 24) | ('C' << 16) | ('M' << 8) | 'D'; private static final Map sServiceCache = Collections.synchronizedMap(new ArrayMap<>()); @NonNull public static IBinder getService(String serviceName) throws ServiceNotFoundException { IBinder binder = sServiceCache.get(serviceName); if (binder == null) { binder = getServiceInternal(serviceName); sServiceCache.put(serviceName, binder); } return new ProxyBinder(binder); } /** * Some services can't be called without certain permissions * so we redirect to AMService who can make that call no mater which mode it's in. * as 0, 1000, and 2000 all have access to the overlay service. * * @param serviceName service to be loaded * @return binder to that service */ @NotNull private static IBinder getServiceInternal(String serviceName) throws ServiceNotFoundException { IBinder binder = ServiceManager.getService(serviceName); if (LocalServices.alive() && binder == null) { try { binder = LocalServices.getAmService().getService(serviceName); } catch (RemoteException e) { Log.e(TAG, e); throw new ServiceNotFoundException("Service couldn't be loaded: " + serviceName, e); } } if (binder == null) { throw new ServiceNotFoundException("Service couldn't be found: " + serviceName); } return binder; } @NonNull public static IBinder getUnprivilegedService(String serviceName) throws ServiceNotFoundException { IBinder binder = sServiceCache.get(serviceName); if (binder == null) { binder = ServiceManager.getService(serviceName); sServiceCache.put(serviceName, binder); } if (binder == null) { throw new ServiceNotFoundException("Service couldn't be found: " + serviceName); } return binder; } /** * @see BinderCompat#shellCommand(IBinder, FileDescriptor, FileDescriptor, FileDescriptor, String[], ShellCallback, ResultReceiver) */ @RequiresApi(Build.VERSION_CODES.N) public static void shellCommand(@NonNull IBinder binder, @NonNull FileDescriptor in, @NonNull FileDescriptor out, @NonNull FileDescriptor err, @NonNull String[] args, @Nullable ShellCallback callback, @NonNull ResultReceiver resultReceiver) throws RemoteException { if (!(binder instanceof ProxyBinder)) { BinderCompat.shellCommand(binder, in, out, err, args, callback, resultReceiver); return; } ProxyBinder proxyBinder = (ProxyBinder) binder; Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeFileDescriptor(in); data.writeFileDescriptor(out); data.writeFileDescriptor(err); data.writeStringArray(args); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { ShellCallback.writeToParcel(callback, data); } resultReceiver.writeToParcel(data, 0); try { proxyBinder.transact(SHELL_COMMAND_TRANSACTION, data, reply, 0); reply.readException(); } finally { data.recycle(); reply.recycle(); } } private final IBinder mOriginal; public ProxyBinder(@NonNull IBinder original) { mOriginal = Objects.requireNonNull(original); } @Override public boolean transact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags) throws RemoteException { if (LocalServices.alive()) { IBinder targetBinder = LocalServices.getAmService().asBinder(); Parcel newData = ParcelCompat2.obtain(targetBinder); try { newData.writeInterfaceToken(IRootServiceManager.class.getName()); newData.writeStrongBinder(mOriginal); newData.writeInt(code); newData.writeInt(flags); newData.appendFrom(data, 0, data.dataSize()); // Transact via AMService targetBinder.transact(PROXY_BINDER_TRANSACTION, newData, reply, 0); } finally { newData.recycle(); } return true; } // Run unprivileged code as a fallback method return mOriginal.transact(code, data, reply, flags); } @Nullable @Override public String getInterfaceDescriptor() { try { return mOriginal.getInterfaceDescriptor(); } catch (RemoteException e) { throw new IllegalStateException(e.getClass().getSimpleName(), e); } } @Override public boolean pingBinder() { return mOriginal.pingBinder(); } @Override public boolean isBinderAlive() { return mOriginal.isBinderAlive(); } @Nullable @Override public IInterface queryLocalInterface(@NonNull String descriptor) { return null; } @Override public void dump(@NonNull FileDescriptor fd, @Nullable String[] args) { try { mOriginal.dump(fd, args); } catch (RemoteException e) { throw new IllegalStateException(e.getClass().getSimpleName(), e); } } @Override public void dumpAsync(@NonNull FileDescriptor fd, @Nullable String[] args) { try { mOriginal.dumpAsync(fd, args); } catch (RemoteException e) { throw new IllegalStateException(e.getClass().getSimpleName(), e); } } @Override public void linkToDeath(@NonNull DeathRecipient recipient, int flags) { try { mOriginal.linkToDeath(recipient, flags); } catch (RemoteException e) { throw new IllegalStateException(e.getClass().getSimpleName(), e); } } @Override public boolean unlinkToDeath(@NonNull DeathRecipient recipient, int flags) { return mOriginal.unlinkToDeath(recipient, flags); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/ipc/RemoteProcess.java ================================================ // SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.ipc; import android.os.IBinder; import android.os.Parcel; import android.os.ParcelFileDescriptor; import android.os.Parcelable; import android.os.RemoteException; import androidx.annotation.NonNull; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.concurrent.TimeUnit; import io.github.muntashirakon.AppManager.IRemoteProcess; // Copyright 2020 Rikka // Copyright 2023 Muntashir Al-Islam public class RemoteProcess extends Process implements Parcelable { private final IRemoteProcess mRemote; private OutputStream mOs; private InputStream mIs; public RemoteProcess(IRemoteProcess remote) { mRemote = remote; } @Override public OutputStream getOutputStream() { if (mOs == null) { mOs = new RemoteOutputStream(mRemote); } return mOs; } @Override public InputStream getInputStream() { if (mIs == null) { try { mIs = new ParcelFileDescriptor.AutoCloseInputStream(mRemote.getInputStream()); } catch (RemoteException e) { throw new RuntimeException(e); } } return mIs; } @Override public InputStream getErrorStream() { try { return new ParcelFileDescriptor.AutoCloseInputStream(mRemote.getErrorStream()); } catch (RemoteException e) { throw new RuntimeException(e); } } @Override public int waitFor() throws InterruptedException { try { return mRemote.waitFor(); } catch (RemoteException e) { throw new RuntimeException(e); } } @Override public int exitValue() { try { return mRemote.exitValue(); } catch (RemoteException e) { throw new RuntimeException(e); } } @Override public void destroy() { try { mRemote.destroy(); } catch (RemoteException e) { throw new RuntimeException(e); } } public boolean alive() { try { return mRemote.alive(); } catch (RemoteException e) { throw new RuntimeException(e); } } public boolean waitForTimeout(long timeout, TimeUnit unit) throws InterruptedException { try { return mRemote.waitForTimeout(timeout, unit.toString()); } catch (RemoteException e) { throw new RuntimeException(e); } } public IBinder asBinder() { return mRemote.asBinder(); } private RemoteProcess(Parcel in) { mRemote = IRemoteProcess.Stub.asInterface(in.readStrongBinder()); } public static final Creator CREATOR = new Creator() { @Override public RemoteProcess createFromParcel(Parcel in) { return new RemoteProcess(in); } @Override public RemoteProcess[] newArray(int size) { return new RemoteProcess[size]; } }; @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeStrongBinder(mRemote.asBinder()); } private static class RemoteOutputStream extends OutputStream { @NonNull private final IRemoteProcess mRemoteProcess; private OutputStream mOutputStream; private boolean mIsClosed = false; public RemoteOutputStream(@NonNull IRemoteProcess remoteProcess) { mRemoteProcess = remoteProcess; } @Override public void write(int b) throws IOException { if (mIsClosed) { throw new IOException("Remote is closed."); } if (mOutputStream == null) { try { mOutputStream = new ParcelFileDescriptor.AutoCloseOutputStream(mRemoteProcess.getOutputStream()); } catch (RemoteException e) { throw new IOException(e); } } mOutputStream.write(b); } @Override public void flush() throws IOException { if (mIsClosed) { throw new IOException("Remote is closed."); } if (mOutputStream != null) { mOutputStream.close(); } mOutputStream = null; } @Override public void close() throws IOException { mIsClosed = true; if (mOutputStream != null) { mOutputStream.close(); } try { mRemoteProcess.closeOutputStream(); } catch (RemoteException e) { throw new IOException(e); } } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/ipc/RemoteProcessImpl.java ================================================ // SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.ipc; import android.os.ParcelFileDescriptor; import android.os.SystemClock; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import io.github.muntashirakon.AppManager.IRemoteProcess; import io.github.muntashirakon.io.IoUtils; // Copyright 2020 Rikka // Copyright 2023 Muntashir Al-Islam public class RemoteProcessImpl extends IRemoteProcess.Stub { private final Process mProcess; private ParcelFileDescriptor mIn; private OutputTransferThread mOutputTransferThread; public RemoteProcessImpl(Process process) { mProcess = process; } @Override public ParcelFileDescriptor getOutputStream() { if (mOutputTransferThread == null) { mOutputTransferThread = new OutputTransferThread(mProcess); mOutputTransferThread.start(); } try { return mOutputTransferThread.getWriteSide(); } catch (IOException | InterruptedException e) { throw new IllegalStateException(e); } } @Override public void closeOutputStream() { if (mOutputTransferThread != null) { mOutputTransferThread.interrupt(); } } @Override public ParcelFileDescriptor getInputStream() { if (mIn == null) { try { InputTransferThread thread = new InputTransferThread(mProcess, false); thread.start(); mIn = thread.getReadSide(); } catch (IOException | InterruptedException e) { throw new IllegalStateException(e); } } return mIn; } @Override public ParcelFileDescriptor getErrorStream() { try { InputTransferThread thread = new InputTransferThread(mProcess, true); thread.start(); return thread.getReadSide(); } catch (IOException | InterruptedException e) { throw new IllegalStateException(e); } } @Override public int waitFor() { try { return mProcess.waitFor(); } catch (InterruptedException e) { throw new IllegalStateException(e); } } @Override public int exitValue() { return mProcess.exitValue(); } @Override public void destroy() { mProcess.destroy(); } @Override public boolean alive() { try { exitValue(); return false; } catch (IllegalThreadStateException e) { return true; } } @Override public boolean waitForTimeout(long timeout, String unitName) { TimeUnit unit = TimeUnit.valueOf(unitName); long startTime = System.nanoTime(); long rem = unit.toNanos(timeout); do { try { exitValue(); return true; } catch (IllegalThreadStateException ex) { if (rem > 0) { SystemClock.sleep(Math.min(TimeUnit.NANOSECONDS.toMillis(rem) + 1, 100)); } } rem = unit.toNanos(timeout) - (System.nanoTime() - startTime); } while (rem > 0); return false; } private static class OutputTransferThread extends Thread { private final Process mProcess; private OutputStream mProcessOutputStream; @Nullable private volatile ParcelFileDescriptor mWriteSide; @NonNull private CountDownLatch mWaitForWriteSide; private OutputTransferThread(Process process) { super(); mProcess = process; mWaitForWriteSide = new CountDownLatch(1); setDaemon(true); } @NonNull public ParcelFileDescriptor getWriteSide() throws IOException, InterruptedException { mWaitForWriteSide.await(); ParcelFileDescriptor writeSide = mWriteSide; if (writeSide == null) { throw new IOException("Could not get the write side"); } return writeSide; } @Override public void run() { if (mProcessOutputStream == null) { mProcessOutputStream = mProcess.getOutputStream(); } try { do { if (mWaitForWriteSide.getCount() == 0) { mWaitForWriteSide = new CountDownLatch(1); } ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); ParcelFileDescriptor readSide = pipe[0]; mWriteSide = pipe[1]; mWaitForWriteSide.countDown(); try (InputStream in = new ParcelFileDescriptor.AutoCloseInputStream(readSide)) { byte[] buf = new byte[IoUtils.DEFAULT_BUFFER_SIZE]; int len; while ((len = in.read(buf)) > 0) { mProcessOutputStream.write(buf, 0, len); } } mProcessOutputStream.flush(); } while (!isInterrupted()); mProcessOutputStream.close(); } catch (IOException e) { Log.e("FD", "IOException when writing to out", e); mWaitForWriteSide.countDown(); } } } private static class InputTransferThread extends Thread { private final Process mProcess; private final boolean mErrorStream; @NonNull private final CountDownLatch mWaitForReadSide; @Nullable private volatile ParcelFileDescriptor mReadSide; InputTransferThread(Process process, boolean errorStream) { super(); mProcess = process; mErrorStream = errorStream; mWaitForReadSide = new CountDownLatch(1); setDaemon(true); } @NonNull public ParcelFileDescriptor getReadSide() throws IOException, InterruptedException { mWaitForReadSide.await(); ParcelFileDescriptor writeSide = mReadSide; if (writeSide == null) { throw new IOException("Could not get the write side"); } return writeSide; } @Override public void run() { try { ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); mReadSide = pipe[0]; ParcelFileDescriptor writeSide = pipe[1]; mWaitForReadSide.countDown(); try (OutputStream out = new ParcelFileDescriptor.AutoCloseOutputStream(writeSide); InputStream in = mErrorStream ? mProcess.getErrorStream() : mProcess.getInputStream()) { byte[] buf = new byte[IoUtils.DEFAULT_BUFFER_SIZE]; int len; while ((len = in.read(buf)) > 0) { out.write(buf, 0, len); } } } catch (IOException e) { Log.e("FD", "IOException when writing to out", e); mWaitForReadSide.countDown(); } } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/ipc/RemoteShellImpl.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.ipc; import android.os.ParcelFileDescriptor; import com.topjohnwu.superuser.Shell; import aosp.android.content.pm.StringParceledListSlice; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.IRemoteShell; import io.github.muntashirakon.AppManager.IShellResult; class RemoteShellImpl extends IRemoteShell.Stub { static { Shell.enableVerboseLogging = BuildConfig.DEBUG; Shell.setDefaultBuilder(Shell.Builder.create() .setFlags(Shell.FLAG_MOUNT_MASTER) .setTimeout(10)); } private final Shell.Job mJob; public RemoteShellImpl(String[] cmd) { mJob = Shell.cmd(cmd); } @Override public void addCommand(String[] commands) { mJob.add(commands); } @Override public void addInputStream(ParcelFileDescriptor inputStream) { mJob.add(new ParcelFileDescriptor.AutoCloseInputStream(inputStream)); } @Override public IShellResult exec() { Shell.Result result = mJob.exec(); return new IShellResult.Stub() { @Override public StringParceledListSlice getStdout() { return new StringParceledListSlice(result.getOut()); } @Override public StringParceledListSlice getStderr() { return new StringParceledListSlice(result.getErr()); } @Override public int getExitCode() { return result.getCode(); } @Override public boolean isSuccessful() { return result.isSuccess(); } }; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/ipc/RootService.java ================================================ // SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.ipc; import android.annotation.SuppressLint; import android.app.Service; import android.content.ComponentName; import android.content.Context; import android.content.ContextWrapper; import android.content.Intent; import android.content.ServiceConnection; import android.os.IBinder; import android.os.Messenger; import android.util.Log; import androidx.annotation.MainThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.topjohnwu.superuser.Shell; import com.topjohnwu.superuser.internal.UiThreadHandler; import java.io.ByteArrayOutputStream; import java.util.concurrent.Executor; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.runner.Runner; import io.github.muntashirakon.AppManager.servermanager.LocalServer; import io.github.muntashirakon.AppManager.settings.Ops; import io.github.muntashirakon.AppManager.utils.ContextUtils; /** * A remote root service using native Android Binder IPC. *

* Important: while developing an app with RootServices, modify the run/debug configuration and * check the "Always install with package manager" option if testing on Android 11+, or else the * code changes will not be reflected after Android Studio's deployment. *

* This class is almost a complete recreation of a bound service running in a root process. * Instead of using the original {@code Context.bindService(...)} methods to start and bind * to a service, use the provided static methods {@code RootService.bind(...)}. * Because the service will not run in the same process as your application, you have to use either * {@link Messenger} or AIDL to define the IPC interface for communication. Please read the * official documentations for more details. *

* Even though a {@code RootService} is a {@link Context} of your application, the ContextImpl * is not constructed in a normal way, so the functionality is much more limited compared * to the normal case. Be aware of this and do not expect all context methods to work. *

* All RootServices launched from the same process will run in the same root process. * A root service will be destroyed as soon as there are no clients bound to it. * This means all services will be destroyed immediately when the client process is terminated. * The library will NOT attempt to automatically restart and bind to a service after it was unbound. *

* Daemon Mode:
* If you want the service to run in the background independent from the application lifecycle, * launch the service in "Daemon Mode". Check the description of {@link #CATEGORY_DAEMON_MODE} * for instructions on how to do so. * All services running in "Daemon Mode" will run in a daemon process created per-package that * is separate from regular root services. This daemon process will be used across application * re-launches, and even across different users on the device. * A root service running in "Daemon Mode" will be destroyed when any client called * {@link #stop(Intent)}, or the root service itself called {@link #stopSelf()}. *

* A root service process, including the daemon process, will terminate under these conditions: *

    *
  • When the application is updated or deleted
  • *
  • When all services running in the process are destroyed * (after {@link #onDestroy()} is called)
  • *
* * @see Bound services * @see Android Interface Definition Language (AIDL) */ // Copyright 2020 John "topjohnwu" Wu public abstract class RootService extends ContextWrapper { /** * Launch the service in "Daemon Mode". *

* Add this category in the intent passed to {@link #bind(Intent, ServiceConnection)}, * {@link #bind(Intent, Executor, ServiceConnection)}, or * {@link #bindOrTask(Intent, Executor, ServiceConnection)} * to have the service launch in "Daemon Mode". * This category also has to be added in the intent passed to {@link #stop(Intent)} * and {@link #stopOrTask(Intent)} in order to refer to a daemon service instead of * a regular root service. */ public static final String CATEGORY_DAEMON_MODE = BuildConfig.APPLICATION_ID + ".DAEMON_MODE"; static final String TAG = RootService.class.getSimpleName(); /** * Bind to a root service, launching a new root process if needed. * * @param intent identifies the service to connect to. * @param executor callbacks on ServiceConnection will be called on this executor. * @param conn receives information as the service is started and stopped. * @see Context#bindService(Intent, int, Executor, ServiceConnection) */ @MainThread public static void bind( @NonNull Intent intent, @NonNull Executor executor, @NonNull ServiceConnection conn) { Shell.Task task = bindOrTask(intent, executor, conn); if (task != null) { Shell.EXECUTOR.execute(asRunnable(task)); } } /** * Bind to a root service, launching a new root process if needed. * * @param intent identifies the service to connect to. * @param conn receives information as the service is started and stopped. * @see Context#bindService(Intent, ServiceConnection, int) */ @MainThread public static void bind(@NonNull Intent intent, @NonNull ServiceConnection conn) { bind(intent, UiThreadHandler.executor, conn); } /** * Bind to a root service, creating a task to launch a new root process if needed. *

* If the application is already connected to a root process, binding will happen immediately * and this method will return {@code null}. Or else this method returns a {@link Shell.Task} * that has to be executed to launch the root process. Binding will only happen after the * developer has executed the returned task with {@link Shell#execTask(Shell.Task)}. * * @return the task to launch a root process. If there is no need to launch a new root * process, {@code null} is returned. * @see #bind(Intent, Executor, ServiceConnection) */ @MainThread @Nullable public static Shell.Task bindOrTask( @NonNull Intent intent, @NonNull Executor executor, @NonNull ServiceConnection conn) { return RootServiceManager.getInstance().createBindTask(intent, executor, conn); } /** * Unbind from a root service. * * @param conn the connection interface previously supplied to * {@link #bind(Intent, ServiceConnection)} * @see Context#unbindService(ServiceConnection) */ @MainThread public static void unbind(@NonNull ServiceConnection conn) { RootServiceManager.getInstance().unbind(conn); } /** * Force stop a root service, launching a new root process if needed. *

* This method is used to immediately stop a root service regardless of its state. * ONLY use this method to stop a daemon root service; for normal root services, please use * {@link #unbind(ServiceConnection)} instead as this method has to potentially launch * an additional root process to ensure daemon services are stopped. * * @param intent identifies the service to stop. */ @MainThread public static void stop(@NonNull Intent intent) { Shell.Task task = stopOrTask(intent); if (task != null) { Shell.EXECUTOR.execute(asRunnable(task)); } } /** * Force stop a root service, creating a task to launch a new root process if needed. *

* This method returns a {@link Shell.Task} that has to be executed to launch a * root process if necessary, or else {@code null} will be returned. * * @see #stop(Intent) */ @MainThread @Nullable public static Shell.Task stopOrTask(@NonNull Intent intent) { return RootServiceManager.getInstance().createStopTask(intent); } private static Runnable asRunnable(Shell.Task task) { return () -> { try { // Run task while fetching the entire command. ByteArrayOutputStream os = new ByteArrayOutputStream(); // This works because we do not need the other streams. task.run(os, null, null); // The whole command has now been fetched. String cmd = os.toString(); if (Ops.isDirectRoot()) { if (!Runner.runCommand(cmd).isSuccessful()) { Log.e(TAG, "Couldn't start service using root.", new Throwable()); } } else if (LocalServer.alive(ContextUtils.getContext())) { if (LocalServer.getInstance().runCommand(cmd).getStatusCode() != 0) { Log.e(TAG, "Couldn't start service using ADB.", new Throwable()); } } else { Log.e(TAG, "Unable to start service using an unsupported mode.", new Throwable()); } } catch (Throwable e) { Log.e(TAG, e.getMessage(), e); } }; } public RootService() { super(null); } @SuppressLint("RestrictedApi") @Override protected final void attachBaseContext(Context base) { super.attachBaseContext(onAttach(ContextUtils.getContextImpl(base))); RootServiceServer.getInstance(base).register(this); onCreate(); } /** * Called when the RootService is getting attached with a {@link Context}. * * @param base the context being attached. * @return the passed in context by default. */ @NonNull protected Context onAttach(@NonNull Context base) { return base; } /** * Return the component name that will be used for service lookup. *

* Overriding this method is only for very unusual use cases when a different * component name other than the actual class name is desired. * * @return the desired component name. */ @NonNull public ComponentName getComponentName() { return new ComponentName(this, getClass()); } @SuppressLint("RestrictedApi") @Override public final Context getApplicationContext() { return ContextUtils.rootContext; } /** * @see Service#onBind(Intent) */ abstract public IBinder onBind(@NonNull Intent intent); /** * @see Service#onCreate() */ public void onCreate() { } /** * @see Service#onUnbind(Intent) */ public boolean onUnbind(@NonNull Intent intent) { return false; } /** * @see Service#onRebind(Intent) */ public void onRebind(@NonNull Intent intent) { } /** * @see Service#onDestroy() */ public void onDestroy() { } /** * Force stop this root service. */ public final void stopSelf() { RootServiceServer.getInstance(this).selfStop(getComponentName()); } // Deprecated APIs /** * @deprecated use {@link #bindOrTask(Intent, Executor, ServiceConnection)} */ @MainThread @Nullable @Deprecated public static Runnable createBindTask( @NonNull Intent intent, @NonNull Executor executor, @NonNull ServiceConnection conn) { Shell.Task task = bindOrTask(intent, executor, conn); return task == null ? null : asRunnable(task); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/ipc/RootServiceManager.java ================================================ // SPDX-License-Identifier: Apache-2.0 package io.github.muntashirakon.AppManager.ipc; import static io.github.muntashirakon.AppManager.ipc.RootService.CATEGORY_DAEMON_MODE; import static io.github.muntashirakon.AppManager.server.common.ServerUtils.CMDLINE_START_DAEMON; import static io.github.muntashirakon.AppManager.server.common.ServerUtils.CMDLINE_START_SERVICE; import static io.github.muntashirakon.AppManager.server.common.ServerUtils.CMDLINE_STOP_SERVICE; import static io.github.muntashirakon.AppManager.utils.PackageUtils.PACKAGE_STAGING_DIRECTORY; import android.annotation.SuppressLint; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; import android.os.Build; import android.os.Bundle; import android.os.Debug; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.Messenger; import android.os.Process; import android.os.RemoteException; import android.util.ArrayMap; import android.util.Log; import android.util.Pair; import androidx.annotation.NonNull; import androidx.annotation.RestrictTo; import androidx.core.content.ContextCompat; import com.topjohnwu.superuser.Shell; import com.topjohnwu.superuser.ShellUtils; import com.topjohnwu.superuser.internal.Utils; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.concurrent.Executor; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.compat.ManifestCompat; import io.github.muntashirakon.AppManager.server.common.IRootServiceManager; import io.github.muntashirakon.AppManager.settings.Ops; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.FileUtils; import io.github.muntashirakon.AppManager.utils.PackageUtils; /** * Runs in the non-root (client) process. *

* Starts the root process and manages connections with the remote process. */ // Copyright 2021 John "topjohnwu" Wu @RestrictTo(RestrictTo.Scope.LIBRARY) public class RootServiceManager implements Handler.Callback { static final String TAG = RootServiceManager.class.getSimpleName(); static final String LOGGING_ENV = "AM_VERBOSE_LOGGING"; static final String DEBUG_ENV = "AM_DEBUGGER"; static final String CLASSPATH_ENV = "CLASSPATH"; static final int MSG_STOP = 1; private static final String BUNDLE_BINDER_KEY = "binder"; private static final String INTENT_BUNDLE_KEY = "extra.bundle"; private static final String INTENT_DAEMON_KEY = "extra.daemon"; private static final String RECEIVER_BROADCAST = BuildConfig.APPLICATION_ID + ".RECEIVER_BROADCAST"; private static final String API_27_DEBUG = "-Xrunjdwp:transport=dt_android_adb,suspend=n,server=y " + "-Xcompiler-option --debuggable"; private static final String API_28_DEBUG = "-XjdwpProvider:adbconnection -XjdwpOptions:suspend=n,server=y " + "-Xcompiler-option --debuggable"; private static final String JVMTI_ERROR = " \n" + "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + "! Warning: JVMTI agent is enabled. Please enable the !\n" + "! 'Always install with package manager' option in !\n" + "! Android Studio. For more details and information, !\n" + "! check out RootService's Javadoc. !\n" + "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n"; private static final int REMOTE_EN_ROUTE = 1 << 0; private static final int DAEMON_EN_ROUTE = 1 << 1; private static final int RECEIVER_REGISTERED = 1 << 2; private static final String IPCMAIN_CLASSNAME = "io.github.muntashirakon.AppManager.server.RootServiceMain"; private static RootServiceManager sInstance; public static RootServiceManager getInstance() { if (sInstance == null) { sInstance = new RootServiceManager(); } return sInstance; } @SuppressLint("WrongConstant") static Intent getBroadcastIntent(IBinder binder, boolean isDaemon) { Bundle bundle = new Bundle(); bundle.putBinder(BUNDLE_BINDER_KEY, binder); return new Intent(RECEIVER_BROADCAST) .setPackage(ContextUtils.rootContext.getPackageName()) .addFlags(HiddenAPIs.FLAG_RECEIVER_FROM_SHELL) .putExtra(INTENT_DAEMON_KEY, isDaemon) .putExtra(INTENT_BUNDLE_KEY, bundle); } private static void enforceMainThread() { if (!ShellUtils.onMainThread()) { throw new IllegalStateException("This method can only be called on the main thread"); } } @NonNull private static ServiceKey parseIntent(Intent intent) { ComponentName name = intent.getComponent(); if (name == null) { throw new IllegalArgumentException("The intent does not have a component set"); } if (!name.getPackageName().equals(ContextUtils.getContext().getPackageName())) { throw new IllegalArgumentException("RootServices outside of the app are not supported"); } return new ServiceKey(name, intent.hasCategory(CATEGORY_DAEMON_MODE)); } private RemoteProcess mRemote; private RemoteProcess mDaemon; private int mFlags = 0; private final List mPendingTasks = new ArrayList<>(); private final Map mServices = new ArrayMap<>(); private final Map mConnections = new ArrayMap<>(); private RootServiceManager() { } @SuppressLint("RestrictedApi") private Shell.Task startRootProcess(ComponentName name, String action) { Context context = ContextUtils.getContext(); if ((mFlags & RECEIVER_REGISTERED) == 0) { // Register receiver to receive binder from root process IntentFilter filter = new IntentFilter(RECEIVER_BROADCAST); // Guard the receiver behind permission UPDATE_APP_OPS_STATS. This permission // is not obtainable by normal apps, making the receiver effectively non-exported, // but will allow any root/ADB/system process to send broadcast message. ContextCompat.registerReceiver(context, new ServiceReceiver(), filter, ManifestCompat.permission.UPDATE_APP_OPS_STATS, null, ContextCompat.RECEIVER_EXPORTED); mFlags |= RECEIVER_REGISTERED; } return (stdin, stdout, stderr) -> { if (Utils.hasStartupAgents(context)) { Log.e(TAG, JVMTI_ERROR); } String mainJarName = "main.jar"; Context ctx = ContextUtils.getContext(); Context de = ContextUtils.getDeContext(ctx); File mainJar; try { mainJar = new File(FileUtils.getExternalCachePath(de), mainJarName); } catch (IOException e) { throw new IllegalStateException("External directory unavailable.", e); } File stagingMainJar = new File(PACKAGE_STAGING_DIRECTORY, mainJarName); // Dump main.jar as trampoline try (InputStream in = context.getResources().getAssets().open("main.jar"); OutputStream out = new FileOutputStream(mainJar)) { Utils.pump(in, out); } FileUtils.chmod644(mainJar); StringBuilder env = new StringBuilder(); String params = getParams(env); String cmd = getRunnerScript(env, mainJar, stagingMainJar, name, action, params); Log.d(TAG, cmd); // Write command to stdin byte[] bytes = cmd.getBytes(StandardCharsets.UTF_8); stdin.write(bytes); stdin.write('\n'); stdin.flush(); // Since all output for the command is redirected to /dev/null and // the command runs in the background, we don't need to wait and // can just return. }; } @SuppressLint("RestrictedApi") @NonNull private static String getParams(StringBuilder env) { String params = ""; if (Utils.vLog()) { env.append(LOGGING_ENV + "=1 "); } // Only support debugging on SDK >= 27 if (Build.VERSION.SDK_INT >= 27 && Debug.isDebuggerConnected()) { env.append(DEBUG_ENV + "=1 "); // Reference of the params to start jdwp: // https://developer.android.com/ndk/guides/wrap-script#debugging_when_using_wrapsh if (Build.VERSION.SDK_INT == 27) { params = API_27_DEBUG; } else { params = API_28_DEBUG; } } // Disable image dex2oat as it can be quite slow in some ROMs if triggered return params + " -Xnoimage-dex2oat"; } @NonNull private String getRunnerScript(@NonNull StringBuilder env, @NonNull File mainJar, @NonNull File stagingMainJar, @NonNull ComponentName serviceName, @NonNull String action, @NonNull String debugParams) { // We cannot readlink /proc/self/exe on old kernels @SuppressLint("RestrictedApi") String execFile = "/system/bin/app_process" + (Utils.isProcess64Bit() ? "64" : "32"); String packageStagingCommand; env.append(CLASSPATH_ENV).append("="); if (Ops.hasRoot()) { // Avoid using the package staging directory env.append(mainJar); packageStagingCommand = ""; } else if (!Ops.isSystem()) { // Use package staging directory env.append(stagingMainJar); packageStagingCommand = PackageUtils.ensurePackageStagingDirectoryCommand() + // Copy to main.jar to package staging directory String.format(Locale.ROOT, " && cp %s %s && ", mainJar, PACKAGE_STAGING_DIRECTORY) + // Change permission of the main.jar String.format(Locale.ROOT, "chmod 755 %s && chown shell:shell %s && ", stagingMainJar, stagingMainJar); } else { // System can't use package staging directory env.append(mainJar); packageStagingCommand = ""; } env.append(" "); return (packageStagingCommand + String.format(Locale.ROOT, "(%s %s %s /system/bin %s %s '%s' %d %s 2>&1)&", env, // Environments execFile, // Executable debugParams, // Debug parameters getNiceNameArg(action), // Process name IPCMAIN_CLASSNAME, // Java command serviceName.flattenToString(), // args[0] Process.myUid(), // args[1] action)); // args[2] } @NonNull private String getNiceNameArg(@NonNull String action) { switch (action) { case CMDLINE_START_SERVICE: return String.format(Locale.ROOT, "--nice-name=%s:priv:%d", BuildConfig.APPLICATION_ID, Process.myUid() / 100000); case CMDLINE_START_DAEMON: return "--nice-name=" + BuildConfig.APPLICATION_ID + ":priv:daemon"; default: return ""; } } // Returns null if binding is done synchronously, or else return key private ServiceKey bindInternal(Intent intent, Executor executor, ServiceConnection conn) { enforceMainThread(); // Local cache ServiceKey key = parseIntent(intent); RemoteServiceRecord s = mServices.get(key); if (s != null) { mConnections.put(conn, new ConnectionRecord(s, executor)); s.refCount++; IBinder binder = s.binder; executor.execute(() -> conn.onServiceConnected(key.getName(), binder)); return null; } RemoteProcess p = key.isDaemon() ? mDaemon : mRemote; if (p == null) return key; try { IBinder binder = p.mgr.bind(intent); if (binder != null) { s = new RemoteServiceRecord(key, binder, p); mConnections.put(conn, new ConnectionRecord(s, executor)); mServices.put(key, s); executor.execute(() -> conn.onServiceConnected(key.getName(), binder)); } else if (Build.VERSION.SDK_INT >= 28) { executor.execute(() -> conn.onNullBinding(key.getName())); } } catch (RemoteException e) { Log.e(TAG, e.getMessage(), e); p.binderDied(); return key; } return null; } public Shell.Task createBindTask(Intent intent, Executor executor, ServiceConnection conn) { ServiceKey key = bindInternal(intent, executor, conn); if (key != null) { mPendingTasks.add(() -> bindInternal(intent, executor, conn) == null); int mask = key.isDaemon() ? DAEMON_EN_ROUTE : REMOTE_EN_ROUTE; if ((mFlags & mask) == 0) { mFlags |= mask; String action = key.isDaemon() ? CMDLINE_START_DAEMON : CMDLINE_START_SERVICE; return startRootProcess(key.getName(), action); } } return null; } public void unbind(@NonNull ServiceConnection conn) { enforceMainThread(); ConnectionRecord r = mConnections.remove(conn); if (r != null) { RemoteServiceRecord s = r.getService(); s.refCount--; if (s.refCount == 0) { // Actually close the service mServices.remove(s.key); try { s.host.mgr.unbind(s.key.getName()); } catch (RemoteException e) { Log.e(TAG, e.getMessage(), e); } } } } private void dropConnections(Predicate predicate) { Iterator> it = mConnections.entrySet().iterator(); while (it.hasNext()) { Map.Entry e = it.next(); ConnectionRecord r = e.getValue(); if (predicate.eval(r.getService())) { r.disconnect(e.getKey()); it.remove(); } } } private void onServiceStopped(ServiceKey key) { RemoteServiceRecord s = mServices.remove(key); if (s != null) dropConnections(s::equals); } public Shell.Task createStopTask(Intent intent) { enforceMainThread(); ServiceKey key = parseIntent(intent); RemoteProcess p = key.isDaemon() ? mDaemon : mRemote; if (p == null) { if (key.isDaemon()) { // Start a new root process to stop daemon return startRootProcess(key.getName(), CMDLINE_STOP_SERVICE); } return null; } try { p.mgr.stop(key.getName(), -1); } catch (RemoteException e) { Log.e(TAG, e.getMessage(), e); } onServiceStopped(key); return null; } @Override public boolean handleMessage(@NonNull Message msg) { if (msg.what == MSG_STOP) { onServiceStopped(new ServiceKey((ComponentName) msg.obj, msg.arg1 != 0)); } return false; } private static class ServiceKey extends Pair { ServiceKey(ComponentName name, boolean isDaemon) { super(name, isDaemon); } ComponentName getName() { return first; } boolean isDaemon() { return second; } } private static class ConnectionRecord extends Pair { ConnectionRecord(RemoteServiceRecord s, Executor e) { super(s, e); } RemoteServiceRecord getService() { return first; } void disconnect(ServiceConnection conn) { second.execute(() -> conn.onServiceDisconnected(first.key.getName())); } } private class RemoteProcess extends BinderHolder { final IRootServiceManager mgr; RemoteProcess(IRootServiceManager s) throws RemoteException { super(s.asBinder()); mgr = s; } @Override protected void onBinderDied() { if (mRemote == this) mRemote = null; if (mDaemon == this) mDaemon = null; Iterator sit = mServices.values().iterator(); while (sit.hasNext()) { if (sit.next().host == this) { sit.remove(); } } dropConnections(s -> s.host == this); } } private static class RemoteServiceRecord { final ServiceKey key; final IBinder binder; final RemoteProcess host; int refCount = 1; RemoteServiceRecord(ServiceKey key, IBinder binder, RemoteProcess host) { this.key = key; this.binder = binder; this.host = host; } } private class ServiceReceiver extends BroadcastReceiver { private final Messenger m; ServiceReceiver() { // Create messenger to receive service stop notification Handler h = new Handler(Looper.getMainLooper(), RootServiceManager.this); m = new Messenger(h); } @Override public void onReceive(Context context, Intent intent) { Bundle bundle = intent.getBundleExtra(INTENT_BUNDLE_KEY); if (bundle == null) return; IBinder binder = bundle.getBinder(BUNDLE_BINDER_KEY); if (binder == null) return; IRootServiceManager mgr = IRootServiceManager.Stub.asInterface(binder); try { mgr.connect(m.getBinder()); RemoteProcess p = new RemoteProcess(mgr); if (intent.getBooleanExtra(INTENT_DAEMON_KEY, false)) { mDaemon = p; mFlags &= ~DAEMON_EN_ROUTE; } else { mRemote = p; mFlags &= ~REMOTE_EN_ROUTE; } for (int i = mPendingTasks.size() - 1; i >= 0; --i) { if (mPendingTasks.get(i).run()) { mPendingTasks.remove(i); } } } catch (RemoteException e) { Log.e(TAG, e.getMessage(), e); } } } private interface BindTask { boolean run(); } private interface Predicate { boolean eval(RemoteServiceRecord s); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/ipc/RootServiceServer.java ================================================ // SPDX-License-Identifier: Apache-2.0 package io.github.muntashirakon.AppManager.ipc; import android.annotation.SuppressLint; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.os.Build; import android.os.Debug; import android.os.FileObserver; import android.os.IBinder; import android.os.Message; import android.os.Messenger; import android.os.RemoteException; import android.os.UserHandle; import android.util.ArrayMap; import android.util.ArraySet; import android.util.SparseArray; import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; import com.topjohnwu.superuser.Shell; import com.topjohnwu.superuser.internal.UiThreadHandler; import com.topjohnwu.superuser.internal.Utils; import java.io.File; import java.lang.reflect.Constructor; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import io.github.muntashirakon.AppManager.server.common.IRootServiceManager; import io.github.muntashirakon.AppManager.server.common.ServerUtils; import io.github.muntashirakon.AppManager.utils.ContextUtils; import static io.github.muntashirakon.AppManager.ipc.RootServiceManager.DEBUG_ENV; import static io.github.muntashirakon.AppManager.ipc.RootServiceManager.LOGGING_ENV; import static io.github.muntashirakon.AppManager.ipc.RootServiceManager.MSG_STOP; /** * Runs in the root (server) process. *

* Manages the lifecycle of RootServices and the root process. */ // Copyright 2021 John "topjohnwu" Wu @RestrictTo(RestrictTo.Scope.LIBRARY) @SuppressLint("RestrictedApi") public class RootServiceServer extends IRootServiceManager.Stub implements Runnable { public static final String TAG = RootServiceServer.class.getSimpleName(); @SuppressLint("StaticFieldLeak") private static RootServiceServer sInstance; public static RootServiceServer getInstance(Context context) { if (sInstance == null) { sInstance = new RootServiceServer(context); } return sInstance; } @SuppressWarnings("FieldCanBeLocal") private final FileObserver mObserver; /* A strong reference is required */ private final Map mServices = new ArrayMap<>(); private final SparseArray mClients = new SparseArray<>(); private final boolean mIsDaemon; private final Context mContext; @SuppressWarnings("rawtypes") private RootServiceServer(Context context) { Shell.enableVerboseLogging = System.getenv(LOGGING_ENV) != null; mContext = context; ContextUtils.rootContext = context; // Wait for debugger to attach if needed if (System.getenv(DEBUG_ENV) != null) { // ActivityThread.attach(true, 0) will set this to system_process HiddenAPIs.setAppName(context.getPackageName() + ":priv"); Utils.log(TAG, "Waiting for debugger to be attached..."); // For some reason Debug.waitForDebugger() won't work, manual spin lock... while (!Debug.isDebuggerConnected()) { try { // noinspection BusyWait Thread.sleep(200); } catch (InterruptedException ignored) { } } Utils.log(TAG, "Debugger attached!"); } mObserver = new AppObserver(new File(context.getPackageCodePath())); mObserver.startWatching(); if (context instanceof Callable) { try { Object[] objs = (Object[]) ((Callable) context).call(); mIsDaemon = (boolean) objs[1]; if (mIsDaemon) { // Register ourselves as system service HiddenAPIs.addService(ServerUtils.getServiceName(context.getPackageName()), this); } broadcast((int) objs[0]); } catch (Exception e) { throw new RuntimeException(e); } } else { throw new IllegalArgumentException("Expected Context to be Callable"); } if (!mIsDaemon) { // Terminate the process if idle for 10 seconds, UiThreadHandler.handler.postDelayed(this, 10 * 1000); } } @Override public void run() { if (mClients.size() == 0) { exit("No active clients"); } } @Override public void connect(IBinder binder) { int uid = getCallingUid(); UiThreadHandler.run(() -> connectInternal(uid, binder)); } private void connectInternal(int uid, IBinder binder) { if (mClients.get(uid) != null) return; try { mClients.put(uid, new ClientProcess(binder, uid)); UiThreadHandler.handler.removeCallbacks(this); } catch (RemoteException e) { Utils.err(TAG, e); } } @Override public void broadcast(int uid) { // Use the UID argument iff caller is root uid = getCallingUid() == 0 ? uid : getCallingUid(); Utils.log(TAG, "broadcast to uid=" + uid); Intent intent = RootServiceManager.getBroadcastIntent(this, mIsDaemon); if (Build.VERSION.SDK_INT >= 24) { UserHandle h = UserHandle.getUserHandleForUid(uid); mContext.sendBroadcastAsUser(intent, h); } else { mContext.sendBroadcast(intent); } } @Override public IBinder bind(Intent intent) { IBinder[] b = new IBinder[1]; int uid = getCallingUid(); UiThreadHandler.runAndWait(() -> { try { b[0] = bindInternal(uid, intent); } catch (Exception e) { Utils.err(TAG, e); } }); return b[0]; } @Override public void unbind(ComponentName name) { int uid = getCallingUid(); UiThreadHandler.run(() -> { Utils.log(TAG, name.getClassName() + " unbind"); unbindService(uid, name); }); } @Override public void stop(ComponentName name, int uid) { // Use the UID argument iff caller is root int clientUid = getCallingUid() == 0 ? uid : getCallingUid(); UiThreadHandler.run(() -> { Utils.log(TAG, name.getClassName() + " stop"); unbindService(-1, name); // If we aren't killed yet, send another broadcast broadcast(clientUid); }); } public void selfStop(ComponentName name) { UiThreadHandler.run(() -> { Utils.log(TAG, name.getClassName() + " selfStop"); unbindService(-1, name); }); } public void register(RootService service) { ServiceRecord s = new ServiceRecord(service); mServices.put(service.getComponentName(), s); } @Nullable private IBinder bindInternal(int uid, Intent intent) throws Exception { ClientProcess c = mClients.get(uid); if (c == null) return null; ComponentName name = intent.getComponent(); ServiceRecord s = mServices.get(name); if (s == null) { Class clz = mContext.getClassLoader().loadClass(name.getClassName()); Constructor ctor = clz.getDeclaredConstructor(); ctor.setAccessible(true); HiddenAPIs.attachBaseContext(ctor.newInstance(), mContext); // RootService should be registered after attachBaseContext s = mServices.get(name); if (s == null) { return null; } } if (s.binder != null) { Utils.log(TAG, name.getClassName() + " rebind"); if (s.rebind) s.service.onRebind(s.intent); } else { Utils.log(TAG, name.getClassName() + " bind"); s.binder = s.service.onBind(intent); s.intent = intent.cloneFilter(); } s.users.add(uid); return s.binder; } private void unbindInternal(ServiceRecord s, int uid, Runnable onDestroy) { boolean hadUsers = !s.users.isEmpty(); s.users.remove(uid); if (uid < 0 || s.users.isEmpty()) { if (hadUsers) { s.rebind = s.service.onUnbind(s.intent); } if (uid < 0 || !mIsDaemon) { s.service.onDestroy(); onDestroy.run(); // Notify all other users for (int user : s.users) { ClientProcess c = mClients.get(user); if (c == null) continue; Message msg = Message.obtain(); msg.what = MSG_STOP; msg.arg1 = mIsDaemon ? 1 : 0; msg.obj = s.intent.getComponent(); try { c.m.send(msg); } catch (RemoteException e) { Utils.err(TAG, e); } finally { msg.recycle(); } } } } if (mServices.isEmpty()) { exit("No active services"); } } private void unbindService(int uid, ComponentName name) { ServiceRecord s = mServices.get(name); if (s == null) return; unbindInternal(s, uid, () -> mServices.remove(name)); } private void unbindServices(int uid) { Iterator> it = mServices.entrySet().iterator(); while (it.hasNext()) { ServiceRecord s = it.next().getValue(); if (uid < 0) { // App is updated/deleted, all clients will get killed anyway, // no need to notify anyone. s.users.clear(); } unbindInternal(s, uid, it::remove); } } private void exit(String reason) { Utils.log(TAG, "Terminate process: " + reason); System.exit(0); } private class AppObserver extends FileObserver { private final String mName; AppObserver(File path) { super(path.getParent(), CREATE | DELETE | DELETE_SELF | MOVED_TO | MOVED_FROM); Utils.log(TAG, "Start monitoring: " + path.getParent()); mName = path.getName(); } @Override public void onEvent(int event, @Nullable String path) { // App APK update, force close the root process if (event == DELETE_SELF || mName.equals(path)) { exit("Package updated"); } } } private class ClientProcess extends BinderHolder { final Messenger m; final int uid; ClientProcess(IBinder b, int uid) throws RemoteException { super(b); m = new Messenger(b); this.uid = uid; } @Override protected void onBinderDied() { Utils.log(TAG, "Client process terminated, uid=" + uid); mClients.remove(uid); unbindServices(uid); } } private static class ServiceRecord { final RootService service; final Set users = Build.VERSION.SDK_INT >= 23 ? new ArraySet<>() : new HashSet<>(); Intent intent; IBinder binder; boolean rebind; ServiceRecord(RootService s) { service = s; } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/ipc/SerialExecutorService.java ================================================ // SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.ipc; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.List; import java.util.concurrent.AbstractExecutorService; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import static com.topjohnwu.superuser.Shell.EXECUTOR; // Copyright 2020 John "topjohnwu" Wu public class SerialExecutorService extends AbstractExecutorService implements Callable { private boolean mIsShutdown = false; private final ArrayDeque mTasks = new ArrayDeque<>(); private FutureTask mScheduleTask = null; @Override public Void call() { for (; ; ) { Runnable task; synchronized (this) { if ((task = mTasks.poll()) == null) { mScheduleTask = null; return null; } } task.run(); } } @Override public synchronized void execute(Runnable r) { if (mIsShutdown) { throw new RejectedExecutionException( "Task " + r.toString() + " rejected from " + this); } mTasks.offer(r); if (mScheduleTask == null) { mScheduleTask = new FutureTask<>(this); EXECUTOR.execute(mScheduleTask); } } @Override public synchronized void shutdown() { mIsShutdown = true; mTasks.clear(); } @Override public synchronized List shutdownNow() { mIsShutdown = true; if (mScheduleTask != null) mScheduleTask.cancel(true); try { return new ArrayList<>(mTasks); } finally { mTasks.clear(); } } @Override public synchronized boolean isShutdown() { return mIsShutdown; } @Override public synchronized boolean isTerminated() { return mIsShutdown && mScheduleTask == null; } @Override public synchronized boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { if (mScheduleTask == null) return true; try { mScheduleTask.get(timeout, unit); } catch (TimeoutException e) { return false; } catch (ExecutionException ignored) { } return true; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/ipc/ServiceConnectionWrapper.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.ipc; import android.content.ComponentName; import android.content.Intent; import android.content.ServiceConnection; import android.os.IBinder; import android.os.RemoteException; import androidx.annotation.MainThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import java.util.Objects; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.misc.NoOps; import io.github.muntashirakon.AppManager.utils.ThreadUtils; class ServiceConnectionWrapper { public static final String TAG = ServiceConnectionWrapper.class.getSimpleName(); @Nullable private IBinder mIBinder; @Nullable private CountDownLatch mServiceBoundWatcher; private class ServiceConnectionImpl implements ServiceConnection { @Override public void onServiceConnected(ComponentName name, IBinder service) { Log.d(TAG, "service onServiceConnected: %s", name); mIBinder = service; onResponseReceived(); } @Override public void onServiceDisconnected(ComponentName name) { Log.d(TAG, "service onServiceDisconnected: %s", name); mIBinder = null; onResponseReceived(); } @Override public void onBindingDied(ComponentName name) { Log.d(TAG, "service onBindingDied: %s", name); mIBinder = null; onResponseReceived(); } @Override public void onNullBinding(ComponentName name) { Log.d(TAG, "service onNullBinding: %s", name); mIBinder = null; onResponseReceived(); } private void onResponseReceived() { if (mServiceBoundWatcher != null) { // Should never be null mServiceBoundWatcher.countDown(); } else throw new RuntimeException("Service watcher should never be null!"); } } @NonNull private final ComponentName mComponentName; @NonNull private final ServiceConnectionImpl mServiceConnection; public ServiceConnectionWrapper(@NonNull String pkgName, @NonNull String className) { this(new ComponentName(pkgName, className)); } public ServiceConnectionWrapper(@NonNull ComponentName cn) { mComponentName = cn; mServiceConnection = new ServiceConnectionImpl(); } @NonNull public IBinder getService() throws RemoteException { if (!isBinderActive()) { throw new RemoteException("Binder not running."); } return Objects.requireNonNull(mIBinder); } @NonNull @NoOps(used = true) public IBinder bindService() throws RemoteException { synchronized (mServiceConnection) { if (!isBinderActive()) { startDaemon(); } return getService(); } } @MainThread public void unbindService() { synchronized (mServiceConnection) { RootService.unbind(mServiceConnection); } } @WorkerThread private void startDaemon() { synchronized (mServiceConnection) { if (isBinderActive()) { Log.d(TAG, "Binder is already active?"); return; } mServiceBoundWatcher = new CountDownLatch(1); Log.d(TAG, "Launching service..."); Intent intent = new Intent(); intent.setComponent(mComponentName); ThreadUtils.postOnMainThread(() -> { if (mIBinder != null) { RootService.stop(intent); } RootService.bind(intent, mServiceConnection); }); // Wait for service to be bound try { mServiceBoundWatcher.await(45, TimeUnit.SECONDS); } catch (InterruptedException e) { Log.e(TAG, "Service watcher interrupted."); } } } @WorkerThread public void stopDaemon() { Intent intent = new Intent(); intent.setComponent(mComponentName); ThreadUtils.postOnMainThread(() -> RootService.stop(intent)); mIBinder = null; } boolean isBinderActive() { return mIBinder != null && mIBinder.pingBinder(); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/ipc/ServiceNotFoundException.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.ipc; public class ServiceNotFoundException extends RuntimeException { public ServiceNotFoundException() { super(); } public ServiceNotFoundException(String name) { super(name); } public ServiceNotFoundException(String name, Exception cause) { super(name, cause); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/ipc/package.html ================================================

Java classes except IPCUtils and RemoteProcess defined in the package must be executed via IPC. Main app can only access them.

================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/ipc/ps/ProcessEntry.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.ipc.ps; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.os.ParcelCompat; import java.util.Objects; public class ProcessEntry implements Parcelable { public int pid; public int ppid; public int priority; public int niceness; public long instructionPointer; public long virtualMemorySize; public long residentSetSize; public long sharedMemory; public int processGroupId; public int majorPageFaults; public int minorPageFaults; public int realTimePriority; public int schedulingPolicy; public int cpu; public int threadCount; public int tty; @Nullable public String seLinuxPolicy; public String name; public ProcessUsers users; public long cpuTimeConsumed; public long cCpuTimeConsumed; public long elapsedTime; public String processState; public String processStatePlus; ProcessEntry() { } protected ProcessEntry(@NonNull Parcel in) { pid = in.readInt(); ppid = in.readInt(); priority = in.readInt(); niceness = in.readInt(); instructionPointer = in.readLong(); virtualMemorySize = in.readLong(); residentSetSize = in.readLong(); sharedMemory = in.readLong(); processGroupId = in.readInt(); majorPageFaults = in.readInt(); minorPageFaults = in.readInt(); realTimePriority = in.readInt(); schedulingPolicy = in.readInt(); cpu = in.readInt(); threadCount = in.readInt(); tty = in.readInt(); seLinuxPolicy = in.readString(); name = in.readString(); users = Objects.requireNonNull(ParcelCompat.readParcelable(in, ProcessUsers.class.getClassLoader(), ProcessUsers.class)); cpuTimeConsumed = in.readLong(); cCpuTimeConsumed = in.readLong(); elapsedTime = in.readLong(); processState = in.readString(); processStatePlus = in.readString(); } public static final Creator CREATOR = new Creator() { @Override @NonNull public ProcessEntry createFromParcel(Parcel in) { return new ProcessEntry(in); } @Override @NonNull public ProcessEntry[] newArray(int size) { return new ProcessEntry[size]; } }; @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeInt(pid); dest.writeInt(ppid); dest.writeInt(priority); dest.writeInt(niceness); dest.writeLong(instructionPointer); dest.writeLong(virtualMemorySize); dest.writeLong(residentSetSize); dest.writeLong(sharedMemory); dest.writeInt(processGroupId); dest.writeInt(majorPageFaults); dest.writeInt(minorPageFaults); dest.writeInt(realTimePriority); dest.writeInt(schedulingPolicy); dest.writeInt(cpu); dest.writeInt(threadCount); dest.writeInt(tty); dest.writeString(seLinuxPolicy); dest.writeString(name); dest.writeParcelable(users, flags); dest.writeLong(cpuTimeConsumed); dest.writeLong(cCpuTimeConsumed); dest.writeLong(elapsedTime); dest.writeString(processState); dest.writeString(processStatePlus); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/ipc/ps/ProcessUsers.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.ipc.ps; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; public class ProcessUsers implements Parcelable { public final int realUid; public final int realGid; public final int effectiveUid; public final int effectiveGid; public final int savedSetUid; public final int savedSetGid; public final int fsUid; public final int fsGid; ProcessUsers(@Nullable String uidLine, @Nullable String gidLine) { if (uidLine == null || gidLine == null) { throw new IllegalArgumentException("UID/GID must be non null"); } String[] uids = uidLine.split("\\s+"); String[] gids = gidLine.split("\\s+"); if (uids.length != gids.length && uids.length >= 4) { throw new IllegalArgumentException("Invalid UID/GID.\nUid: " + uidLine + "\nGid: " + gidLine); } // Set uids realUid = Integer.decode(uids[0].trim()); effectiveUid = Integer.decode(uids[1].trim()); savedSetUid = Integer.decode(uids[2].trim()); fsUid = Integer.decode(uids[3].trim()); // Set gids realGid = Integer.decode(gids[0].trim()); effectiveGid = Integer.decode(gids[1].trim()); savedSetGid = Integer.decode(gids[2].trim()); fsGid = Integer.decode(gids[3].trim()); } protected ProcessUsers(@NonNull Parcel in) { realUid = in.readInt(); realGid = in.readInt(); effectiveUid = in.readInt(); effectiveGid = in.readInt(); savedSetUid = in.readInt(); savedSetGid = in.readInt(); fsUid = in.readInt(); fsGid = in.readInt(); } public static final Creator CREATOR = new Creator() { @Override @NonNull public ProcessUsers createFromParcel(Parcel in) { return new ProcessUsers(in); } @Override @NonNull public ProcessUsers[] newArray(int size) { return new ProcessUsers[size]; } }; @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeInt(realUid); dest.writeInt(realGid); dest.writeInt(effectiveUid); dest.writeInt(effectiveGid); dest.writeInt(savedSetUid); dest.writeInt(savedSetGid); dest.writeInt(fsUid); dest.writeInt(fsGid); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/ipc/ps/Ps.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.ipc.ps; import android.text.TextUtils; import android.util.Log; import androidx.annotation.AnyThread; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.annotation.WorkerThread; import java.util.ArrayList; import io.github.muntashirakon.AppManager.utils.CpuUtils; import io.github.muntashirakon.AppManager.utils.Utils; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; import io.github.muntashirakon.proc.ProcFs; import io.github.muntashirakon.proc.ProcMemStat; import io.github.muntashirakon.proc.ProcStat; import io.github.muntashirakon.proc.ProcStatus; /** * This is a generic Java-way of parsing processes from /proc. This is a work in progress and by no means perfect. To * create this class, I extensively followed the documentation located at https://www.kernel.org/doc/Documentation/filesystems/proc.txt. */ public final class Ps { public static final String TAG = Ps.class.getSimpleName(); @NonNull private final ProcFs mProcFs; @GuardedBy("processEntries") private final ArrayList mProcessEntries = new ArrayList<>(256); private long mUptime; private long mClockTicks; public Ps() { this(Paths.get("/proc")); } @VisibleForTesting public Ps(@NonNull Path procPath) { mProcFs = new ProcFs(procPath); } @AnyThread @GuardedBy("processEntries") @NonNull public ArrayList getProcesses() { synchronized (mProcessEntries) { return mProcessEntries; } } @WorkerThread @GuardedBy("processEntries") public void loadProcesses() { synchronized (mProcessEntries) { mProcessEntries.clear(); mUptime = mProcFs.getUptime() / 1000; if (!Utils.isRoboUnitTest()) { mClockTicks = CpuUtils.getClockTicksPerSecond(); } else mClockTicks = 100; // To prevent error due to native library // Get process info for each PID for (int pid : mProcFs.getPids()) { ProcItem procItem = new ProcItem(); ProcStat procStat = mProcFs.getStat(pid); ProcMemStat procMemStat = mProcFs.getMemStat(pid); ProcStatus procStatus = mProcFs.getStatus(pid); if (procStat == null) { Log.w(TAG, "Could not read /proc/" + pid + "/stat"); continue; } if (procMemStat == null) { Log.w(TAG, "Could not read /proc/" + pid + "/statm"); continue; } if (procStatus == null) { Log.w(TAG, "Could not read /proc/" + pid + "/status"); continue; } procItem.stat = procStat; procItem.memStat = procMemStat; procItem.status = procStatus; procItem.name = mProcFs.getCmdline(pid); procItem.sepol = mProcFs.getCurrentContext(pid); procItem.wchan = mProcFs.getWchan(pid); mProcessEntries.add(newProcess(procItem)); } } } @NonNull private ProcessEntry newProcess(@NonNull ProcItem procItem) { ProcessEntry processEntry = new ProcessEntry(); processEntry.pid = procItem.stat.getInteger(ProcStat.STAT_PID); processEntry.ppid = procItem.stat.getInteger(ProcStat.STAT_PPID); processEntry.priority = procItem.stat.getInteger(ProcStat.STAT_PRIORITY); processEntry.niceness = procItem.stat.getInteger(ProcStat.STAT_NICE); processEntry.instructionPointer = procItem.stat.getLong(ProcStat.STAT_EIP); processEntry.virtualMemorySize = procItem.stat.getLong(ProcStat.STAT_VSIZE); processEntry.residentSetSize = procItem.stat.getLong(ProcStat.STAT_RSS); processEntry.sharedMemory = procItem.memStat.getLong(ProcMemStat.MEM_STAT_SHARED); processEntry.processGroupId = procItem.stat.getInteger(ProcStat.STAT_PGRP); processEntry.majorPageFaults = procItem.stat.getInteger(ProcStat.STAT_MAJ_FLT); processEntry.minorPageFaults = procItem.stat.getInteger(ProcStat.STAT_MIN_FLT); processEntry.realTimePriority = procItem.stat.getInteger(ProcStat.STAT_RT_PRIORITY); processEntry.schedulingPolicy = procItem.stat.getInteger(ProcStat.STAT_POLICY); processEntry.cpu = procItem.stat.getInteger(ProcStat.STAT_TASK_CPU); processEntry.threadCount = procItem.stat.getInteger(ProcStat.STAT_NUM_THREADS); processEntry.tty = procItem.stat.getInteger(ProcStat.STAT_TTY_NR); processEntry.seLinuxPolicy = procItem.sepol; processEntry.name = TextUtils.isEmpty(procItem.name) ? procItem.status.getString(ProcStatus.STATUS_NAME) : procItem.name; processEntry.users = new ProcessUsers(procItem.status.getString(ProcStatus.STATUS_UID), procItem.status.getString(ProcStatus.STATUS_GID)); processEntry.cpuTimeConsumed = (procItem.stat.getInteger(ProcStat.STAT_UTIME) + procItem.stat.getInteger(ProcStat.STAT_STIME)) / mClockTicks; processEntry.cCpuTimeConsumed = (procItem.stat.getInteger(ProcStat.STAT_CUTIME) + procItem.stat.getInteger(ProcStat.STAT_CSTIME)) / mClockTicks; processEntry.elapsedTime = mUptime - (procItem.stat.getInteger(ProcStat.STAT_START_TIME) / mClockTicks); String state = procItem.status.getString(ProcStatus.STATUS_STATE); if (state == null) { throw new RuntimeException("Process state cannot be empty!"); } processEntry.processState = state.substring(0, 1); StringBuilder stateExtra = new StringBuilder(); if (procItem.stat.getInteger(ProcStat.STAT_NICE) < 0) { stateExtra.append("<"); } else if (procItem.stat.getInteger(ProcStat.STAT_NICE) > 0) { stateExtra.append("N"); } if (procItem.stat.getInteger(ProcStat.STAT_SID) == processEntry.pid) { stateExtra.append("s"); } String vmLck = procItem.status.getString(ProcStatus.STATUS_VM_LCK); if (vmLck != null && Integer.decode(vmLck.substring(0, 1)) > 0) { stateExtra.append("L"); } if (procItem.stat.getInteger(ProcStat.STAT_TTY_PGRP) == processEntry.pid) { stateExtra.append("+"); } processEntry.processStatePlus = stateExtra.toString(); return processEntry; } private static class ProcItem { public ProcStat stat; public ProcMemStat memStat; public ProcStatus status; @Nullable public String name; @Nullable public String sepol; @Nullable public String wchan; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/logcat/AbsLogViewerFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.logcat; import android.content.Context; import android.os.Bundle; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.CheckBox; import android.widget.Filter; import androidx.activity.OnBackPressedCallback; import androidx.annotation.CallSuper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.view.MenuProvider; import androidx.fragment.app.Fragment; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.util.ArrayList; import java.util.List; import java.util.Objects; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.logcat.helper.PreferenceHelper; import io.github.muntashirakon.AppManager.logcat.helper.SaveLogHelper; import io.github.muntashirakon.AppManager.logcat.struct.LogLine; import io.github.muntashirakon.AppManager.logcat.struct.SearchCriteria; import io.github.muntashirakon.AppManager.settings.LogViewerPreferences; import io.github.muntashirakon.AppManager.utils.StoragePermission; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.dialog.SearchableItemsDialogBuilder; import io.github.muntashirakon.dialog.SearchableMultiChoiceDialogBuilder; import io.github.muntashirakon.dialog.SearchableSingleChoiceDialogBuilder; import io.github.muntashirakon.dialog.TextInputDialogBuilder; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.multiselection.MultiSelectionActionsView; import io.github.muntashirakon.util.AdapterUtils; import io.github.muntashirakon.widget.MultiSelectionView; public abstract class AbsLogViewerFragment extends Fragment implements MenuProvider, LogViewerViewModel.LogLinesAvailableInterface, MultiSelectionActionsView.OnItemSelectedListener, MultiSelectionView.OnSelectionModeChangeListener, LogViewerActivity.SearchingInterface, Filter.FilterListener{ public static final String TAG = AbsLogViewerFragment.class.getSimpleName(); protected RecyclerView mRecyclerView; protected MultiSelectionView mMultiSelectionView; protected LogViewerViewModel mViewModel; protected LogViewerActivity mActivity; protected LogViewerRecyclerAdapter mLogListAdapter; protected boolean mAutoscrollToBottom = true; @Nullable protected volatile SearchCriteria mSearchCriteria; protected final StoragePermission mStoragePermission = StoragePermission.init(this); protected final RecyclerView.OnScrollListener mRecyclerViewScrollListener = new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); } @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); // Update what the first viewable item is final LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager(); if (layoutManager != null) { // Stop autoscroll if the bottom of the list isn't visible anymore mAutoscrollToBottom = layoutManager.findLastCompletelyVisibleItemPosition() == (mLogListAdapter.getItemCount() - 1); } } }; private final OnBackPressedCallback mOnBackPressedCallback = new OnBackPressedCallback(false) { @Override public void handleOnBackPressed() { if (mLogListAdapter.isInSelectionMode()) { mMultiSelectionView.cancel(); } else { setEnabled(false); requireActivity().getOnBackPressedDispatcher().onBackPressed(); } } }; @Nullable @Override public final View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_logcat, container, false); } @CallSuper @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { mViewModel = new ViewModelProvider(requireActivity()).get(LogViewerViewModel.class); mActivity = (LogViewerActivity) requireActivity(); mActivity.setSearchingInterface(this); mRecyclerView = view.findViewById(R.id.list); mRecyclerView.setLayoutManager(new LinearLayoutManager(mActivity)); mRecyclerView.setItemAnimator(null); // Check for query string mSearchCriteria = mActivity.getSearchQuery(); if (mSearchCriteria != null) { mRecyclerView.postDelayed(() -> mActivity.search(mSearchCriteria), 1000); } mLogListAdapter = new LogViewerRecyclerAdapter(); mLogListAdapter.setClickListener(mActivity); mMultiSelectionView = view.findViewById(R.id.selection_view); mMultiSelectionView.setAdapter(mLogListAdapter); mMultiSelectionView.setOnItemSelectedListener(this); mMultiSelectionView.setOnSelectionModeChangeListener(this); mMultiSelectionView.hide(); mRecyclerView.setAdapter(mLogListAdapter); mRecyclerView.addOnScrollListener(mRecyclerViewScrollListener); mActivity.addMenuProvider(this, getViewLifecycleOwner(), Lifecycle.State.RESUMED); // Observers mViewModel.getExpandLogsLiveData().observe(getViewLifecycleOwner(), expanded -> { int oldFirstVisibleItem = ((LinearLayoutManager) Objects.requireNonNull(mRecyclerView.getLayoutManager())).findFirstVisibleItemPosition(); mLogListAdapter.setCollapseMode(!expanded); mLogListAdapter.notifyItemRangeChanged(0, mLogListAdapter.getItemCount(), AdapterUtils.STUB); // Scroll to bottom or the first visible item if (mAutoscrollToBottom) { mRecyclerView.scrollToPosition(mLogListAdapter.getItemCount() - 1); } else if (oldFirstVisibleItem != -1) { mRecyclerView.scrollToPosition(oldFirstVisibleItem); } mActivity.supportInvalidateOptionsMenu(); }); mViewModel.observeLogLevelLiveData().observe(getViewLifecycleOwner(), logLevel -> mLogListAdapter.setLogLevelLimit(logLevel)); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); requireActivity().getOnBackPressedDispatcher().addCallback(this, mOnBackPressedCallback); } @CallSuper @Override public void onDestroy() { if (mRecyclerView != null) { mRecyclerView.removeOnScrollListener(mRecyclerViewScrollListener); } super.onDestroy(); } public abstract void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater); @CallSuper @Override public void onPrepareMenu(@NonNull Menu menu) { MenuItem expandMenu = menu.findItem(R.id.action_expand_collapse); if (expandMenu != null) { if (mViewModel.isCollapsedMode()) { expandMenu.setIcon(R.drawable.ic_expand_more); expandMenu.setTitle(R.string.expand_all); } else { expandMenu.setIcon(R.drawable.ic_expand_less); expandMenu.setTitle(R.string.collapse_all); } } } @CallSuper @Override public boolean onMenuItemSelected(@NonNull MenuItem item) { int id = item.getItemId(); if (id == R.id.action_expand_collapse) { mViewModel.setCollapsedMode(!mViewModel.isCollapsedMode()); } else if (id == R.id.action_open) { mStoragePermission.request(granted -> { if (granted) { displayOpenLogFileDialog(); } }); } else if (id == R.id.action_save) { mStoragePermission.request(granted -> { if (granted) { displaySaveLogDialog(false); } }); } else if (id == R.id.action_delete) { displayDeleteSavedLogsDialog(); } else if (id == R.id.action_change_log_level) { CharSequence[] logLevelsLocalised = getResources().getStringArray(R.array.log_levels); new SearchableSingleChoiceDialogBuilder<>(mActivity, LogViewerPreferences.LOG_LEVEL_VALUES, logLevelsLocalised) .setTitle(R.string.log_level) .setSelection(mViewModel.getLogLevel()) .setOnSingleChoiceClickListener((dialog, which, selectedLogLevel, isChecked) -> { mViewModel.setLogLevel(selectedLogLevel); // Search again mActivity.search(mSearchCriteria); }) .setNegativeButton(R.string.close, null) .show(); } else if (id == R.id.action_settings) { mActivity.displayLogViewerSettings(); } else if (id == R.id.action_show_saved_filters) { mViewModel.loadFilters(); } else if (id == R.id.action_share) { displaySaveDebugLogsDialog(true, false); } else if (id == R.id.action_export) { displaySaveDebugLogsDialog(false, false); return true; } else return false; return true; } @Override public void onSelectionModeEnabled() { mOnBackPressedCallback.setEnabled(true); } @Override public void onSelectionModeDisabled() { mOnBackPressedCallback.setEnabled(false); } @CallSuper @Override public void onQuery(@Nullable SearchCriteria searchCriteria) { mSearchCriteria = searchCriteria; Filter filter = mLogListAdapter.getFilter(); filter.filter(searchCriteria != null ? searchCriteria.query : null, this); } @Override public final void onFilterComplete(int count) { mRecyclerView.scrollToPosition(count - 1); } @NonNull protected final List getCurrentLogsAsListOfStrings() { List result = new ArrayList<>(mLogListAdapter.getItemCount()); for (int i = 0; i < mLogListAdapter.getItemCount(); i++) { result.add(mLogListAdapter.getItem(i).getOriginalLine()); } return result; } @NonNull protected final List getSelectedLogsAsStrings() { List result = new ArrayList<>(); for (LogLine logLine : mLogListAdapter.getSelectedLogLines()) { result.add(logLine.getOriginalLine()); } return result; } protected final void displaySaveLogDialog(boolean onlySelected) { new TextInputDialogBuilder(mActivity, R.string.filename) .setTitle(R.string.save_log) .setInputText(SaveLogHelper.createLogFilename()) .setPositiveButton(R.string.ok, (dialog, which, inputText, isChecked) -> { if (SaveLogHelper.isInvalidFilename(inputText)) { UIUtils.displayShortToast(R.string.enter_good_filename); } else { @SuppressWarnings("ConstantConditions") String filename = inputText.toString(); mViewModel.saveLogs(filename, onlySelected ? getSelectedLogsAsStrings() : getCurrentLogsAsListOfStrings()); } }) .setNegativeButton(R.string.cancel, null) .show(); } protected final void displaySaveDebugLogsDialog(boolean share, boolean onlySelected) { View view = View.inflate(mActivity, R.layout.dialog_send_log, null); CheckBox includeDeviceInfoCheckBox = view.findViewById(android.R.id.checkbox); CheckBox includeDmesgCheckBox = view.findViewById(R.id.checkbox_dmesg); includeDeviceInfoCheckBox.setChecked(PreferenceHelper.getIncludeDeviceInfoPreference(mActivity)); includeDeviceInfoCheckBox.setOnCheckedChangeListener((buttonView, isChecked) -> PreferenceHelper.setIncludeDeviceInfoPreference(mActivity, isChecked)); includeDmesgCheckBox.setChecked(PreferenceHelper.getIncludeDmesgPreference(mActivity)); includeDmesgCheckBox.setOnCheckedChangeListener((buttonView, isChecked) -> PreferenceHelper.setIncludeDmesgPreference(mActivity, isChecked)); new MaterialAlertDialogBuilder(mActivity) .setTitle(share ? R.string.share : R.string.pref_export) .setView(view) .setPositiveButton(R.string.ok, (dialog, which) -> shareOrSaveLogs(share, onlySelected, includeDeviceInfoCheckBox.isChecked(), includeDmesgCheckBox.isChecked())) .setNegativeButton(R.string.cancel, null) .show(); } private void shareOrSaveLogs(boolean share, boolean onlySelected, boolean includeDeviceInfo, boolean includeDmesg) { mActivity.setLogsToBeShared(share, includeDeviceInfo || includeDmesg); mViewModel.prepareLogsToBeSent(includeDeviceInfo, includeDmesg, onlySelected ? getSelectedLogsAsStrings() : getCurrentLogsAsListOfStrings()); } private void displayOpenLogFileDialog() { List logFiles = SaveLogHelper.getLogFiles(); if (logFiles.isEmpty()) { UIUtils.displayShortToast(R.string.no_saved_logs); return; } new SearchableItemsDialogBuilder<>(mActivity, SaveLogHelper.getFormattedFilenames(mActivity, logFiles)) .setTitle(R.string.open) .setOnItemClickListener((dialog, which, item) -> mActivity.openLogFile(logFiles.get(which))) .setNegativeButton(R.string.close, null) .show(); } private void displayDeleteSavedLogsDialog() { List logFiles = SaveLogHelper.getLogFiles(); if (logFiles.isEmpty()) { UIUtils.displayShortToast(R.string.no_saved_logs); return; } CharSequence[] filenameArray = SaveLogHelper.getFormattedFilenames(mActivity, logFiles); new SearchableMultiChoiceDialogBuilder<>(mActivity, logFiles, filenameArray) .setTitle(R.string.manage_saved_logs) .setPositiveButton(R.string.delete, (dialog, which, selectedFiles) -> { final int deleteCount = selectedFiles.size(); if (deleteCount == 0) return; new MaterialAlertDialogBuilder(mActivity) .setTitle(R.string.delete_saved_log) .setCancelable(true) .setMessage(getResources().getQuantityString(R.plurals.file_deletion_confirmation, deleteCount, deleteCount)) .setPositiveButton(android.R.string.ok, (dialog1, which1) -> { for (Path selectedFile : selectedFiles) { SaveLogHelper.deleteLogIfExists(selectedFile.getName()); } UIUtils.displayShortToast(R.string.deleted_successfully); }) .setNegativeButton(android.R.string.cancel, null) .show(); }) .setNegativeButton(R.string.cancel, null) .show(); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/logcat/CrazyLoggerService.java ================================================ // SPDX-License-Identifier: WTFPL AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.logcat; import android.content.Intent; import android.os.SystemClock; import android.util.Log; import java.util.Random; import io.github.muntashirakon.AppManager.types.ForegroundService; // Copyright 2012 Nolan Lawson public class CrazyLoggerService extends ForegroundService { public static final String TAG = CrazyLoggerService.class.getSimpleName(); public static final int[] LOG_LEVELS = new int[]{Log.VERBOSE, Log.DEBUG, Log.INFO, Log.WARN, Log.ERROR, Log.ASSERT}; public static final String[] LOG_MESSAGES = new String[]{ "Email: email@me.com", "FTP: ftp://website.com:21", "HTTP: https://website.com", "A simple log", "Another log" }; private static final long INTERVAL = 300; private boolean mKill = false; public CrazyLoggerService() { super(TAG); } protected void onHandleIntent(Intent intent) { while (!mKill) { SystemClock.sleep(INTERVAL); if (new Random().nextInt(100) % 5 == 0) { Log.println(LOG_LEVELS[new Random().nextInt(6)], TAG, LOG_MESSAGES[new Random().nextInt(5)]); } } } @Override public void onDestroy() { super.onDestroy(); mKill = true; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/logcat/LiveLogViewerFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.logcat; import static io.github.muntashirakon.AppManager.logcat.LogViewerActivity.UPDATE_CHECK_INTERVAL; import android.os.Bundle; import android.text.TextUtils; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.widget.Filter; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.lang.ref.WeakReference; import java.util.List; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.logcat.helper.ServiceHelper; import io.github.muntashirakon.AppManager.logcat.struct.LogLine; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.AppManager.utils.Utils; import io.github.muntashirakon.multiselection.MultiSelectionActionsView; // Copyright 2022 Muntashir Al-Islam public class LiveLogViewerFragment extends AbsLogViewerFragment implements LogViewerViewModel.LogLinesAvailableInterface, MultiSelectionActionsView.OnItemSelectedListener, LogViewerActivity.SearchingInterface, Filter.FilterListener { public static final String TAG = LiveLogViewerFragment.class.getSimpleName(); private int mLogCounter = 0; @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); mMultiSelectionView.setOnSelectionChangeListener(selectionCount -> { if (selectionCount == 1) { mViewModel.pauseLogcat(); } else if (selectionCount == 0) { mViewModel.resumeLogcat(); } return false; }); mViewModel.startLogcat(new WeakReference<>(this)); } @Override public void onResume() { if (mLogListAdapter != null && mLogListAdapter.getItemCount() > 0) { // Scroll to bottom // TODO: 31/5/22 Is this really required? mRecyclerView.scrollToPosition(mLogListAdapter.getItemCount() - 1); } if (mActivity.getSupportActionBar() != null) { mActivity.getSupportActionBar().setSubtitle(""); } super.onResume(); } @Override public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) { menuInflater.inflate(R.menu.fragment_live_log_viewer_actions, menu); } @Override public void onPrepareMenu(@NonNull Menu menu) { super.onPrepareMenu(menu); boolean recordingInProgress = ServiceHelper.checkIfServiceIsRunning(requireContext().getApplicationContext(), LogcatRecordingService.class); MenuItem recordMenuItem = menu.findItem(R.id.action_record); recordMenuItem.setEnabled(!recordingInProgress); recordMenuItem.setVisible(!recordingInProgress); MenuItem crazyLoggerMenuItem = menu.findItem(R.id.action_crazy_logger_service); crazyLoggerMenuItem.setEnabled(BuildConfig.DEBUG); crazyLoggerMenuItem.setVisible(BuildConfig.DEBUG); } @Override public boolean onMenuItemSelected(@NonNull MenuItem item) { int id = item.getItemId(); if (id == R.id.action_play_pause) { if (mViewModel.isLogcatPaused()) { mViewModel.resumeLogcat(); item.setIcon(R.drawable.ic_pause); } else { mViewModel.pauseLogcat(); item.setIcon(R.drawable.ic_play_arrow); } } else if (id == R.id.action_clear) { if (mLogListAdapter != null) { mLogListAdapter.clear(); UIUtils.displayLongToast(R.string.log_cleared); } } else if (id == R.id.action_record) { mStoragePermission.request(granted -> { if (granted) { mActivity.showRecordLogDialog(); } }); } else if (id == R.id.action_crazy_logger_service) { ServiceHelper.startOrStopCrazyLogger(mActivity); } else return super.onMenuItemSelected(item); return true; } @Override public void onNewLogsAvailable(@NonNull List logLines) { mActivity.hideProgressBar(); for (LogLine logLine : logLines) { mLogListAdapter.addWithFilter(logLine, mSearchCriteria, true); mActivity.addToAutocompleteSuggestions(logLine); } // How many logs to keep in memory, to avoid OutOfMemoryError int maxNumLogLines = Prefs.LogViewer.getDisplayLimit(); // Check to see if the list needs to be truncated to avoid OutOfMemoryError ++mLogCounter; if (mLogCounter % UPDATE_CHECK_INTERVAL == 0 && mLogListAdapter.getRealSize() > maxNumLogLines) { int numItemsToRemove = mLogListAdapter.getRealSize() - maxNumLogLines; mLogListAdapter.removeFirst(numItemsToRemove); Log.d(TAG, "Truncating %d lines from log list to avoid out of memory errors", numItemsToRemove); } if (mAutoscrollToBottom) { mRecyclerView.scrollToPosition(mLogListAdapter.getItemCount() - 1); } } @Override public boolean onNavigationItemSelected(@NonNull MenuItem item) { int id = item.getItemId(); if (id == R.id.action_save) { displaySaveLogDialog(true); } else if (id == R.id.action_copy) { ThreadUtils.postOnBackgroundThread(() -> { String logs = TextUtils.join("\n", getSelectedLogsAsStrings()); ThreadUtils.postOnMainThread(() -> Utils.copyToClipboard(ContextUtils.getContext(), "Logs", logs)); }); } else if (id == R.id.action_export) { displaySaveDebugLogsDialog(false, true); } else if (id == R.id.action_share) { displaySaveDebugLogsDialog(true, true); } else return false; return true; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/logcat/LogFilterAdapter.java ================================================ // SPDX-License-Identifier: WTFPL AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.logcat; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.button.MaterialButton; import java.util.Collections; import java.util.List; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.db.AppsDb; import io.github.muntashirakon.AppManager.db.entity.LogFilter; import io.github.muntashirakon.util.AdapterUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; // Copyright 2012 Nolan Lawson public class LogFilterAdapter extends RecyclerView.Adapter { private OnClickListener mListener; private final List mItems; public void setOnItemClickListener(OnClickListener listener) { mListener = listener; } public LogFilterAdapter(@NonNull List items) { mItems = items; } public void add(@NonNull LogFilter filter) { int previousSize = mItems.size(); mItems.add(filter); Collections.sort(mItems, LogFilter.COMPARATOR); int currentSize = mItems.size(); AdapterUtils.notifyDataSetChanged(this, previousSize, currentSize); } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_title_action, parent, false); return new ViewHolder(v); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { final LogFilter logFilter = mItems.get(position); holder.textView.setText(logFilter.name); holder.itemView.setOnClickListener(v -> { if (mListener != null) { mListener.onClick(holder.itemView, position, logFilter); } }); holder.actionButton.setOnClickListener(v -> { ThreadUtils.postOnBackgroundThread(() -> AppsDb.getInstance().logFilterDao().delete(logFilter)); mItems.remove(position); notifyItemRemoved(position); }); } @Override public int getItemCount() { return mItems.size(); } public interface OnClickListener { void onClick(View view, int position, LogFilter logFilter); } public static class ViewHolder extends RecyclerView.ViewHolder { TextView textView; MaterialButton actionButton; public ViewHolder(View itemView) { super(itemView); textView = itemView.findViewById(R.id.item_title); actionButton = itemView.findViewById(R.id.item_action); actionButton.setContentDescription(itemView.getContext().getString(R.string.item_remove)); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/logcat/LogViewerActivity.java ================================================ // SPDX-License-Identifier: WTFPL AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.logcat; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.database.MatrixCursor; import android.net.Uri; import android.os.Bundle; import android.os.PowerManager; import android.provider.BaseColumns; import android.text.TextUtils; import android.view.MenuItem; import android.view.View; import android.widget.TextView; import androidx.activity.result.ActivityResult; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; import androidx.core.content.ContextCompat; import androidx.cursoradapter.widget.CursorAdapter; import androidx.cursoradapter.widget.SimpleCursorAdapter; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton; import com.google.android.material.progressindicator.LinearProgressIndicator; import com.google.android.material.textfield.TextInputLayout; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.Set; import io.github.muntashirakon.AppManager.BaseActivity; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.db.AppsDb; import io.github.muntashirakon.AppManager.db.dao.LogFilterDao; import io.github.muntashirakon.AppManager.db.entity.LogFilter; import io.github.muntashirakon.AppManager.fm.FmProvider; import io.github.muntashirakon.AppManager.intercept.IntentCompat; import io.github.muntashirakon.AppManager.logcat.helper.SaveLogHelper; import io.github.muntashirakon.AppManager.logcat.helper.ServiceHelper; import io.github.muntashirakon.AppManager.logcat.struct.LogLine; import io.github.muntashirakon.AppManager.logcat.struct.SearchCriteria; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.settings.SettingsActivity; import io.github.muntashirakon.AppManager.utils.BetterActivityResult; import io.github.muntashirakon.AppManager.utils.CpuUtils; import io.github.muntashirakon.AppManager.utils.MultithreadedExecutor; import io.github.muntashirakon.AppManager.utils.StoragePermission; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.dialog.DialogTitleBuilder; import io.github.muntashirakon.dialog.TextInputDropdownDialogBuilder; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; import io.github.muntashirakon.util.UiUtils; import io.github.muntashirakon.widget.RecyclerView; import io.github.muntashirakon.widget.SearchView; // Copyright 2012 Nolan Lawson // Copyright 2021 Muntashir Al-Islam public class LogViewerActivity extends BaseActivity implements SearchView.OnQueryTextListener, LogViewerRecyclerAdapter.ViewHolder.OnSearchByClickListener, SearchView.OnSuggestionListener { public static final String TAG = LogViewerActivity.class.getSimpleName(); public interface SearchingInterface { void onQuery(@Nullable SearchCriteria searchCriteria); } public static final String EXTRA_FILTER = "filter"; public static final String EXTRA_LEVEL = "level"; // how often to check to see if we've gone over the max size public static final int UPDATE_CHECK_INTERVAL = 200; // how many suggestions to keep in the autosuggestions text private static final int MAX_NUM_SUGGESTIONS = 1000; public static final String EXTRA_FILENAME = "filename"; private LinearProgressIndicator mProgressIndicator; private ExtendedFloatingActionButton mStopRecordingFab; @Nullable private AlertDialog mLoadingDialog; private SearchView mSearchView; @Nullable private SearchCriteria mSearchCriteria; private boolean mDynamicallyEnteringSearchQuery; private final Set mSearchSuggestionsSet = new HashSet<>(); private CursorAdapter mSearchSuggestionsAdapter; private boolean mLogsToBeShared; @Nullable private SearchingInterface mSearchingInterface; private LogViewerViewModel mViewModel; private PowerManager.WakeLock mWakeLock; private final MultithreadedExecutor mExecutor = MultithreadedExecutor.getNewInstance(); private final BetterActivityResult mActivityLauncher = BetterActivityResult.registerActivityForResult(this); private final StoragePermission mStoragePermission = StoragePermission.init(this); private final BetterActivityResult mSaveLauncher = BetterActivityResult .registerForActivityResult(this, new ActivityResultContracts.CreateDocument("*/*")); public static void startChooser(@NonNull Context context, @Nullable String subject, @NonNull String attachmentType, @NonNull Path attachment) { Intent actionSendIntent = new Intent(Intent.ACTION_SEND) .setType(attachmentType) .putExtra(Intent.EXTRA_SUBJECT, subject) .putExtra(Intent.EXTRA_STREAM, FmProvider.getContentUri(attachment)) .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); try { context.startActivity(Intent.createChooser(actionSendIntent, context.getResources().getText(R.string.send_log_title)) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); } catch (Exception e) { UIUtils.displayLongToast(e.getMessage()); } } @Override public void onAuthenticated(Bundle savedInstanceState) { setContentView(R.layout.activity_logcat); setSupportActionBar(findViewById(R.id.toolbar)); mViewModel = new ViewModelProvider(this).get(LogViewerViewModel.class); mProgressIndicator = findViewById(R.id.progress_linear); mProgressIndicator.setVisibilityAfterHide(View.GONE); mStopRecordingFab = findViewById(R.id.fab); UiUtils.applyWindowInsetsAsMargin(mStopRecordingFab); ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setDisplayShowCustomEnabled(true); mSearchView = UIUtils.setupSearchView(actionBar, this); mSearchView.setOnSuggestionListener(this); } mSearchSuggestionsAdapter = new SimpleCursorAdapter(this, io.github.muntashirakon.ui.R.layout.auto_complete_dropdown_item, null, new String[]{"suggestion"}, new int[]{android.R.id.text1}, CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER); mSearchView.setSuggestionsAdapter(mSearchSuggestionsAdapter); // Set removal of sensitive info LogLine.omitSensitiveInfo = Prefs.LogViewer.omitSensitiveInfo(); if ("record".equals(getIntent().getStringExtra("shortcut_action"))) { // Handle shortcut mStoragePermission.request(granted -> { if (granted) { startLogRecorder(); } }); return; } mStopRecordingFab.setOnClickListener(v -> { // Stop log recorder ServiceHelper.stopBackgroundServiceIfRunning(LogViewerActivity.this); }); // Set collapsed mode mViewModel.setCollapsedMode(!Prefs.LogViewer.expandByDefault()); // It doesn't matter whether the permission has been granted or not, we can start logging mViewModel.observeLoggingFinished().observe(this, finished -> { if (finished) { mProgressIndicator.hide(); if (mViewModel.isLogcatPaused()) { mViewModel.resumeLogcat(); } } }); mViewModel.observeLoadingProgress().observe(this, percentage -> mProgressIndicator.setProgressCompat(percentage, true)); mViewModel.observeTruncatedLines().observe(this, maxDisplayedLines -> UIUtils.displayLongToast( getResources().getQuantityString(R.plurals.toast_log_truncated, maxDisplayedLines, maxDisplayedLines))); mViewModel.getLogFilters().observe(this, this::showFiltersDialog); mViewModel.observeLogSaved().observe(this, path -> { if (path != null) { UIUtils.displayShortToast(R.string.log_saved); if (!path.getName().endsWith(".zip")) { openLogFile(path.getName()); } } else { UIUtils.displayLongToast(R.string.unable_to_save_log); } }); mViewModel.getLogsToBeSent().observe(this, sendLogDetails -> { if (mLoadingDialog != null) { mLoadingDialog.dismiss(); } if (sendLogDetails == null || sendLogDetails.getAttachmentType() == null || sendLogDetails.getAttachment() == null) { UIUtils.displayLongToast(R.string.failed); return; } if (mLogsToBeShared) { // Open chooser dialog startChooser(this, sendLogDetails.getSubject(), sendLogDetails.getAttachmentType(), sendLogDetails.getAttachment()); } else { // Open SAF activity mSaveLauncher.launch(sendLogDetails.getAttachment().getName(), uri -> { if (uri == null) return; mViewModel.saveLogs(Paths.get(uri), sendLogDetails); }); } }); startLogging(); } public void loadNewFragment(Fragment fragment) { getSupportFragmentManager() .beginTransaction() .replace(R.id.main_layout, fragment) .addToBackStack(null) .commit(); } private void startLogRecorder() { String logFilename = SaveLogHelper.createLogFilename(); mExecutor.submit(() -> { // Start recording logs Intent intent = ServiceHelper.getLogcatRecorderServiceIfNotAlreadyRunning(this, logFilename, "", Prefs.LogViewer.getLogLevel()); runOnUiThread(() -> { if (intent != null) { ContextCompat.startForegroundService(this, intent); } finish(); }); }); } @WorkerThread private void addFiltersToSuggestions() { for (LogFilter logFilter : AppsDb.getInstance().logFilterDao().getAll()) { addToAutocompleteSuggestions(logFilter.name); } } private void startLogging() { applyFiltersFromIntent(getIntent()); Uri dataUri = IntentCompat.getDataUri(getIntent()); String filename = getIntent().getStringExtra(EXTRA_FILENAME); if (dataUri != null) { openLogFile(dataUri); } else if (filename != null) { openLogFile(filename); } else { mWakeLock = CpuUtils.getPartialWakeLock("logcat_activity"); mWakeLock.acquire(); startLiveLogViewer(false); } } private void applyFiltersFromIntent(@Nullable Intent intent) { if (intent == null) return; String filter = intent.getStringExtra(EXTRA_FILTER); String level = intent.getStringExtra(EXTRA_LEVEL); if (!TextUtils.isEmpty(filter)) { setSearchQuery(filter); } if (!TextUtils.isEmpty(level)) { int logLevelLimit = LogLine.convertCharToLogLevel(level.charAt(0)); if (logLevelLimit == -1) { UIUtils.displayLongToast(R.string.toast_invalid_level, level); } else { mViewModel.setLogLevel(logLevelLimit); search(mSearchCriteria); } } } @Override public void onResume() { super.onResume(); if (mStopRecordingFab != null) { boolean recordingInProgress = ServiceHelper.checkIfServiceIsRunning(getApplicationContext(), LogcatRecordingService.class); mStopRecordingFab.setVisibility(recordingInProgress ? View.VISIBLE : View.GONE); } } @Override protected void onNewIntent(@NonNull Intent intent) { super.onNewIntent(intent); applyFiltersFromIntent(intent); Uri dataUri = IntentCompat.getDataUri(intent); String filename = intent.getStringExtra(EXTRA_FILENAME); if (dataUri != null) { openLogFile(dataUri); } else if (filename != null) { openLogFile(filename); } } @Override public void onDestroy() { CpuUtils.releaseWakeLock(mWakeLock); super.onDestroy(); mExecutor.shutdownNow(); } private void startLiveLogViewer(boolean force) { if (!mViewModel.isLogcatKilled() && !force) { // Logcat already running mViewModel.resumeLogcat(); return; } // (re)start logcat resetDisplay(); mProgressIndicator.hide(); mProgressIndicator.setIndeterminate(true); mProgressIndicator.show(); if (getSupportFragmentManager().findFragmentByTag(LiveLogViewerFragment.TAG) != null) { // Fragment already exists, just restart logcat mViewModel.restartLogcat(); return; } getSupportFragmentManager() .beginTransaction() .replace(R.id.main_layout, new LiveLogViewerFragment(), LiveLogViewerFragment.TAG) .commit(); } private void populateSuggestionsAdapter(@Nullable String query) { final MatrixCursor c = new MatrixCursor(new String[]{BaseColumns._ID, "suggestion"}); List suggestionsForQuery = getSuggestionsForQuery(query); for (int i = 0, suggestionsForQuerySize = suggestionsForQuery.size(); i < suggestionsForQuerySize; i++) { String suggestion = suggestionsForQuery.get(i); c.addRow(new Object[]{i, suggestion}); } mSearchSuggestionsAdapter.changeCursor(c); } @NonNull private List getSuggestionsForQuery(@Nullable String query) { List suggestions = new ArrayList<>(mSearchSuggestionsSet); Collections.sort(suggestions, String.CASE_INSENSITIVE_ORDER); List actualSuggestions = new ArrayList<>(); if (query != null) { for (String suggestion : suggestions) { if (suggestion.toLowerCase(Locale.getDefault()).startsWith(query.toLowerCase(Locale.getDefault()))) { actualSuggestions.add(suggestion); } } } return actualSuggestions; } @Override public boolean onOptionsItemSelected(MenuItem item) { int itemId = item.getItemId(); if (itemId == android.R.id.home) { getOnBackPressedDispatcher().onBackPressed(); return true; } return false; } @Override public boolean onSearchByClick(MenuItem item, LogLine logLine) { if (logLine != null) { if (logLine.getPid() == -1) { // invalid line return false; } showSearchByDialog(logLine); return true; } return false; } @Override public boolean onQueryTextSubmit(String query) { return false; } @Override public boolean onQueryTextChange(String newText) { if (!mDynamicallyEnteringSearchQuery) { search(new SearchCriteria(newText)); populateSuggestionsAdapter(newText); } mDynamicallyEnteringSearchQuery = false; return false; } @Override public boolean onSuggestionSelect(int position) { return false; } @Override public boolean onSuggestionClick(int position) { List suggestions = getSuggestionsForQuery(mSearchCriteria != null ? mSearchCriteria.query : null); if (!suggestions.isEmpty()) { mSearchView.setQuery(suggestions.get(position), true); } return false; } void hideProgressBar() { if (mProgressIndicator != null) { mProgressIndicator.hide(); } } void setLogsToBeShared(boolean logsToBeShared, boolean displayLoader) { mLogsToBeShared = logsToBeShared; if (displayLoader) { mLoadingDialog = UIUtils.getProgressDialog(LogViewerActivity.this, R.string.dialog_compiling_log); mLoadingDialog.show(); } else mLoadingDialog = null; } void addToAutocompleteSuggestions(@NonNull LogLine logLine) { if (logLine.getTagName() == null) return; String tag = logLine.getTagName().trim(); if (!TextUtils.isEmpty(tag)) { addToAutocompleteSuggestions(tag); } } void displayLogViewerSettings() { Intent intent = SettingsActivity.getSettingsIntent(this, "log_viewer_prefs"); mActivityLauncher.launch(intent, result -> { // Preferences may have changed mViewModel.setCollapsedMode(!Prefs.LogViewer.expandByDefault()); if (result.getResultCode() == Activity.RESULT_FIRST_USER) { Intent data = result.getData(); if (data != null && data.getBooleanExtra("bufferChanged", false)) { // Log buffer changed, so update list startLiveLogViewer(true); } } }); } private void showSearchByDialog(final LogLine logLine) { View view = View.inflate(this, R.layout.dialog_searchby, null); AlertDialog dialog = new MaterialAlertDialogBuilder(this) .setTitle(R.string.filter_choice) .setIcon(io.github.muntashirakon.ui.R.drawable.ic_search) .setView(view) .setNegativeButton(R.string.close, null) .show(); TextInputLayout pkg = view.findViewById(R.id.search_by_pkg); TextInputLayout tag = view.findViewById(R.id.search_by_tag); TextInputLayout uid = view.findViewById(R.id.search_by_uid); TextInputLayout pid = view.findViewById(R.id.search_by_pid); if (logLine.getPackageName() == null) { pkg.setVisibility(View.GONE); } if (logLine.getUidOwner() == null) { uid.setVisibility(View.GONE); } TextView pkgText = pkg.getEditText(); TextView tagText = tag.getEditText(); TextView uidText = uid.getEditText(); TextView pidText = pid.getEditText(); Objects.requireNonNull(pkgText).setText(logLine.getPackageName()); Objects.requireNonNull(tagText).setText(logLine.getTagName()); Objects.requireNonNull(uidText).setText(String.format(Locale.ROOT, "%s (%d)", logLine.getUidOwner(), logLine.getUid())); Objects.requireNonNull(pidText).setText(String.valueOf(logLine.getPid())); pkg.setEndIconOnClickListener(v -> { setSearchQuery(SearchCriteria.PKG_KEYWORD + logLine.getPackageName()); dialog.dismiss(); }); tag.setEndIconOnClickListener(v -> { String tagQuery = (logLine.getTagName().contains(" ")) ? ('"' + logLine.getTagName() + '"') : logLine.getTagName(); setSearchQuery(SearchCriteria.TAG_KEYWORD + tagQuery); dialog.dismiss(); }); uid.setEndIconOnClickListener(v -> { setSearchQuery(SearchCriteria.UID_KEYWORD + logLine.getUidOwner()); dialog.dismiss(); }); pid.setEndIconOnClickListener(v -> { setSearchQuery(SearchCriteria.PID_KEYWORD + logLine.getPid()); dialog.dismiss(); }); } void showRecordLogDialog() { String[] suggestions = mSearchSuggestionsSet.toArray(new String[0]); DialogFragment dialog = RecordLogDialogFragment.getInstance(suggestions, () -> { if (mStopRecordingFab != null) { mStopRecordingFab.setVisibility(View.VISIBLE); } }); dialog.show(getSupportFragmentManager(), RecordLogDialogFragment.TAG); } private void showFiltersDialog(List filters) { LogFilterAdapter logFilterAdapter = new LogFilterAdapter(filters); RecyclerView recyclerView = new RecyclerView(this); recyclerView.setLayoutManager(new LinearLayoutManager(this)); recyclerView.setAdapter(logFilterAdapter); DialogTitleBuilder builder = new DialogTitleBuilder(this) .setTitle(R.string.saved_filters) .setEndIcon(R.drawable.ic_add, v -> new TextInputDropdownDialogBuilder(this, R.string.text_filter_text) .setTitle(R.string.add_filter) .setDropdownItems(new ArrayList<>(mSearchSuggestionsSet), -1, true) .setPositiveButton(android.R.string.ok, (dialog1, which, inputText, isChecked) -> handleNewFilterText(inputText == null ? "" : inputText.toString(), logFilterAdapter)) .setNegativeButton(R.string.cancel, null) .show()) .setEndIconContentDescription(R.string.add_filter_ellipsis); AlertDialog alertDialog = new MaterialAlertDialogBuilder(this) .setCustomTitle(builder.build()) .setView(recyclerView) .setNegativeButton(R.string.ok, null) .show(); logFilterAdapter.setOnItemClickListener((v, position, logFilter) -> { setSearchQuery(logFilter.name); alertDialog.dismiss(); }); } protected void handleNewFilterText(@NonNull String text, final LogFilterAdapter logFilterAdapter) { final String trimmed = text.trim(); if (!TextUtils.isEmpty(trimmed)) { mExecutor.submit(() -> { LogFilterDao dao = AppsDb.getInstance().logFilterDao(); long id = dao.insert(trimmed); LogFilter logFilter = dao.get(id); if (logFilter == null) { return; } ThreadUtils.postOnMainThread(() -> { logFilterAdapter.add(logFilter); addToAutocompleteSuggestions(trimmed); }); }); } } void setSearchingInterface(@Nullable SearchingInterface searchingInterface) { mSearchingInterface = searchingInterface; } void openLogFile(Path logFile) { mProgressIndicator.hide(); mProgressIndicator.setIndeterminate(false); mProgressIndicator.show(); loadNewFragment(SavedLogViewerFragment.getInstance(logFile.getUri())); } private void openLogFile(String filename) { try { Path logFile = SaveLogHelper.getFile(filename); mProgressIndicator.hide(); mProgressIndicator.setIndeterminate(false); mProgressIndicator.show(); loadNewFragment(SavedLogViewerFragment.getInstance(logFile.getUri())); } catch (IOException e) { e.printStackTrace(); } } private void openLogFile(Uri logFile) { mProgressIndicator.hide(); mProgressIndicator.setIndeterminate(false); mProgressIndicator.show(); loadNewFragment(SavedLogViewerFragment.getInstance(logFile)); } private void resetDisplay() { mViewModel.setCollapsedMode(!Prefs.LogViewer.expandByDefault()); // Populate suggestions with existing filters (if any) mExecutor.submit(this::addFiltersToSuggestions); resetFilter(); } private void resetFilter() { mViewModel.setLogLevel(Prefs.LogViewer.getLogLevel()); search(mSearchCriteria); } private void setSearchQuery(String text) { // Sets the search text without invoking autosuggestions, which are really only useful when typing mDynamicallyEnteringSearchQuery = true; search(new SearchCriteria(text)); mSearchView.setIconified(false); mSearchView.setQuery(mSearchCriteria != null ? mSearchCriteria.query : null, true); mSearchView.clearFocus(); } @Nullable SearchCriteria getSearchQuery() { return mSearchCriteria; } void search(@Nullable SearchCriteria searchCriteria) { if (mSearchingInterface != null) { mSearchingInterface.onQuery(searchCriteria); } mSearchCriteria = searchCriteria; if (mSearchCriteria == null || !TextUtils.isEmpty(mSearchCriteria.query)) { mDynamicallyEnteringSearchQuery = true; } } private void addToAutocompleteSuggestions(String trimmed) { if (mSearchSuggestionsSet.size() < MAX_NUM_SUGGESTIONS && !mSearchSuggestionsSet.contains(trimmed)) { mSearchSuggestionsSet.add(trimmed); populateSuggestionsAdapter(mSearchCriteria != null ? mSearchCriteria.query : null); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/logcat/LogViewerRecyclerAdapter.java ================================================ // SPDX-License-Identifier: WTFPL AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.logcat; import android.content.Context; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.Filter; import android.widget.Filterable; import android.widget.TextView; import androidx.annotation.ColorInt; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.PopupMenu; import androidx.collection.SparseArrayCompat; import androidx.core.content.ContextCompat; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.logcat.struct.LogLine; import io.github.muntashirakon.AppManager.logcat.struct.SearchCriteria; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.utils.Utils; import io.github.muntashirakon.util.AccessibilityUtils; import io.github.muntashirakon.util.AdapterUtils; import io.github.muntashirakon.widget.MultiSelectionView; // Copyright 2012 Nolan Lawson // Copyright 2021 Muntashir Al-Islam public class LogViewerRecyclerAdapter extends MultiSelectionView.Adapter implements Filterable { public static final String TAG = LogViewerRecyclerAdapter.class.getSimpleName(); private static final SparseArrayCompat BACKGROUND_COLORS = new SparseArrayCompat(7) { { put(android.util.Log.VERBOSE, io.github.muntashirakon.ui.R.color.the_brown_shirts); put(android.util.Log.DEBUG, io.github.muntashirakon.ui.R.color.night_blue_shadow); put(android.util.Log.INFO, io.github.muntashirakon.ui.R.color.blue_popsicle); put(android.util.Log.WARN, io.github.muntashirakon.ui.R.color.red_orange); put(android.util.Log.ERROR, io.github.muntashirakon.ui.R.color.pure_red); put(android.util.Log.ASSERT, io.github.muntashirakon.ui.R.color.pure_red); put(LogLine.LOG_FATAL, io.github.muntashirakon.ui.R.color.electric_red); } }; private static final SparseArrayCompat FOREGROUND_COLORS = new SparseArrayCompat(7) { { put(android.util.Log.VERBOSE, io.github.muntashirakon.ui.R.color.brian_wrinkle_white); put(android.util.Log.DEBUG, io.github.muntashirakon.ui.R.color.brian_wrinkle_white); put(android.util.Log.INFO, io.github.muntashirakon.ui.R.color.brian_wrinkle_white); put(android.util.Log.WARN, io.github.muntashirakon.ui.R.color.brian_wrinkle_white); put(android.util.Log.ERROR, io.github.muntashirakon.ui.R.color.brian_wrinkle_white); put(android.util.Log.ASSERT, io.github.muntashirakon.ui.R.color.brian_wrinkle_white); put(LogLine.LOG_FATAL, io.github.muntashirakon.ui.R.color.brian_wrinkle_white); } }; private static int[] sTagColors; @ColorInt private static int getBackgroundColorForLogLevel(Context context, int logLevel) { Integer result = BACKGROUND_COLORS.get(logLevel); if (result == null) { throw new IllegalArgumentException("Invalid log level: " + logLevel); } return ContextCompat.getColor(context, result); } @ColorInt private static int getForegroundColorForLogLevel(Context context, int logLevel) { Integer result = FOREGROUND_COLORS.get(logLevel); if (result == null) { throw new IllegalArgumentException("Invalid log level: " + logLevel); } return ContextCompat.getColor(context, result); } private static synchronized int getOrCreateTagColor(Context context, String tag) { if (sTagColors == null) { sTagColors = context.getResources().getIntArray(R.array.random_colors); } // Ensure consistency int hashCode = (tag == null) ? 0 : tag.hashCode(); int smear = Math.abs(hashCode) % sTagColors.length; return sTagColors[smear]; } /** * Lock used to modify the content of {@link #mObjects}. Any write operation * performed on the array should be synchronized on this lock. This lock is also * used by the filter (see {@link #getFilter()} to make a synchronized copy of * the original array of data. */ private final Object mLock = new Object(); /** * Contains the list of objects that represent the data of this ArrayAdapter. * The content of this list is referred to as "the array" in the documentation. */ @GuardedBy("mLock") private List mObjects; private ViewHolder.OnSearchByClickListener mSearchByClickListener; private ArrayList mOriginalValues; private ArrayFilter mFilter; private int mLogLevelLimit = Prefs.LogViewer.getLogLevel(); private final Set mSelectedLogLines = new LinkedHashSet<>(); public LogViewerRecyclerAdapter() { mObjects = new ArrayList<>(); setHasStableIds(true); } /** * Adds the specified object at the end of the array. * * @param object The object to add at the end of the array. */ @GuardedBy("mLock") public void add(LogLine object, boolean notify) { synchronized (mLock) { if (mOriginalValues != null) { mOriginalValues.add(object); } mObjects.add(object); if (notify) { notifyItemInserted(mObjects.size() - 1); } } } @GuardedBy("mLock") public void readAll(LogLine object, boolean notify) { synchronized (mLock) { if (mOriginalValues != null) { mOriginalValues.add(object); } mObjects.add(object); if (notify) { notifyItemInserted(mObjects.size() - 1); } } } public void addWithFilter(@NonNull LogLine object, @Nullable SearchCriteria searchCriteria, boolean notify) { if (mOriginalValues != null) { List inputList = Collections.singletonList(object); if (mFilter == null) { mFilter = new ArrayFilter(); } List filteredObjects = mFilter.performFilteringOnList(inputList, searchCriteria); synchronized (mLock) { mOriginalValues.add(object); mObjects.addAll(filteredObjects); if (!filteredObjects.isEmpty() && notify) { notifyItemRangeInserted(mObjects.size() - filteredObjects.size(), filteredObjects.size()); } } } else { synchronized (mLock) { mObjects.add(object); if (notify) { notifyItemInserted(mObjects.size() - 1); } } } } /** * Inserts the specified object at the specified index in the array. * * @param object The object to insert into the array. * @param index The index at which the object must be inserted. */ @GuardedBy("mLock") public void insert(LogLine object, int index) { synchronized (mLock) { if (mOriginalValues != null) { mOriginalValues.add(index, object); } else { mObjects.add(index, object); notifyItemChanged(index, AdapterUtils.STUB); } } } /** * Removes the specified object from the array. * * @param object The object to remove. */ @GuardedBy("mLock") public void remove(LogLine object) { synchronized (mLock) { if (mOriginalValues != null) { mOriginalValues.remove(object); } else { int pos = mObjects.indexOf(object); if (pos >= 0) { mObjects.remove(pos); notifyItemRemoved(pos); } } } } public void removeFirst(int n) { StopWatch stopWatch = new StopWatch("removeFirst()"); if (mOriginalValues != null) { synchronized (mLock) { List subList = mOriginalValues.subList(n, mOriginalValues.size()); for (int i = 0; i < n; i++) { int pos = mObjects.indexOf(mOriginalValues.get(i)); if (pos >= 0) { mObjects.remove(pos); notifyItemRemoved(pos); } } mOriginalValues = new ArrayList<>(subList); } } else { synchronized (mLock) { mObjects = new ArrayList<>(mObjects.subList(n, mObjects.size())); notifyItemRangeRemoved(0, n); } } stopWatch.log(); } /** * Remove all elements from the list. */ @GuardedBy("mLock") public void clear() { synchronized (mLock) { if (mOriginalValues != null) { mOriginalValues.clear(); } int size = mObjects.size(); mObjects.clear(); notifyItemRangeRemoved(0, size); } } @GuardedBy("mLock") public LogLine getItem(int position) { synchronized (mLock) { return mObjects.get(position); } } @Nullable @GuardedBy("mLock") private LogLine getItemSafe(int position) { synchronized (mLock) { if (mObjects.size() > position) { return mObjects.get(position); } return null; } } @GuardedBy("mLock") public int getRealSize() { synchronized (mLock) { return (mOriginalValues != null ? mOriginalValues : mObjects).size(); } } public Set getSelectedLogLines() { return mSelectedLogLines; } @GuardedBy("mLock") public void setCollapseMode(boolean isCollapsed) { synchronized (mLock) { List list = mOriginalValues != null ? mOriginalValues : mObjects; for (LogLine logLine : list) { logLine.setExpanded(!isCollapsed); } } } @Override protected boolean select(int position) { synchronized (mSelectedLogLines) { LogLine logLine = getItemSafe(position); if (logLine != null) { mSelectedLogLines.add(logLine); } return logLine != null; } } @Override protected boolean deselect(int position) { synchronized (mSelectedLogLines) { LogLine logLine = getItemSafe(position); if (logLine != null) { return mSelectedLogLines.remove(logLine); } return false; } } @Override protected boolean isSelected(int position) { synchronized (mSelectedLogLines) { LogLine logLine = getItemSafe(position); if (logLine != null) { return mSelectedLogLines.contains(logLine); } return false; } } @Override protected void cancelSelection() { super.cancelSelection(); synchronized (mSelectedLogLines) { mSelectedLogLines.clear(); } } @Override protected int getSelectedItemCount() { synchronized (mSelectedLogLines) { return mSelectedLogLines.size(); } } @Override protected int getTotalItemCount() { return getItemCount(); } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_logcat, parent, false); return new ViewHolder(v); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { Context context = holder.itemView.getContext(); LogLine logLine = getItem(position); holder.logLine = logLine; int levelColor = getBackgroundColorForLogLevel(context, logLine.getLogLevel()); TextView t = holder.logLevel; t.setText(logLine.getProcessIdText()); t.setBackgroundColor(levelColor); t.setTextColor(getForegroundColorForLogLevel(context, logLine.getLogLevel())); t.setVisibility(logLine.getLogLevel() == -1 ? View.GONE : View.VISIBLE); holder.itemView.setBackgroundResource(0); holder.contentView.setBackgroundResource(position % 2 == 0 ? io.github.muntashirakon.ui.R.drawable.item_semi_transparent : io.github.muntashirakon.ui.R.drawable.item_transparent); // Display message TextView output = holder.output; output.setSingleLine(!logLine.isExpanded()); output.setText(logLine.getLogOutput()); //TAG TEXT VIEW TextView tag = holder.tag; tag.setSingleLine(!logLine.isExpanded()); tag.setText(logLine.getTagName()); tag.setVisibility(logLine.getLogLevel() == -1 ? View.GONE : View.VISIBLE); //EXPANDED INFO boolean extraInfoIsVisible = logLine.isExpanded() && logLine.getPid() != -1 // -1 marks lines like 'beginning of /dev/log...' && Prefs.LogViewer.showPidTidTimestamp(); TextView infoText = holder.info; infoText.setVisibility(extraInfoIsVisible ? View.VISIBLE : View.GONE); if (extraInfoIsVisible) { StringBuilder sb = new StringBuilder(logLine.getTimestamp()); if (logLine.getPid() >= 0) { sb.append(" • ").append(logLine.getPid()); } if (logLine.getUidOwner() != null) { sb.append(" • ").append(logLine.getUidOwner()); } if (logLine.getPackageName() != null) { sb.append(" • ").append(logLine.getPackageName()); } infoText.setText(sb); } tag.setTextColor(getOrCreateTagColor(context, logLine.getTagName())); // Single click on the item: // 1. If it is in selection mode, select the item // 2. Otherwise, expand the item holder.itemView.setOnClickListener(v -> { if (isInSelectionMode()) { toggleSelection(position); AccessibilityUtils.requestAccessibilityFocus(holder.itemView); } else { LogLine line = holder.logLine; line.setExpanded(!line.isExpanded()); notifyItemChanged(position, AdapterUtils.STUB); } }); // Long click on the item: // 1. If it is in selection mode, select range of item // 2. Open context menu holder.itemView.setOnLongClickListener(v -> { if (isInSelectionMode()) { int lastSelectedItemPosition = getLastSelectedItemPosition(); if (lastSelectedItemPosition >= 0) { // Select from last selection to this selection selectRange(lastSelectedItemPosition, position); } else { toggleSelection(position); AccessibilityUtils.requestAccessibilityFocus(holder.itemView); } return true; } PopupMenu popupMenu = new PopupMenu(v.getContext(), v); popupMenu.setForceShowIcon(true); Menu menu = popupMenu.getMenu(); menu.add(R.string.filter_choice) .setIcon(io.github.muntashirakon.ui.R.drawable.ic_search) .setOnMenuItemClickListener(menuItem -> { if (mSearchByClickListener != null) { return mSearchByClickListener.onSearchByClick(menuItem, holder.logLine); } return true; }); menu.add(R.string.copy_to_clipboard) .setIcon(R.drawable.ic_content_copy) .setOnMenuItemClickListener(menuItem -> { Utils.copyToClipboard(context, null, holder.logLine.getOriginalLine()); return true; }); menu.add(R.string.item_select) .setIcon(R.drawable.ic_check_circle) .setOnMenuItemClickListener(menuItem -> { toggleSelection(position); AccessibilityUtils.requestAccessibilityFocus(holder.itemView); return true; }); popupMenu.show(); return true; }); super.onBindViewHolder(holder, position); } @GuardedBy("mLock") @Override public long getItemId(int position) { synchronized (mLock) { return mObjects.get(position).getOriginalLine().hashCode(); } } @GuardedBy("mLock") @Override public int getItemCount() { synchronized (mLock) { return mObjects.size(); } } public int getLogLevelLimit() { return mLogLevelLimit; } public void setLogLevelLimit(int logLevelLimit) { mLogLevelLimit = logLevelLimit; } /** * {@inheritDoc} */ @Override public Filter getFilter() { if (mFilter == null) { mFilter = new ArrayFilter(); } return mFilter; } public void setClickListener(ViewHolder.OnSearchByClickListener clickListener) { mSearchByClickListener = clickListener; } private int getLastSelectedItemPosition() { // Last selected item is the same as the last added item. Iterator it = mSelectedLogLines.iterator(); LogLine lastItem = null; while (it.hasNext()) { lastItem = it.next(); } if (lastItem != null) { int i = 0; for (LogLine fmItem : mObjects) { if (fmItem.equals(lastItem)) { return i; } ++i; } } return -1; } /** *

An array filter constrains the content of the array adapter with * a prefix. Each item that does not start with the supplied prefix * is removed from the list.

*/ private class ArrayFilter extends Filter { @NonNull @Override protected FilterResults performFiltering(CharSequence prefix) { FilterResults results = new FilterResults(); if (mOriginalValues == null) { synchronized (mLock) { mOriginalValues = new ArrayList<>(mObjects); } } SearchCriteria searchCriteria = new SearchCriteria(prefix != null ? prefix.toString() : null); ArrayList allValues = performFilteringOnList(mOriginalValues, searchCriteria); results.values = allValues; results.count = allValues.size(); return results; } public ArrayList performFilteringOnList(List inputList, @Nullable SearchCriteria searchCriteria) { // search by log level ArrayList allValues = new ArrayList<>(); ArrayList logLines; synchronized (mLock) { logLines = new ArrayList<>(inputList); } for (LogLine logLine : logLines) { if (logLine != null && logLine.getLogLevel() >= mLogLevelLimit) { allValues.add(logLine); } } ArrayList finalValues = allValues; // search by criteria if (searchCriteria != null && !searchCriteria.isEmpty()) { final int count = allValues.size(); final ArrayList newValues = new ArrayList<>(count); for (final LogLine value : allValues) { // search the logline based on the criteria if (searchCriteria.matches(value)) { newValues.add(value); } } finalValues = newValues; } return finalValues; } @SuppressWarnings("unchecked") @Override protected void publishResults(CharSequence constraint, FilterResults results) { synchronized (mLock) { int previousCount = mObjects != null ? mObjects.size() : 0; mObjects = (List) results.values; AdapterUtils.notifyDataSetChanged(LogViewerRecyclerAdapter.this, previousCount, mObjects.size()); } } } private static class StopWatch { private long mStartTime; private String mName; public StopWatch(String name) { if (BuildConfig.DEBUG) { mName = name; mStartTime = System.currentTimeMillis(); } } public void log() { Log.d(TAG, "%s took %d ms", mName, (System.currentTimeMillis() - mStartTime)); } } public static class ViewHolder extends MultiSelectionView.ViewHolder { LogLine logLine; View contentView; TextView logLevel; TextView tag; TextView output; TextView info; public ViewHolder(View itemView) { super(itemView); contentView = itemView.findViewById(R.id.log_content); logLevel = itemView.findViewById(R.id.log_level_text); tag = itemView.findViewById(R.id.tag_text); output = itemView.findViewById(R.id.log_output_text); info = itemView.findViewById(R.id.info); } public interface OnSearchByClickListener { boolean onSearchByClick(MenuItem item, LogLine logLine); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/logcat/LogViewerViewModel.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.logcat; import android.app.Application; import android.net.Uri; import androidx.annotation.AnyThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.annotation.WorkerThread; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.ref.WeakReference; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.db.AppsDb; import io.github.muntashirakon.AppManager.db.entity.LogFilter; import io.github.muntashirakon.AppManager.logcat.helper.BuildHelper; import io.github.muntashirakon.AppManager.logcat.helper.SaveLogHelper; import io.github.muntashirakon.AppManager.logcat.reader.LogcatReader; import io.github.muntashirakon.AppManager.logcat.reader.LogcatReaderLoader; import io.github.muntashirakon.AppManager.logcat.struct.LogLine; import io.github.muntashirakon.AppManager.logcat.struct.SavedLog; import io.github.muntashirakon.AppManager.logcat.struct.SendLogDetails; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.runner.Runner; import io.github.muntashirakon.AppManager.self.filecache.FileCache; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.utils.MultithreadedExecutor; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.io.IoUtils; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; // Copyright 2022 Muntashir Al-Islam public class LogViewerViewModel extends AndroidViewModel { public static final String TAG = LogViewerViewModel.class.getSimpleName(); public interface LogLinesAvailableInterface { @UiThread void onNewLogsAvailable(@NonNull List logLines); } private final Object mLock = new Object(); private volatile boolean mPaused; private volatile boolean mKilled = true; private volatile boolean mCollapsedMode; private volatile int mLogLevel; private volatile LogcatReader mReader; private final Pattern mFilterPattern; private final MutableLiveData mExpandLogsLiveData = new MutableLiveData<>(); private final MutableLiveData mLoggingFinishedLiveData = new MutableLiveData<>(); private final MutableLiveData mLoadingProgressLiveData = new MutableLiveData<>(); private final MutableLiveData mTruncatedLinesLiveData = new MutableLiveData<>(); private final MutableLiveData mLogLevelLiveData = new MutableLiveData<>(); private final MutableLiveData> mLogFiltersLiveData = new MutableLiveData<>(); private final MutableLiveData mLogSavedLiveData = new MutableLiveData<>(); private final MutableLiveData mLogToBeSentLiveData = new MutableLiveData<>(); private final MultithreadedExecutor mExecutor = MultithreadedExecutor.getNewInstance(); public LogViewerViewModel(@NonNull Application application) { super(application); mFilterPattern = Pattern.compile(Prefs.LogViewer.getFilterPattern()); } @Override protected void onCleared() { killLogcatReaderInternal(); mExecutor.shutdown(); super.onCleared(); } public LiveData observeLoggingFinished() { return mLoggingFinishedLiveData; } public LiveData observeLoadingProgress() { return mLoadingProgressLiveData; } public LiveData observeTruncatedLines() { return mTruncatedLinesLiveData; } public LiveData> getLogFilters() { return mLogFiltersLiveData; } public LiveData observeLogSaved() { return mLogSavedLiveData; } public MutableLiveData observeLogLevelLiveData() { return mLogLevelLiveData; } public LiveData getLogsToBeSent() { return mLogToBeSentLiveData; } public LiveData getExpandLogsLiveData() { return mExpandLogsLiveData; } @AnyThread public void startLogcat(@Nullable WeakReference logLinesAvailableInterface) { mExecutor.submit(() -> { mKilled = false; try { mReader = LogcatReaderLoader.create(true).loadReader(); int maxLines = Prefs.LogViewer.getDisplayLimit(); String line; LinkedList initialLines = new LinkedList<>(); while ((line = mReader.readLine()) != null && !ThreadUtils.isInterrupted()) { if (mPaused) { synchronized (mLock) { if (mPaused) { mLock.wait(); } } } LogLine logLine = LogLine.newLogLine(line, !mCollapsedMode, mFilterPattern); if (logLine == null) { if (mReader.readyToRecord()) { // Logcat is ready } } else if (!mReader.readyToRecord()) { // "ready to record" in this case means all the initial lines have been flushed from the reader initialLines.add(logLine); if (initialLines.size() > maxLines) { initialLines.removeFirst(); } } else if (!initialLines.isEmpty()) { // flush all the initial lines we've loaded initialLines.add(logLine); sendNewLogs(initialLines, logLinesAvailableInterface); initialLines.clear(); } else { // just proceed as normal sendNewLogs(Collections.singletonList(logLine), logLinesAvailableInterface); } } } catch (Exception e) { Log.e(TAG, e); } finally { if (logLinesAvailableInterface != null) { logLinesAvailableInterface.clear(); } killLogcatReaderInternal(); } mLoggingFinishedLiveData.postValue(true); }); } @AnyThread public void restartLogcat() { mExecutor.submit(() -> { synchronized (mLock) { // Pause -> reload reader -> resume mPaused = true; try { mReader = LogcatReaderLoader.create(true).loadReader(); } catch (Exception e) { // Errors do not matter Log.e(TAG, e); } finally { mPaused = false; mLock.notify(); } } }); } private static void sendNewLogs(@NonNull List logLines, @Nullable WeakReference logLinesAvailableInterface) { if (logLinesAvailableInterface != null) { LogLinesAvailableInterface i = logLinesAvailableInterface.get(); List logLines1 = new ArrayList<>(logLines); if (i != null) { ThreadUtils.postOnMainThread(() -> i.onNewLogsAvailable(logLines1)); } } } @AnyThread public void pauseLogcat() { mExecutor.submit(() -> { synchronized (mLock) { mPaused = true; } }); } @AnyThread public void resumeLogcat() { mExecutor.submit(() -> { synchronized (mLock) { mPaused = false; mLock.notify(); } }); } public boolean isLogcatPaused() { return mPaused; } public boolean isLogcatKilled() { return mKilled; } public boolean isCollapsedMode() { return mCollapsedMode; } public void setCollapsedMode(boolean collapsedMode) { mCollapsedMode = collapsedMode; mExpandLogsLiveData.postValue(collapsedMode); } public int getLogLevel() { return mLogLevel; } public void setLogLevel(int logLevel) { mLogLevel = logLevel; mLogLevelLiveData.postValue(mLogLevel); } @AnyThread public void killLogcatReader() { mExecutor.submit(this::killLogcatReaderInternal); } @WorkerThread private void killLogcatReaderInternal() { if (!mKilled) { synchronized (mLock) { if (!mKilled && mReader != null) { mReader.killQuietly(); mKilled = true; } } } } @AnyThread public void openLogsFromFile(Uri filename, @Nullable WeakReference logLinesAvailableInterface) { mExecutor.submit(() -> { // remove any lines at the beginning if necessary final int maxLines = Prefs.LogViewer.getDisplayLimit(); SavedLog savedLog; savedLog = SaveLogHelper.openLog(filename, maxLines); List lines = savedLog.getLogLines(); List logLines = new ArrayList<>(); for (int lineNumber = 0, linesSize = lines.size(); lineNumber < linesSize; lineNumber++) { String line = lines.get(lineNumber); LogLine logLine = LogLine.newLogLine(line, !mCollapsedMode, mFilterPattern); if (logLine != null) { logLines.add(logLine); } mLoadingProgressLiveData.postValue(lineNumber * 100 / linesSize); } sendNewLogs(logLines, logLinesAvailableInterface); if (savedLog.isTruncated()) { mTruncatedLinesLiveData.postValue(maxLines); } }); } @AnyThread public void loadFilters() { mExecutor.submit(() -> { final List filters = AppsDb.getInstance().logFilterDao().getAll(); Collections.sort(filters); mLogFiltersLiveData.postValue(filters); }); } @AnyThread public void saveLogs(String filename, @NonNull List logLines) { mExecutor.submit(() -> { SaveLogHelper.deleteLogIfExists(filename); mLogSavedLiveData.postValue(SaveLogHelper.saveLog(logLines, filename)); }); } @AnyThread public void saveLogs(@NonNull Path path, @NonNull SendLogDetails sendLogDetails) { mExecutor.submit(() -> { if (sendLogDetails.getAttachmentType() == null || sendLogDetails.getAttachment() == null) { mLogSavedLiveData.postValue(null); return; } try (OutputStream output = path.openOutputStream()) { try (InputStream input = sendLogDetails.getAttachment().openInputStream()) { IoUtils.copy(input, output); } mLogSavedLiveData.postValue(path); } catch (IOException e) { mLogSavedLiveData.postValue(null); e.printStackTrace(); } }); } @AnyThread public void prepareLogsToBeSent(boolean includeDeviceInfo, boolean includeDmesg, @NonNull Collection logLines) { mExecutor.submit(() -> { SendLogDetails sendLogDetails = new SendLogDetails(); sendLogDetails.setSubject(getApplication().getString(R.string.subject_log_report)); // either zip up multiple files or just attach the one file String deviceInfo = null; if (includeDeviceInfo) { deviceInfo = BuildHelper.getBuildInformationAsString(); } String dmesg = null; if (includeDmesg) { Runner.Result result = Runner.runCommand(new String[]{"dmesg"}); if (result.isSuccessful()) { dmesg = result.getOutput(); if (dmesg.length() == 0) { dmesg = null; } } } int exportCount = 0; if (!logLines.isEmpty()) { ++exportCount; } if (deviceInfo != null) { ++exportCount; } if (dmesg != null) { ++exportCount; } if (exportCount == 0) { sendLogDetails.setAttachmentType(null); } else if (exportCount == 1) { Path tempFile; if (!logLines.isEmpty()) { tempFile = SaveLogHelper.saveTemporaryFile("log", null, logLines); } else if (dmesg != null) { tempFile = SaveLogHelper.saveTemporaryFile("txt", dmesg, null); } else { // Device info tempFile = SaveLogHelper.saveTemporaryFile("txt", deviceInfo, null); } sendLogDetails.setAttachmentType("text/plain"); sendLogDetails.setAttachment(tempFile); } else { // Multiple attachments, make zip first try { Path zipFile = Paths.get(FileCache.getGlobalFileCache().createCachedFile("zip")); try (ZipOutputStream output = new ZipOutputStream(new BufferedOutputStream(zipFile.openOutputStream(), 0x1000))) { if (!logLines.isEmpty()) { output.putNextEntry(new ZipEntry(SaveLogHelper.LOG_FILENAME)); for (String logLine : logLines) { output.write(logLine.getBytes(StandardCharsets.UTF_8)); output.write("\n".getBytes(StandardCharsets.UTF_8)); } } if (deviceInfo != null) { output.putNextEntry(new ZipEntry(SaveLogHelper.DEVICE_INFO_FILENAME)); output.write(deviceInfo.getBytes(StandardCharsets.UTF_8)); } if (dmesg != null) { output.putNextEntry(new ZipEntry(SaveLogHelper.DMESG_FILENAME)); output.write(dmesg.getBytes(StandardCharsets.UTF_8)); } } sendLogDetails.setAttachmentType("application/zip"); sendLogDetails.setAttachment(zipFile); } catch (Throwable th) { th.printStackTrace(); sendLogDetails.setAttachmentType(null); } } mLogToBeSentLiveData.postValue(sendLogDetails); }); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/logcat/LogcatRecordingService.java ================================================ // SPDX-License-Identifier: WTFPL AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.logcat; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.Uri; import android.os.PowerManager; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.NotificationManagerCompat; import androidx.core.app.PendingIntentCompat; import androidx.core.app.ServiceCompat; import androidx.core.content.ContextCompat; import java.io.IOException; import java.util.Random; import java.util.regex.Pattern; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.intercept.IntentCompat; import io.github.muntashirakon.AppManager.logcat.helper.SaveLogHelper; import io.github.muntashirakon.AppManager.logcat.helper.ServiceHelper; import io.github.muntashirakon.AppManager.logcat.helper.WidgetHelper; import io.github.muntashirakon.AppManager.logcat.reader.LogcatReader; import io.github.muntashirakon.AppManager.logcat.reader.LogcatReaderLoader; import io.github.muntashirakon.AppManager.logcat.struct.LogLine; import io.github.muntashirakon.AppManager.logcat.struct.SearchCriteria; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.progress.NotificationProgressHandler; import io.github.muntashirakon.AppManager.progress.QueuedProgressHandler; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.types.ForegroundService; import io.github.muntashirakon.AppManager.utils.CpuUtils; import io.github.muntashirakon.AppManager.utils.NotificationUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; /** * Reads logs. */ // Copyright 2012 Nolan Lawson // Copyright 2021 Muntashir Al-Islam public class LogcatRecordingService extends ForegroundService { public static final String TAG = LogcatRecordingService.class.getSimpleName(); public static final String URI_SCHEME = "am_logcat_recording_service"; public static final String EXTRA_FILENAME = "filename"; public static final String EXTRA_LOADER = "loader"; public static final String EXTRA_QUERY_FILTER = "filter"; public static final String EXTRA_LEVEL = "level"; private static final String ACTION_STOP_RECORDING = BuildConfig.APPLICATION_ID + ".action.STOP_RECORDING"; public static final String CHANNEL_ID = BuildConfig.APPLICATION_ID + ".channel.LOGCAT_RECORDER"; private final Object mLock = new Object(); private LogcatReader mReader; private boolean mKilled; private QueuedProgressHandler mProgressHandler; private PowerManager.WakeLock mWakeLock; private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { // Received broadcast to kill service killProcess(); ServiceHelper.stopBackgroundServiceIfRunning(context); } }; public LogcatRecordingService() { super("AppTrackerService"); } @Override public void onCreate() { super.onCreate(); IntentFilter intentFilter = new IntentFilter(ACTION_STOP_RECORDING); intentFilter.addDataScheme(URI_SCHEME); ContextCompat.registerReceiver(this, mReceiver, intentFilter, ContextCompat.RECEIVER_NOT_EXPORTED); mWakeLock = CpuUtils.getPartialWakeLock("logcat_recorder"); mWakeLock.acquire(); } private void initializeReader(@NonNull LogcatReaderLoader loader) { try { mReader = loader.loadReader(); while (mReader != null && !mReader.readyToRecord() && !mKilled) { mReader.readLine(); // Keep skipping lines until we find one that is past the last log line, i.e. // it's ready to record } if (!mKilled) { ThreadUtils.postOnMainThread(() -> UIUtils.displayShortToast(R.string.log_recording_started)); } } catch (IOException e) { Log.e(TAG, e); } } @Override public void onDestroy() { CpuUtils.releaseWakeLock(mWakeLock); super.onDestroy(); killProcess(); unregisterReceiver(mReceiver); ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE); WidgetHelper.updateWidgets(getApplicationContext(), false); } @Override public int onStartCommand(Intent intent, int flags, int startId) { // Update widget WidgetHelper.updateWidgets(getApplicationContext()); mProgressHandler = new NotificationProgressHandler(this, new NotificationProgressHandler.NotificationManagerInfo(CHANNEL_ID, "Logcat Recorder", NotificationManagerCompat.IMPORTANCE_DEFAULT), NotificationUtils.HIGH_PRIORITY_NOTIFICATION_INFO, null); Intent stopRecordingIntent = new Intent(); stopRecordingIntent.setAction(ACTION_STOP_RECORDING); // Have to make this unique for God knows what reason stopRecordingIntent.setData(Uri.withAppendedPath(Uri.parse(URI_SCHEME + "://stop/"), Long.toHexString(new Random().nextLong()))); PendingIntent pendingIntent = PendingIntentCompat.getBroadcast(this, 0 /* no requestCode */, stopRecordingIntent, PendingIntent.FLAG_ONE_SHOT, false); Object notificationInfo = new NotificationProgressHandler.NotificationInfo() .setTitle(getString(R.string.notification_title)) .setBody(getString(R.string.notification_subtext)) .setStatusBarText(getText(R.string.notification_ticker)) .setDefaultAction(pendingIntent); mProgressHandler.onAttach(this, notificationInfo); return super.onStartCommand(intent, flags, startId); } protected void onHandleIntent(@Nullable Intent intent) { if (intent == null) { // Empty calls return; } Log.d(TAG, "Starting with intent: %s", intent); String filename = intent.getStringExtra(EXTRA_FILENAME); String queryText = intent.getStringExtra(EXTRA_QUERY_FILTER); SearchCriteria searchCriteria = new SearchCriteria(queryText); int logLevel = intent.getIntExtra(EXTRA_LEVEL, Prefs.LogViewer.getLogLevel()); boolean searchCriteriaWillAlwaysMatch = searchCriteria.isEmpty(); boolean logLevelAcceptsEverything = logLevel == android.util.Log.VERBOSE; StringBuilder stringBuilder = new StringBuilder(); LogcatReaderLoader loader = IntentCompat.getUnwrappedParcelableExtra(intent, EXTRA_LOADER, LogcatReaderLoader.class); if (loader == null) { // No loader found return; } SaveLogHelper.deleteLogIfExists(filename); initializeReader(loader); try { String line; int lineCount = 0; int logLinePeriod = Prefs.LogViewer.getLogWritingInterval(); Pattern filterPattern = Pattern.compile(Prefs.LogViewer.getFilterPattern()); while (mReader != null && (line = mReader.readLine()) != null && !mKilled) { // filter if (!searchCriteriaWillAlwaysMatch || !logLevelAcceptsEverything) { if (!checkLogLine(line, searchCriteria, logLevel, filterPattern)) { continue; } } stringBuilder.append(line).append("\n"); if (++lineCount % logLinePeriod == 0) { // avoid OutOfMemoryErrors; flush now SaveLogHelper.saveLog(stringBuilder, filename); stringBuilder.delete(0, stringBuilder.length()); // clear } } } catch (IOException e) { Log.e(TAG, e); } finally { killProcess(); Log.d(TAG, "Service ended"); boolean logSaved = SaveLogHelper.saveLog(stringBuilder, filename); NotificationProgressHandler.NotificationInfo notificationInfo = new NotificationProgressHandler.NotificationInfo() .setTitle(getString(R.string.notification_title)) .setAutoCancel(true); if (logSaved) { notificationInfo.setTitle(getString(R.string.log_saved)) .setStatusBarText(getString(R.string.log_saved)) .setBody(getString(R.string.tap_to_see_details)) .setDefaultAction(getLogcatActivityToViewSavedFile(filename)); } else { notificationInfo.setTitle(getString(R.string.unable_to_save_log)); } ThreadUtils.postOnMainThread(() -> mProgressHandler.onResult(notificationInfo)); } } private boolean checkLogLine(String line, SearchCriteria searchCriteria, int logLevel, Pattern filterPattern) { LogLine logLine = LogLine.newLogLine(line, false, filterPattern); return logLine != null && logLine.getLogLevel() >= logLevel && searchCriteria.matches(logLine); } private PendingIntent getLogcatActivityToViewSavedFile(String filename) { // Start up the logcat activity if necessary and show the saved file Intent targetIntent = new Intent(getApplicationContext(), LogViewerActivity.class); targetIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); targetIntent.setAction(Intent.ACTION_MAIN); targetIntent.putExtra(LogViewerActivity.EXTRA_FILENAME, filename); return PendingIntentCompat.getActivity(this, 0, targetIntent, PendingIntent.FLAG_ONE_SHOT, false); } private void killProcess() { if (!mKilled) { synchronized (mLock) { if (!mKilled && mReader != null) { // kill the logcat process mReader.killQuietly(); mKilled = true; } } } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/logcat/RecordLogDialogActivity.java ================================================ // SPDX-License-Identifier: WTFPL AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.logcat; import android.os.Bundle; import io.github.muntashirakon.AppManager.BaseActivity; // Copyright 2012 Nolan Lawson // Copyright 2021 Muntashir Al-Islam public class RecordLogDialogActivity extends BaseActivity { public static final String EXTRA_QUERY_SUGGESTIONS = "suggestions"; @Override public boolean getTransparentBackground() { return true; } @Override protected void onAuthenticated(Bundle savedInstanceState) { RecordLogDialogFragment dialog; dialog = RecordLogDialogFragment.getInstance(getIntent().getStringArrayExtra(EXTRA_QUERY_SUGGESTIONS), null); dialog.show(getSupportFragmentManager(), RecordLogDialogFragment.TAG); dialog.setOnDismissListener(v -> finish()); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/logcat/RecordLogDialogFragment.java ================================================ // SPDX-License-Identifier: WTFPL AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.logcat; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; import android.widget.Button; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.core.content.ContextCompat; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.FragmentActivity; import java.util.Arrays; import java.util.Collections; import java.util.List; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.logcat.helper.SaveLogHelper; import io.github.muntashirakon.AppManager.logcat.helper.ServiceHelper; import io.github.muntashirakon.AppManager.logcat.helper.WidgetHelper; import io.github.muntashirakon.AppManager.settings.LogViewerPreferences; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.dialog.TextInputDialogBuilder; import io.github.muntashirakon.dialog.TextInputDropdownDialogBuilder; // Copyright 2012 Nolan Lawson // Copyright 2021 Muntashir Al-Islam public class RecordLogDialogFragment extends DialogFragment { public static final String TAG = RecordLogDialogFragment.class.getSimpleName(); private static final String QUERY_SUGGESTIONS = "suggestions"; public interface OnRecordingServiceStartedListenerInterface { void onServiceStarted(); } @NonNull public static RecordLogDialogFragment getInstance(@Nullable String[] suggestions, @Nullable OnRecordingServiceStartedListenerInterface listener) { RecordLogDialogFragment dialogFragment = new RecordLogDialogFragment(); Bundle args = new Bundle(); args.putStringArray(QUERY_SUGGESTIONS, suggestions); dialogFragment.setArguments(args); dialogFragment.mListener = listener; return dialogFragment; } private FragmentActivity mActivity; private String mFilterQuery; private int mLogLevel; @Nullable private OnRecordingServiceStartedListenerInterface mListener; @Nullable private DialogInterface.OnDismissListener mDismissListener; public void setOnDismissListener(DialogInterface.OnDismissListener dismissListener) { mDismissListener = dismissListener; } @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { mActivity = requireActivity(); String[] suggestions = requireArguments().getStringArray(QUERY_SUGGESTIONS); String logFilename = SaveLogHelper.createLogFilename(); mLogLevel = Prefs.LogViewer.getLogLevel(); mFilterQuery = ""; return new TextInputDialogBuilder(mActivity, R.string.enter_filename) .setTitle(R.string.record_log) .setInputText(logFilename) .setPositiveButton(R.string.ok, (dialog, which, inputText, isChecked) -> { if (SaveLogHelper.isInvalidFilename(inputText)) { UIUtils.displayShortToast(R.string.enter_good_filename); } else { //noinspection ConstantConditions String filename = inputText.toString(); Context context = mActivity.getApplicationContext(); ThreadUtils.postOnBackgroundThread(() -> { Intent intent = ServiceHelper.getLogcatRecorderServiceIfNotAlreadyRunning(context, filename, mFilterQuery, mLogLevel); ThreadUtils.postOnMainThread(() -> { if (intent != null) { ContextCompat.startForegroundService(context, intent); } if (mListener != null && !(mActivity.isFinishing() || mActivity.isDestroyed())) { mListener.onServiceStarted(); } }); }); } }) .setNegativeButton(R.string.cancel, (dialog, which, inputText, isChecked) -> WidgetHelper.updateWidgets(mActivity)) .setNeutralButton(R.string.text_filter_ellipsis, null) .setOnShowListener(dialog -> { AlertDialog dialog1 = (AlertDialog) dialog; Button filterButton = dialog1.getButton(AlertDialog.BUTTON_NEUTRAL); filterButton.setOnClickListener(v -> { WidgetHelper.updateWidgets(mActivity); showFilterDialogForRecording(suggestions != null ? Arrays.asList(suggestions) : Collections.emptyList()); }); }) .create(); } @Override public void onDismiss(@NonNull DialogInterface dialog) { super.onDismiss(dialog); if (mDismissListener != null) { mDismissListener.onDismiss(dialog); } } public void showFilterDialogForRecording(List filterQuerySuggestions) { List logLevelsLocalised = Arrays.asList(getResources().getStringArray(R.array.log_levels)); int idx = LogViewerPreferences.LOG_LEVEL_VALUES.indexOf(mLogLevel); TextInputDropdownDialogBuilder builder = new TextInputDropdownDialogBuilder(mActivity, R.string.text_filter_text) .setTitle(R.string.filter) .setInputText(mFilterQuery) .setDropdownItems(filterQuerySuggestions, -1, true) .setAuxiliaryInput(getText(R.string.log_level), null, logLevelsLocalised.get(idx), logLevelsLocalised, true) .setNegativeButton(R.string.cancel, null); builder.setPositiveButton(R.string.ok, (dialog, which, inputText, isChecked) -> { if (inputText == null || builder.getAuxiliaryInput() == null) return; int logLevelIdx = logLevelsLocalised.indexOf(builder.getAuxiliaryInput().toString().trim()); if (logLevelIdx == -1) return; mLogLevel = LogViewerPreferences.LOG_LEVEL_VALUES.get(logLevelIdx); mFilterQuery = inputText.toString(); }).show(); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/logcat/RecordingWidgetProvider.java ================================================ // SPDX-License-Identifier: WTFPL AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.logcat; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProvider; import android.content.Context; import android.content.Intent; import androidx.annotation.NonNull; import io.github.muntashirakon.AppManager.logcat.helper.PreferenceHelper; import io.github.muntashirakon.AppManager.logcat.helper.ServiceHelper; import io.github.muntashirakon.AppManager.logcat.helper.WidgetHelper; import java.util.Arrays; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.logs.Log; // Copyright 2012 Nolan Lawson public class RecordingWidgetProvider extends AppWidgetProvider { public static final String TAG = RecordingWidgetProvider.class.getSimpleName(); public static final String ACTION_RECORD_OR_STOP = BuildConfig.APPLICATION_ID + ".action.RECORD_OR_STOP"; public static final String URI_SCHEME = "log_viewer_widget"; @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { super.onUpdate(context, appWidgetManager, appWidgetIds); Log.d(TAG, "onUpdate() for appWidgetIds %s", Arrays.toString(appWidgetIds)); // track which widgets were created, since there's a bug in the android system that lets // stale app widget ids stick around PreferenceHelper.setWidgetExistsPreference(context, appWidgetIds); WidgetHelper.updateWidgets(context, appWidgetIds); } @Override public void onReceive(@NonNull final Context context, @NonNull Intent intent) { super.onReceive(context, intent); Log.d(TAG, "onReceive called with intent %s", intent); if (ACTION_RECORD_OR_STOP.equals(intent.getAction())) { // Start or stop recording as necessary synchronized (RecordingWidgetProvider.class) { boolean alreadyRunning = ServiceHelper.checkIfServiceIsRunning(context, LogcatRecordingService.class); if (alreadyRunning) { // stop the current recording process ServiceHelper.stopBackgroundServiceIfRunning(context); } else { // start a new recording process Intent targetIntent = new Intent(); targetIntent.setClass(context, RecordLogDialogActivity.class); targetIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); context.startActivity(targetIntent); } } } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/logcat/SavedLogViewerFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.logcat; import android.net.Uri; import android.os.Bundle; import android.text.TextUtils; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.widget.Filter; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.os.BundleCompat; import java.lang.ref.WeakReference; import java.util.List; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.logcat.struct.LogLine; import io.github.muntashirakon.AppManager.logcat.struct.SearchCriteria; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.Utils; import io.github.muntashirakon.multiselection.MultiSelectionActionsView; // Copyright 2022 Muntashir Al-Islam public class SavedLogViewerFragment extends AbsLogViewerFragment implements LogViewerViewModel.LogLinesAvailableInterface, MultiSelectionActionsView.OnItemSelectedListener, LogViewerActivity.SearchingInterface, Filter.FilterListener { public static final String TAG = SavedLogViewerFragment.class.getSimpleName(); public static final String ARG_FILE_URI = "file_uri"; @NonNull public static SavedLogViewerFragment getInstance(@NonNull Uri uri) { SavedLogViewerFragment fragment = new SavedLogViewerFragment(); Bundle args = new Bundle(); args.putParcelable(ARG_FILE_URI, uri); fragment.setArguments(args); return fragment; } private String mFilename = ""; @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); Uri uri = BundleCompat.getParcelable(requireArguments(), ARG_FILE_URI, Uri.class); if (uri == null) { // TODO: 31/5/22 Handle invalid URI return; } mFilename = uri.getLastPathSegment(); mViewModel.openLogsFromFile(uri, new WeakReference<>(this)); } @Override public void onResume() { if (mLogListAdapter != null && mLogListAdapter.getItemCount() > 0) { // Scroll to bottom // TODO: 31/5/22 Is this really required? mRecyclerView.scrollToPosition(mLogListAdapter.getItemCount() - 1); } if (mActivity.getSupportActionBar() != null) { mActivity.getSupportActionBar().setSubtitle(mFilename); } super.onResume(); } @Override public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) { menuInflater.inflate(R.menu.fragment_saved_log_viewer_actions, menu); } @Override public boolean onMenuItemSelected(@NonNull MenuItem item) { return super.onMenuItemSelected(item); } @Override public void onNewLogsAvailable(@NonNull List logLines) { mActivity.hideProgressBar(); for (LogLine logLine : logLines) { mLogListAdapter.addWithFilter(logLine, new SearchCriteria(null), true); mActivity.addToAutocompleteSuggestions(logLine); } mRecyclerView.scrollToPosition(mLogListAdapter.getItemCount() - 1); } @Override public boolean onNavigationItemSelected(@NonNull MenuItem item) { int id = item.getItemId(); if (id == R.id.action_save) { displaySaveLogDialog(true); } else if (id == R.id.action_copy) { ThreadUtils.postOnBackgroundThread(() -> { String logs = TextUtils.join("\n", getSelectedLogsAsStrings()); ThreadUtils.postOnMainThread(() -> Utils.copyToClipboard(ContextUtils.getContext(), "Logs", logs)); }); } else if (id == R.id.action_export) { displaySaveDebugLogsDialog(false, true); } else if (id == R.id.action_share) { displaySaveDebugLogsDialog(true, true); } else return false; return true; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/logcat/helper/BuildHelper.java ================================================ // SPDX-License-Identifier: WTFPL AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.logcat.helper; import android.os.Build; import androidx.annotation.NonNull; import java.lang.reflect.Field; import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.Map.Entry; import java.util.SortedMap; import java.util.TreeMap; // Copyright 2012 Nolan Lawson public class BuildHelper { // From android.os.Build private static final List BUILD_FIELDS = Arrays.asList( "BOARD", "BOOTLOADER", "BRAND", "CPU_ABI", "CPU_ABI2", "DEVICE", "DISPLAY", "FINGERPRINT", "HARDWARE", "HOST", "ID", "MANUFACTURER", "MODEL", "PRODUCT", "RADIO", "SERIAL", "TAGS", "TIME", "TYPE", "USER"); // From android.os.Build.Version private static final List BUILD_VERSION_FIELDS = Arrays.asList( "CODENAME", "INCREMENTAL", "RELEASE", "SDK_INT"); @NonNull public static String getBuildInformationAsString() { SortedMap keysToValues = new TreeMap<>(); for (String buildField : BUILD_FIELDS) { putKeyValue(Build.class, buildField, keysToValues); } for (String buildVersionField : BUILD_VERSION_FIELDS) { putKeyValue(Build.VERSION.class, buildVersionField, keysToValues); } StringBuilder stringBuilder = new StringBuilder(); for (Entry entry : keysToValues.entrySet()) { stringBuilder.append(entry.getKey()).append(": ").append(entry.getValue()).append('\n'); } return stringBuilder.toString(); } private static void putKeyValue(@NonNull Class clazz, String buildField, @NonNull SortedMap keysToValues) { try { Field field = clazz.getField(buildField); Object value = field.get(null); String key = clazz.getSimpleName().toLowerCase(Locale.ROOT) + "." + buildField.toLowerCase(Locale.ROOT); keysToValues.put(key, String.valueOf(value)); } catch (SecurityException | NoSuchFieldException | IllegalAccessException ignore) { } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/logcat/helper/LogcatHelper.java ================================================ // SPDX-License-Identifier: WTFPL AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.logcat.helper; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import io.github.muntashirakon.AppManager.compat.ProcessCompat; import io.github.muntashirakon.AppManager.logs.Log; // Copyright 2012 Nolan Lawson public class LogcatHelper { public static final String TAG = LogcatHelper.class.getSimpleName(); @IntDef(value = {LOG_ID_MAIN, LOG_ID_RADIO, LOG_ID_EVENTS, LOG_ID_SYSTEM, LOG_ID_CRASH}, flag = true) @Retention(RetentionPolicy.SOURCE) public @interface LogBufferId { } public static final int LOG_ID_MAIN = 1; public static final int LOG_ID_RADIO = 1 << 1; public static final int LOG_ID_EVENTS = 1 << 2; public static final int LOG_ID_SYSTEM = 1 << 3; public static final int LOG_ID_CRASH = 1 << 4; public static final int LOG_ID_ALL = LOG_ID_MAIN | LOG_ID_RADIO | LOG_ID_EVENTS | LOG_ID_SYSTEM | LOG_ID_CRASH; public static final int LOG_ID_DEFAULT = LOG_ID_MAIN | LOG_ID_SYSTEM | LOG_ID_CRASH; public static final String BUFFER_MAIN = "main"; public static final String BUFFER_RADIO = "radio"; public static final String BUFFER_EVENTS = "events"; public static final String BUFFER_SYSTEM = "system"; public static final String BUFFER_CRASH = "crash"; public static final String BUFFER_ALL = "all"; public static final String BUFFER_DEFAULT = "default"; public static final int DEFAULT_DISPLAY_LIMIT = 10_000; public static final int DEFAULT_LOG_WRITE_INTERVAL = 200; public static Process getLogcatProcess(@LogBufferId int buffers) throws IOException { return ProcessCompat.exec(getLogcatArgs(buffers, false)); } @Nullable public static String getLastLogLine(@LogBufferId int buffers) { Process dumpLogcatProcess = null; String result = null; try { dumpLogcatProcess = ProcessCompat.exec(getLogcatArgs(buffers, true)); try (BufferedReader reader = new BufferedReader(new InputStreamReader(dumpLogcatProcess .getInputStream()), 8192)) { String line; while ((line = reader.readLine()) != null) { result = line; } } } catch (IOException e) { Log.e(TAG, e); } finally { if (dumpLogcatProcess != null) { dumpLogcatProcess.destroy(); Log.d(TAG, "destroyed 1 dump logcat process"); } } return result; } @NonNull public static String[] getLogcatArgs(@LogBufferId int buffers, boolean dumpAndExit) { // https://cs.android.com/android/platform/superproject/main/+/main:system/logging/liblog/logprint.cpp;l=1547;drc=b4d6320e2ae398b36f0aaafb2ecd83609d2d99af // threadtime: : // Modifiers: // - uid: Display UID (Android 7 onwards) // - descriptive: Descriptive output, currently NOP (Android 8 onwards) // * UID is not guaranteed List args = new ArrayList<>(Arrays.asList("logcat", "-v", "threadtime", "-v", "uid")); if (buffers == LOG_ID_ALL) { args.add("-b"); args.add(BUFFER_ALL); } else if (buffers == LOG_ID_DEFAULT) { args.add("-b"); args.add(BUFFER_DEFAULT); } else { if ((buffers & LOG_ID_MAIN) != 0) { args.add("-b"); args.add(BUFFER_MAIN); } if ((buffers & LOG_ID_RADIO) != 0) { args.add("-b"); args.add(BUFFER_RADIO); } if ((buffers & LOG_ID_EVENTS) != 0) { args.add("-b"); args.add(BUFFER_EVENTS); } if ((buffers & LOG_ID_SYSTEM) != 0) { args.add("-b"); args.add(BUFFER_SYSTEM); } if ((buffers & LOG_ID_CRASH) != 0) { args.add("-b"); args.add(BUFFER_CRASH); } } if (dumpAndExit) args.add("-d"); return args.toArray(new String[0]); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/logcat/helper/PreferenceHelper.java ================================================ // SPDX-License-Identifier: WTFPL AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.logcat.helper; import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.preference.PreferenceManager; import androidx.annotation.NonNull; import java.util.ArrayList; import java.util.List; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.settings.Prefs; // Copyright 2012 Nolan Lawson public class PreferenceHelper { private static final String WIDGET_EXISTS_PREFIX = "widget_"; public static boolean getWidgetExistsPreference(Context context, int appWidgetId) { SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context); String widgetExists = WIDGET_EXISTS_PREFIX.concat(Integer.toString(appWidgetId)); return sharedPrefs.getBoolean(widgetExists, false); } public static void setWidgetExistsPreference(Context context, @NonNull int[] appWidgetIds) { SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context); Editor editor = sharedPrefs.edit(); for (int appWidgetId : appWidgetIds) { String widgetExists = WIDGET_EXISTS_PREFIX.concat(Integer.toString(appWidgetId)); editor.putBoolean(widgetExists, true); } editor.apply(); } @NonNull public static List getBuffers() { return getBuffers(Prefs.LogViewer.getBuffers()); } @NonNull public static List getBuffers(@LogcatHelper.LogBufferId int buffers) { List separatedBuffers = new ArrayList<>(); if ((buffers & LogcatHelper.LOG_ID_MAIN) != 0) { separatedBuffers.add(LogcatHelper.LOG_ID_MAIN); } if ((buffers & LogcatHelper.LOG_ID_RADIO) != 0) { separatedBuffers.add(LogcatHelper.LOG_ID_RADIO); } if ((buffers & LogcatHelper.LOG_ID_EVENTS) != 0) { separatedBuffers.add(LogcatHelper.LOG_ID_EVENTS); } if ((buffers & LogcatHelper.LOG_ID_SYSTEM) != 0) { separatedBuffers.add(LogcatHelper.LOG_ID_SYSTEM); } if ((buffers & LogcatHelper.LOG_ID_CRASH) != 0) { separatedBuffers.add(LogcatHelper.LOG_ID_CRASH); } return separatedBuffers; } public static boolean getIncludeDeviceInfoPreference(Context context) { SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context); return sharedPrefs.getBoolean(context.getString(R.string.pref_include_device_info), true); } public static void setIncludeDeviceInfoPreference(Context context, boolean value) { SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context); Editor editor = sharedPrefs.edit(); editor.putBoolean(context.getString(R.string.pref_include_device_info), value); editor.apply(); } public static boolean getIncludeDmesgPreference(Context context) { SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context); return sharedPrefs.getBoolean(context.getString(R.string.pref_include_dmesg), true); } public static void setIncludeDmesgPreference(Context context, boolean value) { SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context); Editor editor = sharedPrefs.edit(); editor.putBoolean(context.getString(R.string.pref_include_dmesg), value); editor.apply(); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/logcat/helper/SaveLogHelper.java ================================================ // SPDX-License-Identifier: WTFPL AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.logcat.helper; import android.content.Context; import android.net.Uri; import android.text.SpannableStringBuilder; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.jetbrains.annotations.Contract; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintStream; import java.text.DateFormat; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.GregorianCalendar; import java.util.LinkedList; import java.util.List; import io.github.muntashirakon.AppManager.logcat.struct.SavedLog; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.self.filecache.FileCache; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; // Copyright 2012 Nolan Lawson public class SaveLogHelper { public static final String TAG = SaveLogHelper.class.getSimpleName(); public static final String DEVICE_INFO_FILENAME = "device_info.txt"; public static final String LOG_FILENAME = "logcat.am.log"; public static final String DMESG_FILENAME = "dmesg.txt"; public static final String SAVED_LOGS_DIR = "saved_logs"; private static final int BUFFER = 0x1000; // 4K @Nullable public static Path saveTemporaryFile(String extension, CharSequence text, Collection lines) { try { Path tempFile = Paths.get(FileCache.getGlobalFileCache().createCachedFile(extension)); try (PrintStream out = new PrintStream(new BufferedOutputStream(tempFile.openOutputStream(), BUFFER))) { if (text != null) { // one big string out.print(text); } else { // multiple lines separated by newline for (CharSequence line : lines) { out.println(line); } } Log.d(TAG, "Saved temp file: %s", tempFile); return tempFile; } } catch (IOException e) { Log.e(TAG, e); return null; } } @NonNull public static Path getFile(@NonNull String filename) throws IOException { return getSavedLogsDirectory().findFile(filename); } public static void deleteLogIfExists(@Nullable String filename) { if (filename == null) return; try { getFile(filename).delete(); } catch (IOException ignore) { } } @NonNull public static CharSequence[] getFormattedFilenames(@NonNull Context context, @NonNull List files) { CharSequence[] fileNames = new CharSequence[files.size()]; DateFormat dateFormat = DateFormat.getDateTimeInstance(); for (int i = 0; i < files.size(); ++i) { fileNames[i] = new SpannableStringBuilder(files.get(i).getName()) .append("\n").append(UIUtils.getSmallerText(UIUtils.getSecondaryText(context, dateFormat.format(new Date(files.get(i).lastModified()))))); } return fileNames; } @NonNull public static List getLogFiles() { try { Path[] filesArray = getSavedLogsDirectory().listFiles(); List files = new ArrayList<>(Arrays.asList(filesArray)); Collections.sort(files, (o1, o2) -> Long.compare(o2.lastModified(), o1.lastModified())); return files; } catch (IOException e) { return Collections.emptyList(); } } @NonNull public static SavedLog openLog(@NonNull Uri fileUri, int maxLines) { Path logFile = Paths.get(fileUri); LinkedList logLines = new LinkedList<>(); boolean truncated = false; try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(logFile.openInputStream()), BUFFER)) { while (bufferedReader.ready()) { logLines.add(bufferedReader.readLine()); if (logLines.size() > maxLines) { logLines.removeFirst(); truncated = true; } } } catch (IOException e) { Log.e(TAG, e); } return new SavedLog(logLines, truncated); } public static synchronized boolean saveLog(CharSequence logString, String filename) { try { saveLog(null, logString, filename); return true; } catch (IOException e) { e.printStackTrace(); return false; } } @Nullable public static synchronized Path saveLog(List logLines, String filename) { try { return saveLog(logLines, null, filename); } catch (IOException e) { e.printStackTrace(); return null; } } @NonNull private static Path saveLog(List logLines, CharSequence logString, String filename) throws IOException { Path newFile = getSavedLogsDirectory().createNewFile(filename, null); try (PrintStream out = new PrintStream(new BufferedOutputStream(newFile.openOutputStream(), BUFFER))) { // Save a log as either a list of strings if (logLines != null) { for (CharSequence line : logLines) { out.println(line); } } else if (logString != null) { out.print(logString); } } return newFile; } @NonNull private static Path getSavedLogsDirectory() throws IOException { Path amDir = Prefs.Storage.getAppManagerDirectory(); if (!amDir.exists()) { amDir.mkdir(); } return amDir.findOrCreateDirectory(SAVED_LOGS_DIR); } @NonNull public static String createLogFilename() { Date date = new Date(); GregorianCalendar calendar = new GregorianCalendar(); calendar.setTime(date); DecimalFormat twoDigitDecimalFormat = new DecimalFormat("00"); DecimalFormat fourDigitDecimalFormat = new DecimalFormat("0000"); String year = fourDigitDecimalFormat.format(calendar.get(Calendar.YEAR)); String month = twoDigitDecimalFormat.format(calendar.get(Calendar.MONTH) + 1); String day = twoDigitDecimalFormat.format(calendar.get(Calendar.DAY_OF_MONTH)); String hour = twoDigitDecimalFormat.format(calendar.get(Calendar.HOUR_OF_DAY)); String minute = twoDigitDecimalFormat.format(calendar.get(Calendar.MINUTE)); String second = twoDigitDecimalFormat.format(calendar.get(Calendar.SECOND)); return year + "-" + month + "-" + day + "-" + hour + "-" + minute + "-" + second + ".am.log"; } @Contract("null -> true") public static boolean isInvalidFilename(@Nullable CharSequence filename) { String filenameAsString; return TextUtils.isEmpty(filename) || (filenameAsString = filename.toString()).contains("/") || filenameAsString.contains(":") || filenameAsString.contains(" ") || !filenameAsString.endsWith(".log"); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/logcat/helper/ServiceHelper.java ================================================ // SPDX-License-Identifier: WTFPL AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.logcat.helper; import android.app.ActivityManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import androidx.annotation.Nullable; import java.util.List; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.intercept.IntentCompat; import io.github.muntashirakon.AppManager.logcat.CrazyLoggerService; import io.github.muntashirakon.AppManager.logcat.LogcatRecordingService; import io.github.muntashirakon.AppManager.logcat.reader.LogcatReaderLoader; import io.github.muntashirakon.AppManager.logs.Log; // Copyright 2012 Nolan Lawson public class ServiceHelper { public static final String TAG = ServiceHelper.class.getSimpleName(); public static void startOrStopCrazyLogger(Context context) { if (BuildConfig.DEBUG) { Intent intent = new Intent(context, CrazyLoggerService.class); if (!context.stopService(intent)) { // Service wasn't running context.startService(intent); } } } public static synchronized void stopBackgroundServiceIfRunning(Context context) { boolean alreadyRunning = ServiceHelper.checkIfServiceIsRunning(context, LogcatRecordingService.class); Log.d(TAG, "Is LogcatRecordingService running: %s", alreadyRunning); if (alreadyRunning) { Intent intent = new Intent(context, LogcatRecordingService.class); context.stopService(intent); } } @Nullable public static synchronized Intent getLogcatRecorderServiceIfNotAlreadyRunning(Context context, String filename, String queryFilter, int logLevel) { boolean alreadyRunning = ServiceHelper.checkIfServiceIsRunning(context, LogcatRecordingService.class); if (alreadyRunning) { return null; } Intent intent = new Intent(context, LogcatRecordingService.class); intent.putExtra(LogcatRecordingService.EXTRA_FILENAME, filename); // Load "lastLine" in the background LogcatReaderLoader loader = LogcatReaderLoader.create(true); IntentCompat.putWrappedParcelableExtra(intent, LogcatRecordingService.EXTRA_LOADER, loader); // Add query text and log level intent.putExtra(LogcatRecordingService.EXTRA_QUERY_FILTER, queryFilter); intent.putExtra(LogcatRecordingService.EXTRA_LEVEL, logLevel); return intent; } public static boolean checkIfServiceIsRunning(Context context, Class service) { String serviceName = service.getName(); ComponentName componentName = new ComponentName(context.getPackageName(), serviceName); ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); List procList = activityManager.getRunningServices(Integer.MAX_VALUE); if (procList != null) { for (ActivityManager.RunningServiceInfo appProcInfo : procList) { if (appProcInfo != null && componentName.equals(appProcInfo.service)) { Log.d(TAG, "%s is already running.", serviceName); return true; } } } Log.d(TAG, "%s is not running.", serviceName); return false; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/logcat/helper/WidgetHelper.java ================================================ // SPDX-License-Identifier: WTFPL AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.logcat.helper; import android.app.PendingIntent; import android.appwidget.AppWidgetManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.widget.RemoteViews; import androidx.annotation.NonNull; import androidx.core.app.PendingIntentCompat; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.logcat.LogcatRecordingService; import io.github.muntashirakon.AppManager.logcat.RecordingWidgetProvider; import io.github.muntashirakon.AppManager.logs.Log; // Copyright 2012 Nolan Lawson public class WidgetHelper { public static void updateWidgets(Context context) { int[] appWidgetIds = findAppWidgetIds(context); updateWidgets(context, appWidgetIds); } /** * manually tell us if the service is running or not */ public static void updateWidgets(Context context, boolean serviceRunning) { int[] appWidgetIds = findAppWidgetIds(context); updateWidgets(context, appWidgetIds, serviceRunning); } public static void updateWidgets(Context context, int[] appWidgetIds) { boolean serviceRunning = ServiceHelper.checkIfServiceIsRunning(context, LogcatRecordingService.class); updateWidgets(context, appWidgetIds, serviceRunning); } public static void updateWidgets(Context context, @NonNull int[] appWidgetIds, boolean serviceRunning) { AppWidgetManager manager = AppWidgetManager.getInstance(context); for (int appWidgetId : appWidgetIds) { if (!PreferenceHelper.getWidgetExistsPreference(context, appWidgetId)) { // android has a bug that sometimes keeps stale app widget ids around Log.d("WidgetHelper", "Found stale app widget id %d; skipping...", appWidgetId); continue; } updateWidget(context, manager, appWidgetId, serviceRunning); } } private static void updateWidget(Context context, AppWidgetManager manager, int appWidgetId, boolean serviceRunning) { RemoteViews updateViews = new RemoteViews(context.getPackageName(), R.layout.widget_recording); // change the subtext depending on whether the service is running or not CharSequence subtext = context.getText(serviceRunning ? R.string.widget_recording_in_progress : R.string.widget_start_recording); updateViews.setTextViewText(R.id.widget_subtext, subtext); PendingIntent pendingIntent = getPendingIntent(context, appWidgetId); updateViews.setOnClickPendingIntent(R.id.clickable_linear_layout, pendingIntent); manager.updateAppWidget(appWidgetId, updateViews); } private static PendingIntent getPendingIntent(Context context, int appWidgetId) { Intent intent = new Intent(context, RecordingWidgetProvider.class); intent.setAction(RecordingWidgetProvider.ACTION_RECORD_OR_STOP); intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); // gotta make this unique for this appwidgetid - otherwise, the PendingIntents conflict // it seems to be a quasi-bug in Android Uri data = Uri.withAppendedPath(Uri.parse(RecordingWidgetProvider.URI_SCHEME + "://widget/id/#"), String.valueOf(appWidgetId)); intent.setData(data); return PendingIntentCompat.getBroadcast(context, 0 /* no requestCode */, intent, PendingIntent.FLAG_UPDATE_CURRENT, false); } private static int[] findAppWidgetIds(Context context) { AppWidgetManager manager = AppWidgetManager.getInstance(context); ComponentName widget = new ComponentName(context, RecordingWidgetProvider.class); return manager.getAppWidgetIds(widget); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/logcat/reader/AbsLogcatReader.java ================================================ // SPDX-License-Identifier: WTFPL AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.logcat.reader; // Copyright 2012 Nolan Lawson abstract class AbsLogcatReader implements LogcatReader { protected boolean recordingMode; public AbsLogcatReader(boolean recordingMode) { this.recordingMode = recordingMode; } public boolean isRecordingMode() { return recordingMode; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/logcat/reader/LogcatReader.java ================================================ // SPDX-License-Identifier: WTFPL AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.logcat.reader; import java.io.IOException; import java.util.List; // Copyright 2012 Nolan Lawson public interface LogcatReader { /** * Read a single log line, ala {@link java.io.BufferedReader#readLine()}. * * @return A single log line */ String readLine() throws IOException; /** * Kill the reader and close all resources without throwing any exceptions. */ void killQuietly(); @SuppressWarnings("BooleanMethodIsAlwaysInverted") boolean readyToRecord(); List getProcesses(); } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/logcat/reader/LogcatReaderLoader.java ================================================ // SPDX-License-Identifier: WTFPL AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.logcat.reader; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; import io.github.muntashirakon.AppManager.logcat.helper.LogcatHelper; import io.github.muntashirakon.AppManager.logcat.helper.PreferenceHelper; import io.github.muntashirakon.util.ParcelUtils; // Copyright 2012 Nolan Lawson // Copyright 2021 Muntashir Al-Islam public class LogcatReaderLoader implements Parcelable { private final Map mLastLines; private final boolean mRecordingMode; private final boolean mMultipleBuffers; private LogcatReaderLoader(@LogcatHelper.LogBufferId @NonNull List buffers, boolean recordingMode) { this.mRecordingMode = recordingMode; this.mMultipleBuffers = buffers.size() > 1; this.mLastLines = new HashMap<>(); for (Integer buffer : buffers) { // No need to grab the last line if this isn't recording mode String lastLine = recordingMode ? LogcatHelper.getLastLogLine(buffer) : null; mLastLines.put(buffer, lastLine); } } @NonNull public static LogcatReaderLoader create(boolean recordingMode) { List buffers = PreferenceHelper.getBuffers(); return new LogcatReaderLoader(buffers, recordingMode); } public LogcatReader loadReader() throws IOException { LogcatReader reader; if (!mMultipleBuffers) { // single reader Integer buffers = mLastLines.keySet().iterator().next(); String lastLine = mLastLines.values().iterator().next(); reader = new SingleLogcatReader(mRecordingMode, buffers, lastLine); } else { // multiple reader reader = new MultipleLogcatReader(mRecordingMode, mLastLines); } return reader; } @Override public int describeContents() { return 0; } public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { public LogcatReaderLoader createFromParcel(Parcel in) { return new LogcatReaderLoader(in); } public LogcatReaderLoader[] newArray(int size) { return new LogcatReaderLoader[size]; } }; @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeInt(mRecordingMode ? 1 : 0); dest.writeInt(mMultipleBuffers ? 1 : 0); ParcelUtils.writeMap(mLastLines, dest); } private LogcatReaderLoader(@NonNull Parcel in) { this.mRecordingMode = in.readInt() == 1; this.mMultipleBuffers = in.readInt() == 1; this.mLastLines = ParcelUtils.readMap(in, Integer.class.getClassLoader(), String.class.getClassLoader()); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/logcat/reader/MultipleLogcatReader.java ================================================ // SPDX-License-Identifier: WTFPL AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.logcat.reader; import java.io.IOException; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import io.github.muntashirakon.AppManager.logcat.helper.LogcatHelper; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.utils.ThreadUtils; /** * Combines multiple buffered readers into a single reader that merges all input synchronously. */ // Copyright 2012 Nolan Lawson public class MultipleLogcatReader extends AbsLogcatReader { public static final String TAG = MultipleLogcatReader.class.getSimpleName(); private static final String DUMMY_NULL = ""; // Stop marker private final List mReaderThreads = new LinkedList<>(); private final BlockingQueue mQueue = new ArrayBlockingQueue<>(1); public MultipleLogcatReader(boolean recordingMode, Map lastLines) throws IOException { super(recordingMode); // Read from all three buffers all at once for (Entry entry : lastLines.entrySet()) { Integer buffers = entry.getKey(); String lastLine = entry.getValue(); ReaderThread readerThread = new ReaderThread(buffers, lastLine); readerThread.start(); mReaderThreads.add(readerThread); } } public String readLine() throws IOException { try { String value = mQueue.take(); if (!value.equals(DUMMY_NULL)) { return value; } } catch (InterruptedException e) { Log.e(TAG, e); } return null; } @Override public boolean readyToRecord() { for (ReaderThread thread : mReaderThreads) { if (!thread.mReader.readyToRecord()) { return false; } } return true; } @Override public void killQuietly() { for (ReaderThread thread : mReaderThreads) { thread.mKilled = true; } // Kill all threads in the background ThreadUtils.postOnBackgroundThread(() -> { for (ReaderThread thread : mReaderThreads) { thread.mReader.killQuietly(); } mQueue.offer(DUMMY_NULL); }); } @Override public List getProcesses() { List result = new ArrayList<>(); for (ReaderThread thread : mReaderThreads) { result.addAll(thread.mReader.getProcesses()); } return result; } private class ReaderThread extends Thread { private final SingleLogcatReader mReader; private boolean mKilled; public ReaderThread(@LogcatHelper.LogBufferId int logBuffer, String lastLine) throws IOException { mReader = new SingleLogcatReader(recordingMode, logBuffer, lastLine); } @Override public void run() { String line; try { while (!mKilled && (line = mReader.readLine()) != null && !mKilled) { mQueue.put(line); } } catch (IOException | InterruptedException e) { Log.e(TAG, e); } Log.w(TAG, "Thread died"); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/logcat/reader/ScrubberUtils.java ================================================ // SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.logcat.reader; import java.util.regex.Pattern; // Copyright 2014 CyanogenMod Project public class ScrubberUtils { private static final Pattern EMAIL_PATTERN = Pattern.compile("[a-zA-Z0-9_]+(?:\\.[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+)*(@|%40)(?!([a-zA-Z0-9]*\\.[a-zA-Z0-9]*\\.[a-zA-Z0-9]*\\.))(?:[A-Za-z0-9](?:[a-zA-Z0-9-]*[A-Za-z0-9])?\\.)+[a-zA-Z](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?"); private static final Pattern PHONE_NUMBER_PATTERN = Pattern.compile("^(?:(?:\\+?1\\s*(?:[.-]\\s*)?)?(?:\\(\\s*([2-9]1[02-9]|[2-9][02-8]1|[2-9][02-8][02-9])\\s*\\)|([2-9]1[02-9]|[2-9][02-8]1|[2-9][02-8][02-9]))\\s*(?:[.-]\\s*)?)?([2-9]1[02-9]|[2-9][02-9]1|[2-9][02-9]{2})\\s*(?:[.-]\\s*)?([0-9]{4})(?:\\s*(?:#|x\\.?|ext\\.?|extension)\\s*(\\d+))?$"); private static final Pattern WEB_URL_PATTERN = Pattern.compile("\\b(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]"); private static final Pattern IP_ADDRESS_PATTERN = Pattern.compile("^([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])$"); private static final Pattern PHONE_INFO_PATTERN = Pattern.compile("(msisdn=|mMsisdn=|iccid=|iccid: |mImsi=)[a-zA-Z0-9]*", Pattern.CASE_INSENSITIVE); private static final Pattern USER_INFO_PATTERN = Pattern.compile("(UserInfo\\{\\d:)[a-zA-Z0-9\\s]*", Pattern.CASE_INSENSITIVE); private static final Pattern ACCOUNT_INFO_PATTERN = Pattern.compile("(Account \\{name=)[a-zA-Z0-9]*", Pattern.CASE_INSENSITIVE); private static final String IGNORE_DATA_RESOURCE_CACHE = "/data/resource-cache"; private static final String IGNORE_DATA_DALVIK_CACHE = "/data/dalvik-cache"; private static final String IGNORE_CACHE_DALVIK_CACHE = "/cache/dalvik-cache"; public static String scrubLine(String line) { if (line.contains(IGNORE_DATA_RESOURCE_CACHE) || line.contains(IGNORE_DATA_DALVIK_CACHE) || line.contains(IGNORE_CACHE_DALVIK_CACHE)) { // Ugly work around :/ return line; } line = IP_ADDRESS_PATTERN.matcher(line).replaceAll(""); line = EMAIL_PATTERN.matcher(line).replaceAll(""); line = PHONE_NUMBER_PATTERN.matcher(line).replaceAll(""); line = WEB_URL_PATTERN.matcher(line).replaceAll(""); line = PHONE_INFO_PATTERN.matcher(line).replaceAll(""); line = USER_INFO_PATTERN.matcher(line).replaceAll(""); line = ACCOUNT_INFO_PATTERN.matcher(line).replaceAll(""); return line; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/logcat/reader/SingleLogcatReader.java ================================================ // SPDX-License-Identifier: WTFPL AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.logcat.reader; import android.text.TextUtils; import androidx.annotation.Nullable; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.Collections; import java.util.List; import io.github.muntashirakon.AppManager.logcat.helper.LogcatHelper; import io.github.muntashirakon.AppManager.logs.Log; // Copyright 2012 Nolan Lawson public class SingleLogcatReader extends AbsLogcatReader { private final Process mLogcatProcess; private final BufferedReader mBufferedReader; @Nullable private String mLastLine; public SingleLogcatReader(boolean recordingMode, @LogcatHelper.LogBufferId int buffers, @Nullable String lastLine) throws IOException { super(recordingMode); mLastLine = lastLine; // Use the "time" log so we can see what time the logs were logged at mLogcatProcess = LogcatHelper.getLogcatProcess(buffers); mBufferedReader = new BufferedReader(new InputStreamReader(mLogcatProcess.getInputStream()), 8192); } @Override public void killQuietly() { if (mLogcatProcess != null) { mLogcatProcess.destroy(); Log.d("SLR", "killed 1 logcat process"); } } @Override public String readLine() throws IOException { String line = mBufferedReader.readLine(); if (recordingMode && mLastLine != null) { // Still skipping past the 'last line' if (mLastLine.equals(line) /*|| isAfterLastTime(line)*/) { mLastLine = null; // Indicates we've passed the last line } } return line; } private boolean isAfterLastTime(String line) { if (mLastLine == null) { return false; } // Doing a string comparison is sufficient to determine whether this line is chronologically // after the last line, because the format they use is exactly the same and // lists larger time period before smaller ones return isDatedLogLine(mLastLine) && isDatedLogLine(line) && line.compareTo(mLastLine) > 0; } private boolean isDatedLogLine(String line) { // 18 is the size of the logcat timestamp return (!TextUtils.isEmpty(line) && line.length() >= 18 && Character.isDigit(line.charAt(0))); } @Override public boolean readyToRecord() { return recordingMode && mLastLine == null; } @Override public List getProcesses() { return Collections.singletonList(mLogcatProcess); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/logcat/struct/LogLine.java ================================================ // SPDX-License-Identifier: WTFPL AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.logcat.struct; import android.os.RemoteException; import android.text.TextUtils; import android.util.Log; import android.util.LruCache; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; import io.github.muntashirakon.AppManager.compat.PackageManagerCompat; import io.github.muntashirakon.AppManager.logcat.helper.LogcatHelper; import io.github.muntashirakon.AppManager.logcat.reader.ScrubberUtils; import io.github.muntashirakon.AppManager.users.Owners; // Copyright 2012 Nolan Lawson // Copyright 2021 Muntashir Al-Islam public class LogLine { public static final String TAG = LogLine.class.getSimpleName(); public static final int LOG_FATAL = 15; /** * %s %5d %5d %c %-8s: * %s %s%5d %5d %c %-8s: (Android 7+) * * @see LogcatHelper#getLogcatArgs(int, boolean) */ private static final Pattern LOG_PATTERN = Pattern.compile( // Timestamp "(\\d{2}-\\d{2}\\s\\d{2}:\\d{2}:\\d{2}\\.\\d{3})\\s+" + // UID PID "(.+\\d+)\\s+" + // TID "(\\d+)\\s+" + // Log level "([ADEIVWF])\\s+" + // Tag "(.+?)" + // Message ": (.*)"); /** * This is the old pattern used prior to v4.0.0. Format: {timestamp} {level}/{tag}(\s{pid}): message */ private static final Pattern LOG_PATTERN_LEGACY = Pattern.compile( // Timestamp "(\\d{2}-\\d{2}\\s\\d{2}:\\d{2}:\\d{2}\\.\\d{3})\\s+" + // Log level "([ADEIVWF])/" + // Tag "([^(].+)" + // PID with optional * prefixed number seen on ZTE blade (Android 4.4) "\\(\\s*(\\d+)(?:\\*\\s*\\d+)?\\)" + // Message ": (.*)"); private static final String BEGIN = "--------- beginning of "; public static boolean omitSensitiveInfo = false; @Nullable public static LogLine newLogLine(@NonNull String originalLine, boolean expanded, @Nullable Pattern filterPattern) { LogLine logLine = new LogLine(originalLine); logLine.setExpanded(expanded); if (matchPattern(originalLine, logLine)) { if (filterPattern != null && filterPattern.matcher(logLine.getTagName()).matches()) { return null; } return logLine; } if (matchPatternLegacy(originalLine, logLine)) { if (filterPattern != null && filterPattern.matcher(logLine.getTagName()).matches()) { return null; } return logLine; } if (originalLine.startsWith(BEGIN)) { Log.d(TAG, "Started buffer: " + originalLine.substring(BEGIN.length())); return null; } else { Log.w(TAG, "Line doesn't match pattern: " + originalLine); logLine.setLogOutput(originalLine); logLine.setLogLevel(-1); } return logLine; } public static int convertCharToLogLevel(char logLevelChar) { switch (logLevelChar) { case 'A': return Log.ASSERT; case 'D': return Log.DEBUG; case 'E': return Log.ERROR; case 'I': return Log.INFO; case 'V': return Log.VERBOSE; case 'W': return Log.WARN; case 'F': return LOG_FATAL; } return -1; } public static char convertLogLevelToChar(int logLevel) { switch (logLevel) { case Log.ASSERT: return 'A'; case Log.DEBUG: return 'D'; case Log.ERROR: return 'E'; case Log.INFO: return 'I'; case Log.VERBOSE: return 'V'; case Log.WARN: return 'W'; case LOG_FATAL: return 'F'; } return ' '; } @NonNull private final String mOriginalLine; @Nullable private String mTimestamp; private int mLogLevel; private String mTagName; private String mLogOutput; private int mPid = -1; private int mTid = -1; private int mUid = -1; @Nullable private String mUidOwner; @Nullable private String mPackageName; private boolean mExpanded = false; public LogLine(@NonNull String originalLine) { mOriginalLine = originalLine; } public String getOriginalLine() { return mOriginalLine; } public String getProcessIdText() { return Character.toString(convertLogLevelToChar(mLogLevel)); } public int getLogLevel() { return mLogLevel; } public void setLogLevel(int logLevel) { mLogLevel = logLevel; } public String getTagName() { return mTagName; } public void setTag(String tag) { mTagName = tag; } public String getLogOutput() { return mLogOutput; } public void setLogOutput(String logOutput) { if (omitSensitiveInfo) { mLogOutput = ScrubberUtils.scrubLine(logOutput); } else { mLogOutput = logOutput; } } public int getPid() { return mPid; } public void setPid(int pid) { mPid = pid; } public int getTid() { return mTid; } public void setTid(int tid) { mTid = tid; } public int getUid() { return mUid; } public void setUid(int uid) { mUid = uid; } @Nullable public String getUidOwner() { return mUidOwner; } public void setUidOwner(@Nullable String owner) { mUidOwner = owner; } @Nullable public String getPackageName() { return mPackageName; } public void setPackageName(@Nullable String packageName) { mPackageName = packageName; } @Nullable public String getTimestamp() { return mTimestamp; } public void setTimestamp(@Nullable String timestamp) { mTimestamp = timestamp; } public boolean isExpanded() { return mExpanded; } public void setExpanded(boolean expanded) { this.mExpanded = expanded; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof LogLine)) return false; LogLine logLine = (LogLine) o; return mOriginalLine.equals(logLine.mOriginalLine); } @Override public int hashCode() { return Objects.hash(mOriginalLine); } @NonNull @Override public String toString() { return mOriginalLine; } private static boolean matchPatternLegacy(@NonNull String originalLine, @NonNull LogLine logLine) { Matcher matcher = LOG_PATTERN_LEGACY.matcher(originalLine); if (!matcher.matches()) { return false; } // Group 1: Timestamp logLine.setTimestamp(Objects.requireNonNull(matcher.group(1))); // Group 2: Log level logLine.setLogLevel(convertCharToLogLevel(Objects.requireNonNull(matcher.group(2)).charAt(0))); // Group 3: Tag logLine.setTag(Objects.requireNonNull(matcher.group(3)).trim()); // Group 4: PID logLine.setPid(Integer.parseInt(matcher.group(4))); // Group 5: Message logLine.setLogOutput(Objects.requireNonNull(matcher.group(5))); return true; } private static boolean matchPattern(@NonNull String originalLine, @NonNull LogLine logLine) { Matcher matcher = LOG_PATTERN.matcher(originalLine); if (!matcher.matches()) { return false; } // Group 1: Timestamp logLine.setTimestamp(Objects.requireNonNull(matcher.group(1))); // Group 2: UID PID String[] uidPid = Objects.requireNonNull(matcher.group(2)).split("\\s+", 2); if (uidPid.length == 2) { String owner = uidPid[0]; int uid = Owners.parseUid(owner); logLine.setUidOwner(owner); logLine.setUid(uid); // Set package name logLine.setPackageName(retrievePackageName(uid)); } logLine.setPid(Integer.parseInt(uidPid[uidPid.length == 2 ? 1 : 0])); // Group 3: TID logLine.setTid(Integer.parseInt(matcher.group(3))); // Group 4: Log level logLine.setLogLevel(convertCharToLogLevel(Objects.requireNonNull(matcher.group(4)).charAt(0))); // Group 5: Tag logLine.setTag(Objects.requireNonNull(matcher.group(5)).trim()); // Group 6: Message logLine.setLogOutput(Objects.requireNonNull(matcher.group(6))); return true; } private static final LruCache sUidPackageNameCache = new LruCache<>(300); @Nullable private static String retrievePackageName(int uid) { if (uid < 0) { return null; } String packageName = sUidPackageNameCache.get(uid); if (packageName != null) { return TextUtils.isEmpty(packageName) ? null : packageName; } // TODO: 1/18/25 // Assumptions for multiple UIDs: // 1. Process name likely matches/starts with the package name // 2. Shortest package name is preferred (the primary package in a shared UID is likely to have the shortest package name) // Ignored assumption: // 3. Primary package is likely to be installed first try { String[] packages = PackageManagerCompat.getPackageManager().getPackagesForUid(uid); String selectedPackage = null; if (packages == null || packages.length == 0) { selectedPackage = null; } else { if (packages.length == 1) { selectedPackage = packages[0]; } else { int shortestIndex = 0; for (int i = 0; i < packages.length; ++i) { if (packages[shortestIndex].length() > packages[i].length()) { shortestIndex = i; } } if (selectedPackage == null) { selectedPackage = packages[shortestIndex]; } } } if (selectedPackage != null) { sUidPackageNameCache.put(uid, selectedPackage); } else { // Still cache this data sUidPackageNameCache.put(uid, ""); } return selectedPackage; } catch (RemoteException e) { return null; } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/logcat/struct/SavedLog.java ================================================ // SPDX-License-Identifier: WTFPL AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.logcat.struct; import java.util.List; // Copyright 2012 Nolan Lawson public class SavedLog { private final List mLogLines; private final boolean mTruncated; public SavedLog(List logLines, boolean truncated) { mLogLines = logLines; mTruncated = truncated; } public List getLogLines() { return mLogLines; } public boolean isTruncated() { return mTruncated; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/logcat/struct/SearchCriteria.java ================================================ // SPDX-License-Identifier: WTFPL AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.logcat.struct; import android.os.Build; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringDef; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.regex.Pattern; import io.github.muntashirakon.AppManager.utils.ArrayUtils; // Copyright 2012 Nolan Lawson public class SearchCriteria { @Retention(RetentionPolicy.SOURCE) @StringDef({TYPE_MSG, TYPE_PID, TYPE_PKG, TYPE_TAG, TYPE_UID}) private @interface FilterType { } private static final String TYPE_MSG = "msg"; private static final String TYPE_PID = "pid"; private static final String TYPE_PKG = "pkg"; private static final String TYPE_TAG = "tag"; private static final String TYPE_UID = "uid"; private static final String[] TYPES = new String[]{ TYPE_MSG, TYPE_PID, TYPE_PKG, TYPE_TAG, TYPE_UID, }; public static final String PID_KEYWORD = TYPE_PID + ":"; public static final String PKG_KEYWORD = TYPE_PKG + "=:"; public static final String TAG_KEYWORD = TYPE_TAG + "=:"; public static final String UID_KEYWORD = TYPE_UID + "=:"; @Nullable public final String query; private final List mFilters = new ArrayList<>(); public SearchCriteria(@Nullable String query) { this.query = query; if (query == null) { return; } String[] parts = query.split(" "); // Check for keywords, we support the following keywords: // 1. pid: // 2. pkg:|"" // 3. proc:|"" // 4. tag:|"" // Each can take regex if it has ~: separator instead of : // For exact match, it has =: instead of : // Each can be inverse if it begins with - StringBuilder lastString = null; StringBuilder queryString = new StringBuilder(); for (String part : parts) { if (lastString != null) { // This part belongs to the last filter if (part.endsWith("\"")) { Filter filter = mFilters.get(mFilters.size() - 1); filter.setValue(lastString + " " + part.substring(0, parts.length - 1)); lastString = null; } else lastString.append(" ").append(part); continue; } int colon = part.indexOf(":"); if (colon > 0) { String type = part.substring(0, colon); boolean inv = type.startsWith("-"); boolean regex = type.endsWith("~"); boolean exact = type.endsWith("="); // Check for inverse if (inv && type.length() > 1) type = type.substring(1); if (regex && type.length() > 1) type = type.substring(0, type.length() - 1); if (exact && type.length() > 1) type = type.substring(0, type.length() - 1); if (ArrayUtils.contains(TYPES, type)) { // Valid type Filter filter = new Filter(type, regex, inv, exact); mFilters.add(filter); if (colon + 1 < part.length()) { String value = part.substring(colon + 1); // Check if value begins with quote if (value.startsWith("\"")) { if (value.length() > 1) { if (value.endsWith("\"")) { filter.setValue(value.substring(1, value.length() - 1)); } else { // Value didn't end here lastString = new StringBuilder(value.substring(1)); } } else lastString = new StringBuilder(); } else filter.setValue(value); } continue; } } // Query string queryString.append(" ").append(part); } String text = queryString.toString().trim(); if (!text.isEmpty()) { mFilters.add(new Filter(TYPE_MSG, queryString.toString().trim())); } } public boolean isEmpty() { for (Filter filter : mFilters) { if (!filter.isEmpty()) { return false; } } return true; } public boolean matches(LogLine logLine) { // Consider the criteria to be ANDed for (Filter filter : mFilters) { if (!filter.matches(logLine)) { return false; } } return true; } private static class Filter { @FilterType private final String mType; private final boolean mRegex; private final boolean mExact; private final boolean mInverse; @Nullable private Object mValue; public Filter(@FilterType String type, boolean regex, boolean inverse, boolean exact) { mType = type; mRegex = regex; mInverse = inverse; mExact = exact; } public Filter(@FilterType String type, @Nullable String value) { mType = type; mRegex = false; mInverse = false; mExact = false; mValue = getRealValue(value); } public void setValue(@Nullable String value) { mValue = getRealValue(value); } public boolean isEmpty() { if (mValue instanceof CharSequence) { return TextUtils.isEmpty((CharSequence) mValue); } return mValue == null; } /** * @noinspection DataFlowIssue */ public boolean matches(@NonNull LogLine logLine) { if (isEmpty()) { // Empty always matches return true; } boolean matches; switch (mType) { case TYPE_MSG: { String tag = logLine.getTagName(); String out = logLine.getLogOutput(); if (tag == null && out == null) { return false; } if (mRegex) { Pattern p = (Pattern) mValue; matches = matchPattern(p, tag) || matchPattern(p, out); } else { String query = (String) mValue; matches = matchQuery(query, tag, mExact) || matchQuery(query, tag, mExact); } break; } case TYPE_PID: { if (mRegex) { matches = matchPattern((Pattern) mValue, String.valueOf(logLine.getPid())); } else { matches = logLine.getPid() == ((int) mValue); } break; } case TYPE_PKG: { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { // Package name is not available for these versions // Use match-all return true; } String packageName = logLine.getPackageName(); if (mRegex) { matches = matchPattern((Pattern) mValue, packageName); } else { matches = matchQuery((String) mValue, packageName, mExact); } break; } case TYPE_UID: { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { // UID is not available for these versions // Use match-all return true; } String owner = logLine.getUidOwner(); int uid = logLine.getUid(); if (mRegex) { Pattern p = (Pattern) mValue; matches = matchPattern(p, owner) || matchPattern(p, String.valueOf(uid)); } else { if (mValue instanceof Integer) { matches = uid == ((int) mValue); } else matches = matchQuery((String) mValue, owner, mExact); } break; } case TYPE_TAG: { String tag = logLine.getTagName(); if (mRegex) { matches = matchPattern((Pattern) mValue, tag); } else { matches = matchQuery((String) mValue, tag, mExact); } break; } default: throw new IllegalArgumentException("Invalid filter: " + mType); } return mInverse != matches; } @Nullable private Object getRealValue(@Nullable String value) { if (value == null) { return null; } if (mRegex) { return Pattern.compile(Pattern.quote(value)); } switch (mType) { case TYPE_UID: { if (TextUtils.isDigitsOnly(value)) { return Integer.parseInt(value); } // else fallthrough } case TYPE_MSG: case TYPE_PKG: case TYPE_TAG: { return mExact ? value : value.toLowerCase(Locale.ROOT); } case TYPE_PID: { return TextUtils.isDigitsOnly(value) ? Integer.parseInt(value) : null; } default: throw new IllegalArgumentException("Invalid filter: " + mType); } } @NonNull @Override public String toString() { return "Filter{" + "mType='" + mType + '\'' + ", mRegex=" + mRegex + ", mExact=" + mExact + ", mInverse=" + mInverse + ", mValue=" + mValue + '}'; } private static boolean matchPattern(@NonNull Pattern pattern, @Nullable String value) { if (value == null) { return false; } return pattern.matcher(value).matches(); } private static boolean matchQuery(@NonNull String query, @Nullable String value, boolean exact) { if (value == null) { return false; } return exact ? value.equals(query) : value.toLowerCase(Locale.ROOT).contains(query); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/logcat/struct/SendLogDetails.java ================================================ // SPDX-License-Identifier: WTFPL AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.logcat.struct; import androidx.annotation.Nullable; import io.github.muntashirakon.io.Path; // Copyright 2012 Nolan Lawson // Copyright 2021 Muntashir Al-Islam public class SendLogDetails { @Nullable private String mSubject; @Nullable private Path mAttachment; @Nullable private String mAttachmentType; @Nullable public String getSubject() { return mSubject; } public void setSubject(@Nullable String subject) { mSubject = subject; } @Nullable public Path getAttachment() { return mAttachment; } public void setAttachment(@Nullable Path attachment) { mAttachment = attachment; } @Nullable public String getAttachmentType() { return mAttachmentType; } public void setAttachmentType(@Nullable String attachmentType) { mAttachmentType = attachmentType; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/magisk/MagiskDenyList.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.magisk; import static io.github.muntashirakon.AppManager.magisk.MagiskUtils.ISOLATED_MAGIC; import android.content.pm.PackageInfo; import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; import java.util.Collection; import java.util.List; import io.github.muntashirakon.AppManager.runner.Runner; import io.github.muntashirakon.AppManager.settings.Ops; @WorkerThread public class MagiskDenyList { /** * Whether Magisk DenyList is available. */ public static boolean available() { return Ops.isWorkingUidRoot() && Runner.runCommand(new String[]{"magisk", "--denylist", "ls"}).isSuccessful(); } /** * Enable Magisk DenyList if it is not already enabled. * * @return {@code true} iff Magisk DenyList is enabled. */ public static boolean enableIfNotAlready(boolean forceEnable) { // Check DenyList status if (!Runner.runCommand(new String[]{"magisk", "--denylist", "status"}).isSuccessful()) { // Enable DenyList if (forceEnable) { return Runner.runCommand(new String[]{"magisk", "--denylist", "enable"}).isSuccessful(); } else return false; } else return true; } public static boolean apply(@NonNull MagiskProcess magiskProcess, boolean forceEnable) { String packageName = magiskProcess.isIsolatedProcess() && !magiskProcess.isAppZygote() ? ISOLATED_MAGIC : magiskProcess.packageName; if (magiskProcess.isEnabled()) { return add(packageName, magiskProcess.name, forceEnable); } return remove(packageName, magiskProcess.name); } private static boolean add(String packageName, String processName, boolean forceEnable) { // Check DenyList status if (!enableIfNotAlready(forceEnable)) return false; // DenyList is enabled, enable hide for the package return Runner.runCommand(new String[]{"magisk", "--denylist", "add", packageName, processName}).isSuccessful(); } private static boolean remove(String packageName, String processName) { // Disable hide for the package (don't need to check for status) return Runner.runCommand(new String[]{"magisk", "--denylist", "rm", packageName, processName}).isSuccessful(); } @NonNull public static List getProcesses(@NonNull PackageInfo packageInfo) { return MagiskUtils.getProcesses(packageInfo, getProcesses(packageInfo.packageName)); } @NonNull public static Collection getProcesses(@NonNull String packageName) { Runner.Result result = Runner.runCommand(new String[]{"magisk", "--denylist", "ls"}); return MagiskUtils.parseProcesses(packageName, result); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/magisk/MagiskHide.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.magisk; import static io.github.muntashirakon.AppManager.magisk.MagiskUtils.ISOLATED_MAGIC; import android.content.pm.PackageInfo; import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; import java.util.Collection; import java.util.List; import io.github.muntashirakon.AppManager.runner.Runner; import io.github.muntashirakon.AppManager.settings.Ops; @WorkerThread public class MagiskHide { /** * Whether MagiskHide is available. */ public static boolean available() { return Ops.isWorkingUidRoot() && Runner.runCommand(new String[]{"command", "-v", "magiskhide"}).isSuccessful(); } /** * Enable MagiskHide if it is not already enabled. * * @return {@code true} iff MagiskHide is enabled. */ public static boolean enableIfNotAlready(boolean forceEnable) { // Check MagiskHide status if (!Runner.runCommand(new String[]{"magiskhide", "status"}).isSuccessful()) { // Enable MagiskHide if (forceEnable) { return Runner.runCommand(new String[]{"magiskhide", "enable"}).isSuccessful(); } else return false; } else return true; } public static boolean apply(@NonNull MagiskProcess magiskProcess, boolean forceEnable) { String packageName = magiskProcess.isIsolatedProcess() && !magiskProcess.isAppZygote() ? ISOLATED_MAGIC : magiskProcess.packageName; if (magiskProcess.isEnabled()) { return add(packageName, magiskProcess.name, forceEnable); } return remove(packageName, magiskProcess.name); } private static boolean add(String packageName, String processName, boolean forceEnable) { // Check MagiskHide status if (!enableIfNotAlready(forceEnable)) return false; // MagiskHide is enabled, enable hide for the package return Runner.runCommand(new String[]{"magiskhide", "add", packageName, processName}).isSuccessful(); } private static boolean remove(String packageName, String processName) { // Disable hide for the package (don't need to check for status) return Runner.runCommand(new String[]{"magiskhide", "rm", packageName, processName}).isSuccessful(); } @NonNull public static List getProcesses(@NonNull PackageInfo packageInfo) { return MagiskUtils.getProcesses(packageInfo, getProcesses(packageInfo.packageName)); } @NonNull public static Collection getProcesses(@NonNull String packageName) { Runner.Result result = Runner.runCommand(new String[]{"magiskhide", "ls"}); return MagiskUtils.parseProcesses(packageName, result); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/magisk/MagiskProcess.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.magisk; import androidx.annotation.NonNull; import java.util.Objects; public class MagiskProcess { @NonNull public final String packageName; @NonNull public final String name; private boolean mIsolatedProcess; private boolean mIsRunning; private boolean mIsEnabled; private boolean mIsAppZygote; public MagiskProcess(@NonNull String packageName, @NonNull String name) { this.packageName = packageName; this.name = name; } public MagiskProcess(@NonNull String packageName) { this.packageName = packageName; this.name = packageName; } public MagiskProcess(@NonNull MagiskProcess magiskProcess) { packageName = magiskProcess.packageName; name = magiskProcess.name; mIsolatedProcess = magiskProcess.mIsolatedProcess; mIsRunning = magiskProcess.mIsRunning; mIsEnabled = magiskProcess.mIsEnabled; mIsAppZygote = magiskProcess.mIsAppZygote; } public void setEnabled(boolean enabled) { mIsEnabled = enabled; } public boolean isEnabled() { return mIsEnabled; } public void setIsolatedProcess(boolean isolatedProcess) { mIsolatedProcess = isolatedProcess; } public void setRunning(boolean running) { mIsRunning = running; } public boolean isIsolatedProcess() { return mIsolatedProcess; } public boolean isRunning() { return mIsRunning; } public void setAppZygote(boolean appZygote) { mIsAppZygote = appZygote; } public boolean isAppZygote() { return mIsAppZygote; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof MagiskProcess)) return false; MagiskProcess that = (MagiskProcess) o; return name.equals(that.name); } @Override public int hashCode() { return Objects.hash(name); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/magisk/MagiskUtils.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.magisk; import android.content.pm.ApplicationInfo; import android.content.pm.ComponentInfo; import android.content.pm.PackageInfo; import android.content.pm.ServiceInfo; import android.os.Build; import androidx.annotation.NonNull; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import io.github.muntashirakon.AppManager.runner.Runner; import io.github.muntashirakon.AppManager.utils.AlphanumComparator; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; public class MagiskUtils { // FIXME(20/9/20): This isn't always true, see check_data in util_functions.sh public static final String NVBASE = "/data/adb"; private static boolean sBootMode = false; public static final String ISOLATED_MAGIC = "isolated"; private static final String[] SCAN_PATHS = new String[]{ "/system/app", "/system/priv-app", "/system/preload", "/system/product/app", "/system/product/priv-app", "/system/product/overlay", "/system/vendor/app", "/system/vendor/overlay", "/system/system_ext/app", "/system/system_ext/priv-app", "/system_ext/app", "/system_ext/priv-app", "/vendor/app", "/vendor/overlay", "/product/app", "/product/priv-app", "/product/overlay", }; @NonNull public static Path getModDir() { return Paths.get(NVBASE + "/modules" + (sBootMode ? "_update" : "")); } public static void setBootMode(boolean bootMode) { MagiskUtils.sBootMode = bootMode; } private static List sSystemlessPaths; @NonNull private static List getSystemlessPaths() { if (sSystemlessPaths == null) { sSystemlessPaths = new ArrayList<>(); Path modDir = getModDir(); if (!modDir.canRead()) { // No permission or no-magisk return Collections.emptyList(); } // Get module paths Path[] modulePaths = modDir.listFiles(Path::isDirectory); // Scan module paths for (Path file : modulePaths) { // Get system apk files for (String sysPath : SCAN_PATHS) { // Always NonNull since it's a Linux FS Path[] paths = Objects.requireNonNull(Paths.build(file, sysPath)).listFiles(Path::isDirectory); for (Path path : paths) { if (hasApkFile(path)) { sSystemlessPaths.add(sysPath + "/" + path.getName()); } } } } } return sSystemlessPaths; } public static boolean isSystemlessPath(@NonNull String path) { return getSystemlessPaths().contains(path); } private static boolean hasApkFile(@NonNull Path file) { if (file.isDirectory()) { Path[] files = file.listFiles((dir, name) -> name.endsWith(".apk")); return files.length > 0; } return false; } @NonNull static List getProcesses(@NonNull PackageInfo packageInfo, @NonNull Collection enabledProcesses) { String packageName = packageInfo.packageName; ApplicationInfo applicationInfo = packageInfo.applicationInfo; Map processNameProcessMap = new HashMap<>(); { // Add default process MagiskProcess mp = new MagiskProcess(packageName); mp.setEnabled(enabledProcesses.contains(packageName)); processNameProcessMap.put(packageName, mp); } // Add other processes: order must be preserved if (packageInfo.services != null) { for (ServiceInfo info : packageInfo.services) { if ((info.flags & ServiceInfo.FLAG_ISOLATED_PROCESS) != 0) { // Isolated process if ((info.flags & ServiceInfo.FLAG_USE_APP_ZYGOTE) != 0) { // Uses app zygote String processName = (applicationInfo.processName == null ? applicationInfo.packageName : applicationInfo.processName) + "_zygote"; if (processNameProcessMap.get(processName) == null) { MagiskProcess mp = new MagiskProcess(packageName, processName); mp.setEnabled(enabledProcesses.contains(processName)); mp.setIsolatedProcess(true); mp.setAppZygote(true); processNameProcessMap.put(processName, mp); } } else { String processName = getProcessName(applicationInfo, info) + (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ? (":" + packageName) : ""); if (processNameProcessMap.get(processName) == null) { MagiskProcess mp = new MagiskProcess(packageName, processName); mp.setEnabled(enabledProcesses.contains(processName)); mp.setIsolatedProcess(true); processNameProcessMap.put(processName, mp); } } } else { String processName = getProcessName(applicationInfo, info); if (processNameProcessMap.get(processName) == null) { MagiskProcess mp = new MagiskProcess(packageName, processName); mp.setEnabled(enabledProcesses.contains(processName)); processNameProcessMap.put(processName, mp); } } } } if (packageInfo.activities != null) { for (ComponentInfo info : packageInfo.activities) { String processName = getProcessName(applicationInfo, info); if (processNameProcessMap.get(processName) == null) { MagiskProcess mp = new MagiskProcess(packageName, processName); mp.setEnabled(enabledProcesses.contains(processName)); processNameProcessMap.put(processName, mp); } } } if (packageInfo.providers != null) { for (ComponentInfo info : packageInfo.providers) { String processName = getProcessName(applicationInfo, info); if (processNameProcessMap.get(processName) == null) { MagiskProcess mp = new MagiskProcess(packageName, processName); mp.setEnabled(enabledProcesses.contains(processName)); processNameProcessMap.put(processName, mp); } } } if (packageInfo.receivers != null) { for (ComponentInfo info : packageInfo.receivers) { String processName = getProcessName(applicationInfo, info); if (processNameProcessMap.get(processName) == null) { MagiskProcess mp = new MagiskProcess(packageName, processName); mp.setEnabled(enabledProcesses.contains(processName)); processNameProcessMap.put(processName, mp); } } } List magiskProcesses = new ArrayList<>(processNameProcessMap.values()); Collections.sort(magiskProcesses, (o1, o2) -> AlphanumComparator.compareStringIgnoreCase(o1.name, o2.name)); return magiskProcesses; } @NonNull static Collection parseProcesses(@NonNull String packageName, @NonNull Runner.Result result) { if (!result.isSuccessful()) { // No matches return Collections.emptyList(); } Set processes = new HashSet<>(); for (String line : result.getOutputAsList()) { String[] splits = line.split("\\|", 2); if (splits.length == 1) { // Old style outputs if (splits[0].equals(packageName)) { processes.add(packageName); } // else mismatch due to greedy algorithm } else if (splits.length == 2) { // New style output if (splits[0].equals(packageName) || splits[0].equals(ISOLATED_MAGIC)) { processes.add(splits[1]); } // else mismatch due to greedy algorithm } // else unknown match } return processes; } @NonNull private static String getProcessName(@NonNull ApplicationInfo applicationInfo, @NonNull ComponentInfo info) { // Priority: component process name > application process name > package name return info.processName != null ? info.processName : (applicationInfo.processName != null ? applicationInfo.processName : applicationInfo.packageName); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/main/ApplicationItem.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.main; import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.GET_SIGNING_CERTIFICATES; import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.MATCH_DISABLED_COMPONENTS; import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES; import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.MATCH_UNINSTALLED_PACKAGES; import android.Manifest; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.ComponentInfo; import android.content.pm.FeatureInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageItemInfo; import android.content.pm.PackageManager; import android.content.pm.PermissionInfo; import android.content.pm.ProviderInfo; import android.content.pm.ServiceInfo; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.RemoteException; import android.os.UserHandleHidden; import android.text.TextUtils; import android.util.Pair; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.WorkerThread; import java.io.InputStream; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.TimeUnit; import aosp.libcore.util.EmptyArray; import io.github.muntashirakon.AppManager.StaticDataset; import io.github.muntashirakon.AppManager.apk.signing.SignerInfo; import io.github.muntashirakon.AppManager.backup.BackupUtils; import io.github.muntashirakon.AppManager.compat.AppOpsManagerCompat; import io.github.muntashirakon.AppManager.compat.ApplicationInfoCompat; import io.github.muntashirakon.AppManager.compat.DeviceIdleManagerCompat; import io.github.muntashirakon.AppManager.compat.InstallSourceInfoCompat; import io.github.muntashirakon.AppManager.compat.ManifestCompat; import io.github.muntashirakon.AppManager.compat.PackageManagerCompat; import io.github.muntashirakon.AppManager.compat.PermissionCompat; import io.github.muntashirakon.AppManager.compat.SensorServiceCompat; import io.github.muntashirakon.AppManager.compat.UsageStatsManagerCompat; import io.github.muntashirakon.AppManager.db.entity.Backup; import io.github.muntashirakon.AppManager.debloat.DebloatObject; import io.github.muntashirakon.AppManager.filters.IFilterableAppInfo; import io.github.muntashirakon.AppManager.filters.options.ComponentsOption; import io.github.muntashirakon.AppManager.filters.options.FreezeOption; import io.github.muntashirakon.AppManager.rules.compontents.ComponentUtils; import io.github.muntashirakon.AppManager.rules.compontents.ComponentsBlocker; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.types.PackageSizeInfo; import io.github.muntashirakon.AppManager.usage.AppUsageStatsManager; import io.github.muntashirakon.AppManager.usage.PackageUsageInfo; import io.github.muntashirakon.AppManager.utils.ArrayUtils; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.DigestUtils; import io.github.muntashirakon.AppManager.utils.ExUtils; import io.github.muntashirakon.AppManager.utils.FreezeUtils; import io.github.muntashirakon.AppManager.utils.KeyStoreUtils; import io.github.muntashirakon.AppManager.utils.PackageUtils; import io.github.muntashirakon.io.Path; /** * Stores an application info */ public class ApplicationItem extends PackageItemInfo implements IFilterableAppInfo { /** * Version name */ public String versionName; /** * Version code */ public long versionCode; /** * Backup info */ @Nullable public Backup backup; /** * Application flags. * See {@link android.content.pm.ApplicationInfo#flags} */ public int flags = 0; /** * Kernel user id. * See {@link android.content.pm.ApplicationInfo#uid} */ public int uid = 0; /** * Shared user id name. * See {@link android.content.pm.PackageInfo#sharedUserId} */ @Nullable public String sharedUserId; /** * Application label (or name) */ public String label; /** * True if debuggable, false otherwise */ public boolean debuggable = false; /** * First install time */ public long firstInstallTime = 0L; /** * Last update time */ public Long lastUpdateTime = 0L; /** * Target SDK version */ public Integer targetSdk; /** * Issuer and signature */ @Nullable public Pair sha; /** * Blocked components count */ public Integer blockedCount = 0; public Integer trackerCount = 0; public Long lastActionTime = 0L; public Long dataUsage = 0L; public Long totalSize = 0L; public int openCount = 0; public Long screenTime = 0L; public Long lastUsageTime = 0L; /** * Whether the item is a user app (or system app) */ public boolean isUser; /** * Whether the app is disabled */ public boolean isDisabled; /** * Whether the app is installed */ public boolean isInstalled = true; public boolean isOnlyDataInstalled = true; /** * Whether the app has any activities */ public boolean hasActivities = false; /** * Whether the app has any splits */ public boolean hasSplits = false; public boolean hasKeystore = false; public boolean usesSaf = false; public String ssaid = null; /** * Whether the item is selected */ public boolean isSelected = false; @NonNull public int[] userIds = EmptyArray.INT; // Other info public boolean isStopped; public boolean isSystem; public boolean isPersistent; public boolean usesCleartextTraffic; public boolean canReadLogs; public boolean allowClearingUserData; public boolean isAppInactive; public String uidOrAppIds; public String issuerShortName; public String versionTag; public String appTypePostfix; public String sdkString; public long diffInstallUpdateInDays; public long lastBackupDays; public StringBuilder backupFlagsStr; // Fields below only required for filters, hence, loaded dynamically @Nullable private PackageInfo mPackageInfo; @Nullable private PackageUsageInfo mPackageUsageInfo; @Nullable private ApplicationInfo mApplicationInfo; private InstallSourceInfoCompat mInstallerInfo; @Nullable private SignerInfo mSignerInfo; private String[] mSignatureSubjectLines; private String[] mSignatureSha256Checksums; private Map mAllComponents; private Map mTrackerComponents; private List mUsedPermissions; private Backup[] mBackups; private List mAppOpEntries; @Nullable private PackageSizeInfo mPackageSizeInfo; @Nullable private AppUsageStatsManager.DataUsage mDataUsage; @Nullable private DebloatObject mBloatwareInfo; private Integer mFreezeFlags = null; private Integer mUserId = null; private Boolean mUsesSensors = null; private Boolean mBatteryOptEnabled = null; private Boolean mHasKeystoreItems = null; private Integer mRulesCount = null; private boolean mIsRunning = false; public ApplicationItem() { super(); } public void generateOtherInfo() { isStopped = (flags & ApplicationInfo.FLAG_STOPPED) != 0; isSystem = (flags & ApplicationInfo.FLAG_SYSTEM) != 0; isPersistent = (flags & ApplicationInfo.FLAG_PERSISTENT) != 0; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { usesCleartextTraffic = (flags & ApplicationInfo.FLAG_USES_CLEARTEXT_TRAFFIC) != 0; } for (int userId : userIds) { canReadLogs |= (PermissionCompat.checkPermission(Manifest.permission.READ_LOGS, packageName, userId) == PackageManager.PERMISSION_GRANTED); isAppInactive |= UsageStatsManagerCompat.isAppInactive(packageName, userId); } allowClearingUserData = (flags & ApplicationInfo.FLAG_ALLOW_CLEAR_USER_DATA) != 0; // UID if (userIds.length > 1) { int appId = UserHandleHidden.getAppId(uid); uidOrAppIds = userIds.length + "+" + appId; } else if (userIds.length == 1) { uidOrAppIds = String.valueOf(uid); } else uidOrAppIds = ""; // Cert short name if (sha != null) { try { issuerShortName = "CN=" + (sha.first).split("CN=", 2)[1]; } catch (ArrayIndexOutOfBoundsException e) { issuerShortName = sha.first; } if (TextUtils.isEmpty(sha.second)) { sha = null; } } // Version info versionTag = versionName; if (isInstalled && (flags & ApplicationInfo.FLAG_HARDWARE_ACCELERATED) == 0) versionTag = "_" + versionTag; if ((flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) versionTag = "debug" + versionTag; if ((flags & ApplicationInfo.FLAG_TEST_ONLY) != 0) versionTag = "~" + versionTag; // App type flags appTypePostfix = ""; if ((flags & ApplicationInfo.FLAG_LARGE_HEAP) != 0) appTypePostfix += "#"; if ((flags & ApplicationInfo.FLAG_SUSPENDED) != 0) appTypePostfix += "°"; if ((flags & ApplicationInfo.FLAG_MULTIARCH) != 0) appTypePostfix += "X"; if ((flags & ApplicationInfo.FLAG_HAS_CODE) == 0) appTypePostfix += "0"; if ((flags & ApplicationInfo.FLAG_VM_SAFE_MODE) != 0) appTypePostfix += "?"; // Sdk if (targetSdk != null && targetSdk > 0) { sdkString = "SDK " + targetSdk; } diffInstallUpdateInDays = TimeUnit.DAYS.convert(lastUpdateTime - firstInstallTime, TimeUnit.MILLISECONDS); // Backup if (backup != null) { lastBackupDays = TimeUnit.DAYS.convert(System.currentTimeMillis() - backup.backupTime, TimeUnit.MILLISECONDS); backupFlagsStr = new StringBuilder(); if (backup.getFlags().backupApkFiles()) backupFlagsStr.append("apk"); if (backup.getFlags().backupData()) { if (backupFlagsStr.length() > 0) backupFlagsStr.append("+"); backupFlagsStr.append("data"); } if (backup.hasRules) { if (backupFlagsStr.length() > 0) backupFlagsStr.append("+"); backupFlagsStr.append("rules"); } } } @WorkerThread @Override public Drawable loadIcon(PackageManager pm) { fetchPackageInfo(); if (mApplicationInfo != null) { return mApplicationInfo.loadIcon(pm); } // App not installed if (backup != null) { try { Path iconFile = backup.getItem().getIconFile(); if (iconFile.exists()) { try (InputStream is = iconFile.openInputStream()) { Drawable drawable = Drawable.createFromStream(is, name); if (drawable != null) { return drawable; } } } } catch (Throwable ignore) { } } return pm.getDefaultActivityIcon(); } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof ApplicationItem)) return false; ApplicationItem item = (ApplicationItem) o; return packageName.equals(item.packageName); } @Override public int hashCode() { return Objects.hash(packageName); } @NonNull @Override public String getPackageName() { return packageName; } @Override public int getUserId() { if (mUserId != null) { return mUserId; } if (userIds.length > 0) { int myUserId = UserHandleHidden.myUserId(); for (int userId : userIds) { // Prefer current user if (userId == myUserId) { mUserId = myUserId; break; } } if (mUserId == null) { // Failed, assign the first user mUserId = userIds[0]; } return mUserId; } return -1; } @Override public int getUid() { int uid = mApplicationInfo != null ? mApplicationInfo.uid : this.uid; return UserHandleHidden.getAppId(uid); } public void setPackageUsageInfo(@Nullable PackageUsageInfo packageUsageInfo) { mPackageUsageInfo = packageUsageInfo; } private void fetchPackageInfo() { if (mPackageInfo != null) { return; } int userId = getUserId(); if (userId < 0) { return; } try { mPackageInfo = PackageManagerCompat.getPackageInfo(packageName, PackageManager.GET_META_DATA | GET_SIGNING_CERTIFICATES | PackageManager.GET_ACTIVITIES | PackageManager.GET_RECEIVERS | PackageManager.GET_PROVIDERS | PackageManager.GET_SERVICES | PackageManager.GET_CONFIGURATIONS | PackageManager.GET_PERMISSIONS | PackageManager.GET_URI_PERMISSION_PATTERNS | MATCH_DISABLED_COMPONENTS | MATCH_UNINSTALLED_PACKAGES | MATCH_STATIC_SHARED_AND_SDK_LIBRARIES, userId); mApplicationInfo = Objects.requireNonNull(mPackageInfo.applicationInfo); // Update dynamic info isStopped = (mApplicationInfo.flags & ApplicationInfo.FLAG_STOPPED) != 0; isDisabled = FreezeUtils.isFrozen(mApplicationInfo); } catch (RemoteException | PackageManager.NameNotFoundException ignore) { } } @NonNull @Override public String getAppLabel() { return label; } @NonNull @Override public Drawable getAppIcon() { return loadIcon(ContextUtils.getContext().getPackageManager()); } @Nullable @Override public String getVersionName() { return versionName; } @Override public long getVersionCode() { return versionCode; } @Override public long getFirstInstallTime() { return firstInstallTime; } @Override public long getLastUpdateTime() { return lastUpdateTime; } @Override public int getTargetSdk() { return targetSdk; } @Override @RequiresApi(Build.VERSION_CODES.S) public int getCompileSdk() { fetchPackageInfo(); if (mApplicationInfo != null) { return mApplicationInfo.compileSdkVersion; } return targetSdk; } @Override @RequiresApi(Build.VERSION_CODES.N) public int getMinSdk() { fetchPackageInfo(); if (mApplicationInfo != null) { return mApplicationInfo.minSdkVersion; } return 0; } @NonNull @Override public Backup[] getBackups() { if (mBackups == null) { mBackups = BackupUtils.getBackupMetadataFromDbNoLockValidate(getPackageName()).toArray(new Backup[0]); } return mBackups; } public void setRunning(boolean running) { mIsRunning = running; } @Override public boolean isRunning() { return mIsRunning; } @NonNull @Override public Map getTrackerComponents() { if (mTrackerComponents == null) { Map allComponents = getAllComponents(); Map trackerComponents = new LinkedHashMap<>(); for (ComponentInfo itemInfo : allComponents.keySet()) { if (ComponentUtils.isTracker(itemInfo.name)) { trackerComponents.put(itemInfo, allComponents.get(itemInfo)); } } mTrackerComponents = trackerComponents; } return mTrackerComponents; } @NonNull @Override public List getAppOps() { fetchPackageInfo(); if (mApplicationInfo != null && mAppOpEntries == null && isInstalled()) { List packageOps = ExUtils.exceptionAsNull(() -> new AppOpsManagerCompat().getOpsForPackage(mApplicationInfo.uid, getPackageName(), null)); if (packageOps != null && packageOps.size() == 1) { mAppOpEntries = packageOps.get(0).getOps(); } } else mAppOpEntries = Collections.emptyList(); return mAppOpEntries; } @NonNull @Override public Map getAllComponents() { fetchPackageInfo(); if (mPackageInfo != null && mAllComponents == null) { Map components = new LinkedHashMap<>(); if (mPackageInfo.activities != null) { for (ActivityInfo info : mPackageInfo.activities) { components.put(info, ComponentsOption.COMPONENT_TYPE_ACTIVITY); } } if (mPackageInfo.services != null) { for (ServiceInfo info : mPackageInfo.services) { components.put(info, ComponentsOption.COMPONENT_TYPE_SERVICE); } } if (mPackageInfo.receivers != null) { for (ActivityInfo info : mPackageInfo.receivers) { components.put(info, ComponentsOption.COMPONENT_TYPE_RECEIVER); } } if (mPackageInfo.providers != null) { for (ProviderInfo info : mPackageInfo.providers) { components.put(info, ComponentsOption.COMPONENT_TYPE_PROVIDER); } } mAllComponents = components; } return mAllComponents; } @NonNull @Override public List getAllPermissions() { fetchPackageInfo(); if (mPackageInfo != null && mUsedPermissions == null) { Set usedPermissions = new HashSet<>(); if (mPackageInfo.requestedPermissions != null) { Collections.addAll(usedPermissions, mPackageInfo.requestedPermissions); } if (mPackageInfo.permissions != null) { for (PermissionInfo perm : mPackageInfo.permissions) { usedPermissions.add(perm.name); } } if (mPackageInfo.activities != null) { for (ActivityInfo info : mPackageInfo.activities) { if (info.permission != null) { usedPermissions.add(info.permission); } } } if (mPackageInfo.services != null) { for (ServiceInfo info : mPackageInfo.services) { if (info.permission != null) { usedPermissions.add(info.permission); } } } if (mPackageInfo.receivers != null) { for (ActivityInfo info : mPackageInfo.receivers) { if (info.permission != null) { usedPermissions.add(info.permission); } } } mUsedPermissions = new ArrayList<>(usedPermissions); } return mUsedPermissions; } @NonNull @Override public FeatureInfo[] getAllRequestedFeatures() { fetchPackageInfo(); if (mPackageInfo != null) { return ArrayUtils.defeatNullable(FeatureInfo.class, mPackageInfo.reqFeatures); } return new FeatureInfo[0]; } @Override public boolean isInstalled() { return isInstalled; } @Override public boolean isFrozen() { return !isEnabled() || isSuspended() || isHidden(); } @Override public int getFreezeFlags() { if (mFreezeFlags != null) { return mFreezeFlags; } mFreezeFlags = 0; if (!isEnabled()) { mFreezeFlags |= FreezeOption.FREEZE_TYPE_DISABLED; } if (isHidden()) { mFreezeFlags |= FreezeOption.FREEZE_TYPE_HIDDEN; } if (isSuspended()) { mFreezeFlags |= FreezeOption.FREEZE_TYPE_SUSPENDED; } return mFreezeFlags; } @Override public boolean isStopped() { return isStopped; } @Override public boolean isTestOnly() { return (flags & ApplicationInfo.FLAG_TEST_ONLY) != 0; } @Override public boolean isDebuggable() { return debuggable; } @Override public boolean isSystemApp() { return isSystem; } @Override public boolean hasCode() { return (flags & ApplicationInfo.FLAG_HAS_CODE) != 0; } @Override public boolean isPersistent() { return isPersistent; } @Override public boolean isUpdatedSystemApp() { return (flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0; } @Override public boolean backupAllowed() { return (flags & ApplicationInfo.FLAG_ALLOW_BACKUP) != 0; } @Override public boolean installedInExternalStorage() { return (flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0; } @Override public boolean requestedLargeHeap() { return (flags & ApplicationInfo.FLAG_LARGE_HEAP) != 0; } @Override public boolean supportsRTL() { return (flags & ApplicationInfo.FLAG_SUPPORTS_RTL) != 0; } @Override public boolean dataOnlyApp() { return (flags & ApplicationInfo.FLAG_IS_DATA_ONLY) != 0; } @Override public boolean usesHttp() { return usesCleartextTraffic; } @Override public boolean isPrivileged() { fetchPackageInfo(); if (mApplicationInfo != null) { return ApplicationInfoCompat.isPrivileged(mApplicationInfo); } return false; } @RequiresApi(Build.VERSION_CODES.P) public boolean usesSensors() { if (mUsesSensors == null) { if (isInstalled() && SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.MANAGE_SENSORS)) { mUsesSensors = SensorServiceCompat.isSensorEnabled(getPackageName(), getUserId()); } else mUsesSensors = isInstalled(); // Worse case: always true } return mUsesSensors; } @Override public boolean isBatteryOptEnabled() { if (mBatteryOptEnabled == null) { if (isInstalled()) { mBatteryOptEnabled = DeviceIdleManagerCompat.isBatteryOptimizedApp(getPackageName()); } else mBatteryOptEnabled = true; } return mBatteryOptEnabled; } @Override public boolean hasKeyStoreItems() { fetchPackageInfo(); if (mHasKeystoreItems == null) { if (mApplicationInfo != null && isInstalled()) { mHasKeystoreItems = KeyStoreUtils.hasKeyStore(mApplicationInfo.uid); } else mHasKeystoreItems = false; } return mHasKeystoreItems; } @Override public int getRuleCount() { if (mRulesCount == null) { mRulesCount = 0; for (int userId : userIds) { try (ComponentsBlocker cb = ComponentsBlocker.getInstance(getPackageName(), userId, false)) { mRulesCount += cb.entryCount(); } } } return mRulesCount; } @Nullable @Override public String getSsaid() { return ssaid; } @Override public boolean hasDomainUrls() { fetchPackageInfo(); if (mApplicationInfo != null) { return ApplicationInfoCompat.hasDomainUrls(mApplicationInfo); } return false; } @Override public boolean hasStaticSharedLibrary() { fetchPackageInfo(); if (mApplicationInfo != null) { return ApplicationInfoCompat.isStaticSharedLibrary(mApplicationInfo); } return false; } @Override public boolean isHidden() { fetchPackageInfo(); if (mApplicationInfo != null) { return ApplicationInfoCompat.isHidden(mApplicationInfo); } return false; } @Override public boolean isSuspended() { fetchPackageInfo(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && mApplicationInfo != null) { return ApplicationInfoCompat.isSuspended(mApplicationInfo); } // Not supported return false; } @Override public boolean isEnabled() { fetchPackageInfo(); if (mApplicationInfo != null) { return mApplicationInfo.enabled; } return !isDisabled; } @Nullable @Override public String getSharedUserId() { fetchPackageInfo(); return mPackageInfo != null ? mPackageInfo.sharedUserId : sharedUserId; } private void fetchPackageSizeInfo() { int userId = getUserId(); if (userId >= 0 && mPackageSizeInfo == null && isInstalled()) { mPackageSizeInfo = PackageUtils.getPackageSizeInfo(ContextUtils.getContext(), getPackageName(), userId, null); } } @Override public long getTotalSize() { fetchPackageSizeInfo(); return mPackageSizeInfo != null ? mPackageSizeInfo.getTotalSize() : 0; } @Override public long getApkSize() { fetchPackageSizeInfo(); return mPackageSizeInfo != null ? (mPackageSizeInfo.codeSize + mPackageSizeInfo.obbSize) : 0; } @Override public long getCacheSize() { fetchPackageSizeInfo(); return mPackageSizeInfo != null ? mPackageSizeInfo.cacheSize : 0; } @Override public long getDataSize() { fetchPackageSizeInfo(); return mPackageSizeInfo != null ? (mPackageSizeInfo.dataSize + mPackageSizeInfo.mediaSize + mPackageSizeInfo.cacheSize) : 0; } @Override @NonNull public AppUsageStatsManager.DataUsage getDataUsage() { if (mDataUsage == null && isInstalled()) { if (mPackageUsageInfo != null) { mDataUsage = AppUsageStatsManager.DataUsage.fromDataUsage(mPackageUsageInfo.mobileData, mPackageUsageInfo.wifiData); } } if (mDataUsage == null) { mDataUsage = AppUsageStatsManager.DataUsage.EMPTY; } return mDataUsage; } @Override public int getTimesOpened() { fetchPackageInfo(); return mPackageUsageInfo != null ? mPackageUsageInfo.timesOpened : openCount; } @Override public long getTotalScreenTime() { fetchPackageInfo(); return mPackageUsageInfo != null ? mPackageUsageInfo.screenTime : screenTime; } @Override public long getLastUsedTime() { fetchPackageInfo(); return mPackageUsageInfo != null ? mPackageUsageInfo.lastUsageTime : lastUsageTime; } @Override @Nullable public SignerInfo fetchSignerInfo() { fetchPackageInfo(); if (mPackageInfo != null && mSignerInfo == null) { mSignerInfo = PackageUtils.getSignerInfo(mPackageInfo, !isInstalled()); } return mSignerInfo; } @Override @NonNull public String[] getSignatureSubjectLines() { fetchSignerInfo(); if (mSignerInfo != null && mSignatureSubjectLines == null) { X509Certificate[] signatures = mSignerInfo.getAllSignerCerts(); if (signatures != null) { mSignatureSubjectLines = new String[signatures.length]; for (int i = 0; i < signatures.length; ++i) { mSignatureSubjectLines[i] = signatures[i].getSubjectX500Principal().getName(); } } } return mSignatureSubjectLines != null ? mSignatureSubjectLines : EmptyArray.STRING; } @Override @NonNull public String[] getSignatureSha256Checksums() { fetchSignerInfo(); if (mSignerInfo != null && mSignatureSha256Checksums == null) { X509Certificate[] signatures = mSignerInfo.getAllSignerCerts(); if (signatures != null) { mSignatureSha256Checksums = new String[signatures.length]; for (int i = 0; i < signatures.length; ++i) { try { mSignatureSha256Checksums[i] = DigestUtils.getHexDigest(DigestUtils.SHA_256, signatures[i].getEncoded()); } catch (CertificateEncodingException e) { mSignatureSha256Checksums[i] = ""; } } } } return mSignatureSha256Checksums != null ? mSignatureSha256Checksums : EmptyArray.STRING; } @Override @Nullable public InstallSourceInfoCompat getInstallerInfo() { int userId = getUserId(); if (userId >= 0 && mInstallerInfo == null && isInstalled()) { try { mInstallerInfo = PackageManagerCompat.getInstallSourceInfo(getPackageName(), userId); } catch (RemoteException ignore) { } } return mInstallerInfo; } @Override @Nullable public DebloatObject getBloatwareInfo() { if (mBloatwareInfo == null) { for (DebloatObject debloatObject : StaticDataset.getDebloatObjects()) { if (getPackageName().equals(debloatObject.packageName)) { mBloatwareInfo = debloatObject; break; } } } return mBloatwareInfo; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/main/MainActivity.java ================================================ // SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.main; import android.annotation.SuppressLint; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; import android.text.TextUtils; import android.view.Gravity; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import androidx.activity.OnBackPressedCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.collection.ArrayMap; import androidx.core.content.ContextCompat; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.checkbox.MaterialCheckBox; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.progressindicator.LinearProgressIndicator; import com.google.android.material.snackbar.Snackbar; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.List; import io.github.muntashirakon.AppManager.BaseActivity; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.apk.behavior.FreezeUnfreeze; import io.github.muntashirakon.AppManager.apk.dexopt.DexOptDialog; import io.github.muntashirakon.AppManager.apk.list.ListExporter; import io.github.muntashirakon.AppManager.backup.dialog.BackupRestoreDialogFragment; import io.github.muntashirakon.AppManager.batchops.BatchOpsManager; import io.github.muntashirakon.AppManager.batchops.BatchOpsService; import io.github.muntashirakon.AppManager.batchops.BatchQueueItem; import io.github.muntashirakon.AppManager.batchops.struct.BatchFreezeOptions; import io.github.muntashirakon.AppManager.batchops.struct.BatchNetPolicyOptions; import io.github.muntashirakon.AppManager.batchops.struct.IBatchOpOptions; import io.github.muntashirakon.AppManager.changelog.Changelog; import io.github.muntashirakon.AppManager.changelog.ChangelogParser; import io.github.muntashirakon.AppManager.changelog.ChangelogRecyclerAdapter; import io.github.muntashirakon.AppManager.compat.NetworkPolicyManagerCompat; import io.github.muntashirakon.AppManager.debloat.DebloaterActivity; import io.github.muntashirakon.AppManager.filters.FinderActivity; import io.github.muntashirakon.AppManager.misc.AdvancedSearchView; import io.github.muntashirakon.AppManager.misc.HelpActivity; import io.github.muntashirakon.AppManager.misc.LabsActivity; import io.github.muntashirakon.AppManager.oneclickops.OneClickOpsActivity; import io.github.muntashirakon.AppManager.profiles.AddToProfileDialogFragment; import io.github.muntashirakon.AppManager.profiles.ProfilesActivity; import io.github.muntashirakon.AppManager.rules.RulesTypeSelectionDialogFragment; import io.github.muntashirakon.AppManager.runningapps.RunningAppsActivity; import io.github.muntashirakon.AppManager.self.life.FundingCampaignChecker; import io.github.muntashirakon.AppManager.settings.FeatureController; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.settings.SettingsActivity; import io.github.muntashirakon.AppManager.usage.AppUsageActivity; import io.github.muntashirakon.AppManager.users.Users; import io.github.muntashirakon.AppManager.utils.AppPref; import io.github.muntashirakon.AppManager.utils.DateUtils; import io.github.muntashirakon.AppManager.utils.StoragePermission; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.dialog.AlertDialogBuilder; import io.github.muntashirakon.dialog.ScrollableDialogBuilder; import io.github.muntashirakon.dialog.SearchableFlagsDialogBuilder; import io.github.muntashirakon.dialog.SearchableSingleChoiceDialogBuilder; import io.github.muntashirakon.io.Paths; import io.github.muntashirakon.multiselection.MultiSelectionActionsView; import io.github.muntashirakon.util.UiUtils; import io.github.muntashirakon.widget.MultiSelectionView; import io.github.muntashirakon.widget.SwipeRefreshLayout; public class MainActivity extends BaseActivity implements AdvancedSearchView.OnQueryTextListener, SwipeRefreshLayout.OnRefreshListener, MultiSelectionActionsView.OnItemSelectedListener, MultiSelectionView.OnSelectionModeChangeListener { private static final String PACKAGE_NAME_APK_UPDATER = "com.apkupdater"; private static final String ACTIVITY_NAME_APK_UPDATER = "com.apkupdater.activity.MainActivity"; private static boolean SHOW_DISCLAIMER = true; MainViewModel viewModel; private MainRecyclerAdapter mAdapter; private AdvancedSearchView mSearchView; private LinearProgressIndicator mProgressIndicator; private SwipeRefreshLayout mSwipeRefresh; private MultiSelectionView mMultiSelectionView; MainBatchOpsHandler mBatchOpsHandler; private MenuItem mAppUsageMenu; private final StoragePermission mStoragePermission = StoragePermission.init(this); private final ActivityResultLauncher mBatchExportRules = registerForActivityResult( new ActivityResultContracts.CreateDocument("text/tab-separated-values"), uri -> { if (uri == null) { // Back button pressed. return; } if (viewModel == null) { // Invalid state return; } RulesTypeSelectionDialogFragment dialogFragment = new RulesTypeSelectionDialogFragment(); Bundle args = new Bundle(); args.putInt(RulesTypeSelectionDialogFragment.ARG_MODE, RulesTypeSelectionDialogFragment.MODE_EXPORT); args.putParcelable(RulesTypeSelectionDialogFragment.ARG_URI, uri); args.putStringArrayList(RulesTypeSelectionDialogFragment.ARG_PKG, new ArrayList<>(viewModel.getSelectedPackages().keySet())); args.putIntArray(RulesTypeSelectionDialogFragment.ARG_USERS, Users.getUsersIds()); dialogFragment.setArguments(args); dialogFragment.show(getSupportFragmentManager(), RulesTypeSelectionDialogFragment.TAG); }); private final ActivityResultLauncher mExportAppListCsv = registerForActivityResult( new ActivityResultContracts.CreateDocument("text/csv"), uri -> { if (uri == null) { // Back button pressed. return; } mProgressIndicator.show(); viewModel.saveExportedAppList(ListExporter.EXPORT_TYPE_CSV, Paths.get(uri)); }); private final ActivityResultLauncher mExportAppListJson = registerForActivityResult( new ActivityResultContracts.CreateDocument("application/json"), uri -> { if (uri == null) { // Back button pressed. return; } mProgressIndicator.show(); viewModel.saveExportedAppList(ListExporter.EXPORT_TYPE_JSON, Paths.get(uri)); }); private final ActivityResultLauncher mExportAppListXml = registerForActivityResult( new ActivityResultContracts.CreateDocument("text/xml"), uri -> { if (uri == null) { // Back button pressed. return; } mProgressIndicator.show(); viewModel.saveExportedAppList(ListExporter.EXPORT_TYPE_XML, Paths.get(uri)); }); private final ActivityResultLauncher mExportAppListMarkdown = registerForActivityResult( new ActivityResultContracts.CreateDocument("text/markdown"), uri -> { if (uri == null) { // Back button pressed. return; } mProgressIndicator.show(); viewModel.saveExportedAppList(ListExporter.EXPORT_TYPE_MARKDOWN, Paths.get(uri)); }); private final BroadcastReceiver mBatchOpsBroadCastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { showProgressIndicator(false); } }; private final OnBackPressedCallback mOnBackPressedCallback = new OnBackPressedCallback(false) { @Override public void handleOnBackPressed() { if (mAdapter != null && mMultiSelectionView != null && mAdapter.isInSelectionMode()) { mMultiSelectionView.cancel(); return; } setEnabled(false); getOnBackPressedDispatcher().onBackPressed(); } }; @SuppressLint("RestrictedApi") @Override protected void onAuthenticated(Bundle savedInstanceState) { setContentView(R.layout.activity_main); setSupportActionBar(findViewById(R.id.toolbar)); getOnBackPressedDispatcher().addCallback(this, mOnBackPressedCallback); viewModel = new ViewModelProvider(this).get(MainViewModel.class); ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayShowCustomEnabled(true); actionBar.setDisplayOptions(0, ActionBar.DISPLAY_SHOW_TITLE); AdvancedSearchView searchView = new AdvancedSearchView(actionBar.getThemedContext()); searchView.setId(R.id.action_search); searchView.setOnQueryTextListener(this); // Set layout params ActionBar.LayoutParams layoutParams = new ActionBar.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); layoutParams.gravity = Gravity.CENTER; actionBar.setCustomView(searchView, layoutParams); mSearchView = searchView; mSearchView.setIconifiedByDefault(false); mSearchView.setOnFocusChangeListener((v, hasFocus) -> { if (!hasFocus) { UiUtils.hideKeyboard(v); } }); // Check for market://search/?q= Uri marketUri = getIntent().getData(); if (marketUri != null && "market".equals(marketUri.getScheme()) && "search".equals(marketUri.getHost())) { String query = marketUri.getQueryParameter("q"); if (query != null) { mSearchView.setQuery(query, true); } } } mProgressIndicator = findViewById(R.id.progress_linear); mProgressIndicator.setVisibilityAfterHide(View.GONE); RecyclerView recyclerView = findViewById(R.id.item_list); recyclerView.requestFocus(); // Initially (the view isn't actually focusable) mSwipeRefresh = findViewById(R.id.swipe_refresh); mSwipeRefresh.setOnRefreshListener(this); mAdapter = new MainRecyclerAdapter(MainActivity.this); mAdapter.setHasStableIds(true); recyclerView.setLayoutManager(UIUtils.getGridLayoutAt450Dp(this)); recyclerView.setAdapter(mAdapter); mMultiSelectionView = findViewById(R.id.selection_view); mMultiSelectionView.setOnItemSelectedListener(this); mMultiSelectionView.setOnSelectionModeChangeListener(this); mMultiSelectionView.setAdapter(mAdapter); mMultiSelectionView.updateCounter(true); mBatchOpsHandler = new MainBatchOpsHandler(mMultiSelectionView, viewModel); mMultiSelectionView.setOnSelectionChangeListener(mBatchOpsHandler); if (SHOW_DISCLAIMER && AppPref.getBoolean(AppPref.PrefKey.PREF_SHOW_DISCLAIMER_BOOL)) { // Disclaimer will only be shown the first time it is loaded. SHOW_DISCLAIMER = false; View view = View.inflate(this, R.layout.dialog_disclaimer, null); new MaterialAlertDialogBuilder(this) .setView(view) .setCancelable(false) .setPositiveButton(R.string.disclaimer_agree, (dialog, which) -> { if (((MaterialCheckBox) view.findViewById(R.id.agree_forever)).isChecked()) { AppPref.set(AppPref.PrefKey.PREF_SHOW_DISCLAIMER_BOOL, false); } displayChangelogIfRequired(); }) .setNegativeButton(R.string.disclaimer_exit, (dialog, which) -> finishAndRemoveTask()) .show(); } else { displayChangelogIfRequired(); } // Set observer viewModel.getApplicationItems().observe(this, applicationItems -> { if (mAdapter != null) mAdapter.setDefaultList(applicationItems); showProgressIndicator(false); }); viewModel.getOperationStatus().observe(this, status -> { mProgressIndicator.hide(); if (status) { UIUtils.displayShortToast(R.string.done); } else { UIUtils.displayLongToast(R.string.failed); } }); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.activity_main_actions, menu); mAppUsageMenu = menu.findItem(R.id.action_app_usage); MenuItem apkUpdaterMenu = menu.findItem(R.id.action_apk_updater); try { if (!getPackageManager().getApplicationInfo(PACKAGE_NAME_APK_UPDATER, 0).enabled) throw new PackageManager.NameNotFoundException(); apkUpdaterMenu.setVisible(true); } catch (PackageManager.NameNotFoundException e) { apkUpdaterMenu.setVisible(false); } MenuItem finderMenu = menu.findItem(R.id.action_finder); finderMenu.setVisible(BuildConfig.DEBUG); return super.onCreateOptionsMenu(menu); } @Override public boolean onPrepareOptionsMenu(@NonNull Menu menu) { super.onPrepareOptionsMenu(menu); mAppUsageMenu.setVisible(FeatureController.isUsageAccessEnabled()); return true; } @SuppressLint("InflateParams") @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int id = item.getItemId(); if (id == R.id.action_instructions) { Intent helpIntent = new Intent(this, HelpActivity.class); helpIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(helpIntent); } else if (id == R.id.action_list_options) { MainListOptions listOptions = new MainListOptions(); listOptions.setListOptionActions(viewModel); listOptions.show(getSupportFragmentManager(), MainListOptions.TAG); } else if (id == R.id.action_refresh) { if (viewModel != null) { showProgressIndicator(true); viewModel.loadApplicationItems(); } } else if (id == R.id.action_settings) { Intent settingsIntent = SettingsActivity.getSettingsIntent(this); startActivity(settingsIntent); } else if (id == R.id.action_app_usage) { Intent usageIntent = new Intent(this, AppUsageActivity.class); startActivity(usageIntent); } else if (id == R.id.action_one_click_ops) { Intent onClickOpsIntent = new Intent(this, OneClickOpsActivity.class); startActivity(onClickOpsIntent); } else if (id == R.id.action_finder) { Intent intent = new Intent(this, FinderActivity.class); startActivity(intent); } else if (id == R.id.action_apk_updater) { try { if (!getPackageManager().getApplicationInfo(PACKAGE_NAME_APK_UPDATER, 0).enabled) throw new PackageManager.NameNotFoundException(); Intent intent = new Intent(); intent.setClassName(PACKAGE_NAME_APK_UPDATER, ACTIVITY_NAME_APK_UPDATER); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); } catch (Exception ignored) { } } else if (id == R.id.action_running_apps) { Intent runningAppsIntent = new Intent(this, RunningAppsActivity.class); startActivity(runningAppsIntent); } else if (id == R.id.action_profiles) { Intent profilesIntent = new Intent(this, ProfilesActivity.class); startActivity(profilesIntent); } else if (id == R.id.action_labs) { Intent intent = new Intent(getApplicationContext(), LabsActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); } else if (id == R.id.action_debloater) { Intent intent = new Intent(getApplicationContext(), DebloaterActivity.class); startActivity(intent); } else return super.onOptionsItemSelected(item); return true; } @Override public void onSelectionModeEnabled() { mOnBackPressedCallback.setEnabled(true); } @Override public void onSelectionModeDisabled() { mOnBackPressedCallback.setEnabled(false); } @Override public boolean onNavigationItemSelected(@NonNull MenuItem item) { int id = item.getItemId(); if (id == R.id.action_backup) { if (viewModel != null) { BackupRestoreDialogFragment fragment = BackupRestoreDialogFragment.getInstance(viewModel.getSelectedPackagesWithUsers()); fragment.setOnActionBeginListener(mode -> showProgressIndicator(true)); fragment.setOnActionCompleteListener((mode, failedPackages) -> showProgressIndicator(false)); fragment.show(getSupportFragmentManager(), BackupRestoreDialogFragment.TAG); } } else if (id == R.id.action_save_apk) { mStoragePermission.request(granted -> { if (granted) handleBatchOp(BatchOpsManager.OP_BACKUP_APK); }); } else if (id == R.id.action_block_unblock_trackers) { new MaterialAlertDialogBuilder(this) .setTitle(R.string.block_unblock_trackers) .setMessage(R.string.choose_what_to_do) .setPositiveButton(R.string.block, (dialog, which) -> handleBatchOp(BatchOpsManager.OP_BLOCK_TRACKERS)) .setNegativeButton(R.string.cancel, null) .setNeutralButton(R.string.unblock, (dialog, which) -> handleBatchOp(BatchOpsManager.OP_UNBLOCK_TRACKERS)) .show(); } else if (id == R.id.action_clear_data_cache) { new MaterialAlertDialogBuilder(this) .setTitle(R.string.clear) .setMessage(R.string.choose_what_to_do) .setPositiveButton(R.string.clear_cache, (dialog, which) -> handleBatchOp(BatchOpsManager.OP_CLEAR_CACHE)) .setNegativeButton(R.string.cancel, null) .setNeutralButton(R.string.clear_data, (dialog, which) -> handleBatchOp(BatchOpsManager.OP_CLEAR_DATA)) .show(); } else if (id == R.id.action_freeze_unfreeze) { showFreezeUnfreezeDialog(Prefs.Blocking.getDefaultFreezingMethod()); } else if (id == R.id.action_disable_background) { new MaterialAlertDialogBuilder(this) .setTitle(R.string.are_you_sure) .setMessage(R.string.disable_background_run_description) .setPositiveButton(R.string.yes, (dialog, which) -> handleBatchOp(BatchOpsManager.OP_DISABLE_BACKGROUND)) .setNegativeButton(R.string.no, null) .show(); } else if (id == R.id.action_net_policy) { ArrayMap netPolicyMap = NetworkPolicyManagerCompat.getAllReadablePolicies(this); Integer[] polices = new Integer[netPolicyMap.size()]; String[] policyStrings = new String[netPolicyMap.size()]; Collection applicationItems = viewModel.getSelectedPackages().values(); Iterator it = applicationItems.iterator(); int selectedPolicies = applicationItems.size() == 1 && it.hasNext() ? NetworkPolicyManagerCompat.getUidPolicy(it.next().uid) : 0; for (int i = 0; i < netPolicyMap.size(); ++i) { polices[i] = netPolicyMap.keyAt(i); policyStrings[i] = netPolicyMap.valueAt(i); } new SearchableFlagsDialogBuilder<>(this, polices, policyStrings, selectedPolicies) .setTitle(R.string.net_policy) .showSelectAll(false) .setNegativeButton(R.string.cancel, null) .setPositiveButton(R.string.apply, (dialog, which, selections) -> { int flags = 0; for (int flag : selections) { flags |= flag; } BatchNetPolicyOptions options = new BatchNetPolicyOptions(flags); handleBatchOp(BatchOpsManager.OP_NET_POLICY, options); }) .show(); } else if (id == R.id.action_optimize) { DexOptDialog dialog = DexOptDialog.getInstance(viewModel.getSelectedPackages().keySet().toArray(new String[0])); dialog.show(getSupportFragmentManager(), DexOptDialog.TAG); } else if (id == R.id.action_export_blocking_rules) { final String fileName = "app_manager_rules_export-" + DateUtils.formatDateTime(this, System.currentTimeMillis()) + ".am.tsv"; mBatchExportRules.launch(fileName); } else if (id == R.id.action_export_app_list) { List exportTypes = Arrays.asList(ListExporter.EXPORT_TYPE_CSV, ListExporter.EXPORT_TYPE_JSON, ListExporter.EXPORT_TYPE_XML, ListExporter.EXPORT_TYPE_MARKDOWN); new SearchableSingleChoiceDialogBuilder<>(this, exportTypes, R.array.export_app_list_options) .setTitle(R.string.export_app_list_select_format) .setOnSingleChoiceClickListener((dialog, which, item1, isChecked) -> { if (!isChecked) { return; } String filename = "app_manager_app_list-" + DateUtils.formatLongDateTime(this, System.currentTimeMillis()) + ".am"; switch (item1) { case ListExporter.EXPORT_TYPE_CSV: mExportAppListCsv.launch(filename + ".csv"); break; case ListExporter.EXPORT_TYPE_JSON: mExportAppListJson.launch(filename + ".json"); break; case ListExporter.EXPORT_TYPE_XML: mExportAppListXml.launch(filename + ".xml"); break; case ListExporter.EXPORT_TYPE_MARKDOWN: mExportAppListMarkdown.launch(filename + ".md"); break; } }) .setNegativeButton(R.string.close, null) .show(); } else if (id == R.id.action_force_stop) { handleBatchOp(BatchOpsManager.OP_FORCE_STOP); } else if (id == R.id.action_uninstall) { handleBatchOpWithWarning(BatchOpsManager.OP_UNINSTALL); } else if (id == R.id.action_add_to_profile) { AddToProfileDialogFragment dialog = AddToProfileDialogFragment.getInstance(viewModel.getSelectedPackages() .keySet().toArray(new String[0])); dialog.show(getSupportFragmentManager(), AddToProfileDialogFragment.TAG); } else { return false; } return true; } @Override public void onRefresh() { showProgressIndicator(true); if (viewModel != null) viewModel.loadApplicationItems(); mSwipeRefresh.setRefreshing(false); } @Override protected void onStart() { super.onStart(); // Set filter if (viewModel != null && mSearchView != null && !TextUtils.isEmpty(viewModel.getSearchQuery())) { if (mSearchView.isIconified()) { mSearchView.setIconified(false); } mSearchView.setQuery(viewModel.getSearchQuery(), false); } // Show/hide app usage menu if (mAppUsageMenu != null) { mAppUsageMenu.setVisible(FeatureController.isUsageAccessEnabled()); } // Check for backup volume if (!Prefs.BackupRestore.backupDirectoryExists()) { new MaterialAlertDialogBuilder(this) .setTitle(R.string.backup_volume) .setMessage(R.string.backup_volume_unavailable_warning) .setPositiveButton(R.string.close, null) .setNeutralButton(R.string.change_backup_volume, (dialog, which) -> { Intent intent = SettingsActivity.getSettingsIntent(this, "backup_restore_prefs", "backup_volume"); startActivity(intent); }) .show(); } } @Override protected void onResume() { super.onResume(); if (viewModel != null) viewModel.onResume(); if (mAdapter != null && mBatchOpsHandler != null && mAdapter.isInSelectionMode()) { mBatchOpsHandler.updateConstraints(); mMultiSelectionView.updateCounter(false); } ContextCompat.registerReceiver(this, mBatchOpsBroadCastReceiver, new IntentFilter(BatchOpsService.ACTION_BATCH_OPS_COMPLETED), ContextCompat.RECEIVER_NOT_EXPORTED); } @Override protected void onPause() { super.onPause(); unregisterReceiver(mBatchOpsBroadCastReceiver); } private void displayChangelogIfRequired() { if (!AppPref.getBoolean(AppPref.PrefKey.PREF_DISPLAY_CHANGELOG_BOOL)) { return; } if (FundingCampaignChecker.campaignRunning()) { new ScrollableDialogBuilder(this) .setMessage(R.string.funding_campaign_dialog_message) .enableAnchors() .show(); } Snackbar.make(findViewById(android.R.id.content), R.string.view_changelog, 3 * 60 * 1000) .setAction(R.string.ok, v -> { long lastVersion = AppPref.getLong(AppPref.PrefKey.PREF_DISPLAY_CHANGELOG_LAST_VERSION_LONG); AppPref.set(AppPref.PrefKey.PREF_DISPLAY_CHANGELOG_BOOL, false); AppPref.set(AppPref.PrefKey.PREF_DISPLAY_CHANGELOG_LAST_VERSION_LONG, (long) BuildConfig.VERSION_CODE); viewModel.executor.submit(() -> { Changelog changelog; try { changelog = new ChangelogParser(getApplication(), R.raw.changelog, lastVersion).parse(); } catch (IOException | XmlPullParserException e) { return; } runOnUiThread(() -> { View view = View.inflate(this, R.layout.dialog_whats_new, null); RecyclerView recyclerView = view.findViewById(android.R.id.list); recyclerView.setLayoutManager(new LinearLayoutManager(recyclerView.getContext())); ChangelogRecyclerAdapter adapter = new ChangelogRecyclerAdapter(); recyclerView.setAdapter(adapter); adapter.setAdapterList(changelog.getChangelogItems()); new AlertDialogBuilder(this, true) .setTitle(R.string.changelog) .setView(recyclerView) .show(); }); }); }).show(); } private void showFreezeUnfreezeDialog(int freezeType) { View view = View.inflate(this, R.layout.item_checkbox, null); MaterialCheckBox checkBox = view.findViewById(R.id.checkbox); checkBox.setText(R.string.freeze_prefer_per_app_option); FreezeUnfreeze.getFreezeDialog(this, freezeType) .setIcon(R.drawable.ic_snowflake) .setTitle(R.string.freeze_unfreeze) .setView(view) .setPositiveButton(R.string.freeze, (dialog, which, selectedItem) -> { if (selectedItem == null) { return; } BatchFreezeOptions options = new BatchFreezeOptions(selectedItem, checkBox.isChecked()); handleBatchOp(BatchOpsManager.OP_ADVANCED_FREEZE, options); }) .setNegativeButton(R.string.cancel, null) .setNeutralButton(R.string.unfreeze, (dialog, which, selectedItem) -> handleBatchOp(BatchOpsManager.OP_UNFREEZE)) .show(); } private void handleBatchOp(@BatchOpsManager.OpType int op) { handleBatchOp(op, null); } private void handleBatchOp(@BatchOpsManager.OpType int op, @Nullable IBatchOpOptions options) { if (viewModel == null) return; showProgressIndicator(true); BatchOpsManager.Result input = new BatchOpsManager.Result(viewModel.getSelectedPackagesWithUsers()); BatchQueueItem item = BatchQueueItem.getBatchOpQueue(op, input.getFailedPackages(), input.getAssociatedUsers(), options); Intent intent = BatchOpsService.getServiceIntent(this, item); ContextCompat.startForegroundService(this, intent); } private void handleBatchOpWithWarning(@BatchOpsManager.OpType int op) { new MaterialAlertDialogBuilder(this) .setTitle(R.string.are_you_sure) .setMessage(R.string.this_action_cannot_be_undone) .setPositiveButton(R.string.yes, (dialog, which) -> handleBatchOp(op)) .setNegativeButton(R.string.no, null) .show(); } void showProgressIndicator(boolean show) { if (show) mProgressIndicator.show(); else mProgressIndicator.hide(); } @Override public boolean onQueryTextChange(String searchQuery, @AdvancedSearchView.SearchType int type) { if (viewModel != null) viewModel.setSearchQuery(searchQuery, type); return true; } @Override public boolean onQueryTextSubmit(String query, int type) { return false; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/main/MainBatchOpsHandler.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.main; import android.Manifest; import android.content.pm.ApplicationInfo; import android.os.Build; import android.view.Menu; import android.view.MenuItem; import java.util.Collection; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.compat.ManifestCompat; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.widget.MultiSelectionView; public class MainBatchOpsHandler implements MultiSelectionView.OnSelectionChangeListener { private final MainViewModel mViewModel; private final MenuItem mUninstallMenu; private final MenuItem mFreezeUnfreezeMenu; private final MenuItem mForceStopMenu; private final MenuItem mClearDataCacheMenu; private final MenuItem mSaveApkMenu; private final MenuItem mBackupRestoreMenu; private final MenuItem mPreventBackgroundMenu; private final MenuItem mBlockUnblockTrackersMenu; private final MenuItem mNetPolicyMenu; private final MenuItem mExportRulesMenu; private final MenuItem mExportAppListMenu; private final MenuItem mOptimizeMenu; private final MenuItem mAddToProfileMenu; private boolean mCanFreezeUnfreezePackages; private boolean mCanForceStopPackages; private boolean mCanClearData; private boolean mCanClearCache; private boolean mCanModifyAppOpMode; private boolean mCanModifyNetPolicy; private boolean mCanModifyComponentState; public MainBatchOpsHandler(MultiSelectionView multiSelectionView, MainViewModel viewModel) { Menu selectionMenu = multiSelectionView.getMenu(); mViewModel = viewModel; mUninstallMenu = selectionMenu.findItem(R.id.action_uninstall); mFreezeUnfreezeMenu = selectionMenu.findItem(R.id.action_freeze_unfreeze); mForceStopMenu = selectionMenu.findItem(R.id.action_force_stop); mClearDataCacheMenu = selectionMenu.findItem(R.id.action_clear_data_cache); mSaveApkMenu = selectionMenu.findItem(R.id.action_save_apk); mBackupRestoreMenu = selectionMenu.findItem(R.id.action_backup); mPreventBackgroundMenu = selectionMenu.findItem(R.id.action_disable_background); mBlockUnblockTrackersMenu = selectionMenu.findItem(R.id.action_block_unblock_trackers); mNetPolicyMenu = selectionMenu.findItem(R.id.action_net_policy); mExportRulesMenu = selectionMenu.findItem(R.id.action_export_blocking_rules); mExportAppListMenu = selectionMenu.findItem(R.id.action_export_app_list); mOptimizeMenu = selectionMenu.findItem(R.id.action_optimize); mAddToProfileMenu = selectionMenu.findItem(R.id.action_add_to_profile); updateConstraints(); } public void updateConstraints() { mCanFreezeUnfreezePackages = SelfPermissions.canFreezeUnfreezePackages(); mCanForceStopPackages = SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.FORCE_STOP_PACKAGES); mCanClearData = SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.CLEAR_APP_USER_DATA); mCanClearCache = SelfPermissions.canClearAppCache(); mCanModifyAppOpMode = SelfPermissions.canModifyAppOpMode(); mCanModifyNetPolicy = SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.MANAGE_NETWORK_POLICY); mCanModifyComponentState = SelfPermissions.checkSelfOrRemotePermission(Manifest.permission.CHANGE_COMPONENT_ENABLED_STATE); } @Override public boolean onSelectionChange(int selectionCount) { Collection selectedItems = mViewModel.getSelectedApplicationItems(); boolean nonZeroSelection = !selectedItems.isEmpty(); // It was ensured that the algorithm is greedy // Best case: O(1) // Worst case: O(n) boolean areAllInstalled = true; boolean areAllUninstalledWithoutData = true; boolean areAllUninstalledSystem = true; boolean doAllUninstalledhaveBackup = true; for (ApplicationItem item : selectedItems) { if (item.isInstalled) continue; areAllInstalled = false; if (areAllUninstalledWithoutData) { areAllUninstalledWithoutData = item.isOnlyDataInstalled; } if (!doAllUninstalledhaveBackup && !areAllUninstalledSystem) { // No need to check further break; } if (areAllUninstalledSystem && item.isUser) { areAllUninstalledSystem = false; } if (doAllUninstalledhaveBackup && item.backup == null) { doAllUninstalledhaveBackup = false; } } /* === Enable/Disable === */ // Enable “Uninstall” action iff all selections are installed mUninstallMenu.setEnabled(nonZeroSelection && (areAllInstalled || areAllUninstalledWithoutData)); mFreezeUnfreezeMenu.setEnabled(nonZeroSelection && areAllInstalled); mForceStopMenu.setEnabled(nonZeroSelection && areAllInstalled); mClearDataCacheMenu.setEnabled(nonZeroSelection && areAllInstalled); mPreventBackgroundMenu.setEnabled(nonZeroSelection && areAllInstalled); mNetPolicyMenu.setEnabled(nonZeroSelection && areAllInstalled); mBlockUnblockTrackersMenu.setEnabled(nonZeroSelection && areAllInstalled); // Enable “Save APK” action iff all selections are installed or the uninstalled apps are all system apps mSaveApkMenu.setEnabled(nonZeroSelection && (areAllInstalled || areAllUninstalledSystem)); // Enable “Backup/restore” action iff all selections are installed or all the uninstalled apps have backups mBackupRestoreMenu.setEnabled(nonZeroSelection && (areAllInstalled || doAllUninstalledhaveBackup)); // Rests are enabled by default mExportRulesMenu.setEnabled(nonZeroSelection); mExportAppListMenu.setEnabled(nonZeroSelection); mOptimizeMenu.setEnabled(nonZeroSelection); mAddToProfileMenu.setEnabled(nonZeroSelection); /* === Visible/Invisible === */ mFreezeUnfreezeMenu.setVisible(mCanFreezeUnfreezePackages); mForceStopMenu.setVisible(mCanForceStopPackages); mClearDataCacheMenu.setVisible(mCanClearData || mCanClearCache); mPreventBackgroundMenu.setVisible(mCanModifyAppOpMode && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N); mNetPolicyMenu.setVisible(mCanModifyNetPolicy); mBlockUnblockTrackersMenu.setVisible(mCanModifyComponentState); mOptimizeMenu.setVisible(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N); return true; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/main/MainListOptions.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.main; import android.os.Bundle; import android.text.TextUtils; import android.view.View; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.concurrent.Future; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.filters.FilterItem; import io.github.muntashirakon.AppManager.filters.options.AppTypeOption; import io.github.muntashirakon.AppManager.filters.options.BackupOption; import io.github.muntashirakon.AppManager.filters.options.ComponentsOption; import io.github.muntashirakon.AppManager.filters.options.FreezeOption; import io.github.muntashirakon.AppManager.filters.options.InstalledOption; import io.github.muntashirakon.AppManager.filters.options.RunningAppsOption; import io.github.muntashirakon.AppManager.misc.ListOptions; import io.github.muntashirakon.AppManager.profiles.ProfileManager; import io.github.muntashirakon.AppManager.settings.FeatureController; import io.github.muntashirakon.AppManager.settings.Ops; import io.github.muntashirakon.AppManager.users.UserInfo; import io.github.muntashirakon.AppManager.users.Users; import io.github.muntashirakon.AppManager.utils.ArrayUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.adapters.SelectedArrayAdapter; import io.github.muntashirakon.dialog.SearchableMultiChoiceDialogBuilder; public class MainListOptions extends ListOptions { public static final String TAG = MainListOptions.class.getSimpleName(); @IntDef(value = { SORT_BY_DOMAIN, SORT_BY_APP_LABEL, SORT_BY_PACKAGE_NAME, SORT_BY_LAST_UPDATE, SORT_BY_SHARED_ID, SORT_BY_TARGET_SDK, SORT_BY_SHA, SORT_BY_FROZEN_APP, SORT_BY_BLOCKED_COMPONENTS, SORT_BY_BACKUP, SORT_BY_TRACKERS, SORT_BY_LAST_ACTION, SORT_BY_INSTALLATION_DATE, SORT_BY_TOTAL_SIZE, SORT_BY_DATA_USAGE, SORT_BY_OPEN_COUNT, SORT_BY_SCREEN_TIME, SORT_BY_LAST_USAGE_TIME, }) @Retention(RetentionPolicy.SOURCE) public @interface SortOrder { } public static final int SORT_BY_DOMAIN = 0; // User/system app public static final int SORT_BY_APP_LABEL = 1; public static final int SORT_BY_PACKAGE_NAME = 2; public static final int SORT_BY_LAST_UPDATE = 3; public static final int SORT_BY_SHARED_ID = 4; public static final int SORT_BY_TARGET_SDK = 5; public static final int SORT_BY_SHA = 6; // Signature public static final int SORT_BY_FROZEN_APP = 7; public static final int SORT_BY_BLOCKED_COMPONENTS = 8; public static final int SORT_BY_BACKUP = 9; public static final int SORT_BY_TRACKERS = 10; public static final int SORT_BY_LAST_ACTION = 11; public static final int SORT_BY_INSTALLATION_DATE = 12; public static final int SORT_BY_TOTAL_SIZE = 13; public static final int SORT_BY_DATA_USAGE = 14; public static final int SORT_BY_OPEN_COUNT = 15; public static final int SORT_BY_SCREEN_TIME = 16; public static final int SORT_BY_LAST_USAGE_TIME = 17; @IntDef(flag = true, value = { FILTER_NO_FILTER, FILTER_USER_APPS, FILTER_SYSTEM_APPS, FILTER_FROZEN_APPS, FILTER_UNFROZEN_APPS, FILTER_APPS_WITH_RULES, FILTER_APPS_WITH_ACTIVITIES, FILTER_APPS_WITH_BACKUPS, FILTER_RUNNING_APPS, FILTER_APPS_WITH_SPLITS, FILTER_INSTALLED_APPS, FILTER_UNINSTALLED_APPS, FILTER_APPS_WITHOUT_BACKUPS, FILTER_APPS_WITH_KEYSTORE, FILTER_APPS_WITH_SAF, FILTER_APPS_WITH_SSAID, FILTER_STOPPED_APPS, }) @Retention(RetentionPolicy.SOURCE) public @interface Filter { } public static final int FILTER_NO_FILTER = 0; public static final int FILTER_USER_APPS = 1; public static final int FILTER_SYSTEM_APPS = 1 << 1; public static final int FILTER_FROZEN_APPS = 1 << 2; public static final int FILTER_APPS_WITH_RULES = 1 << 3; public static final int FILTER_APPS_WITH_ACTIVITIES = 1 << 4; public static final int FILTER_APPS_WITH_BACKUPS = 1 << 5; public static final int FILTER_RUNNING_APPS = 1 << 6; public static final int FILTER_APPS_WITH_SPLITS = 1 << 7; public static final int FILTER_INSTALLED_APPS = 1 << 8; public static final int FILTER_UNINSTALLED_APPS = 1 << 9; public static final int FILTER_APPS_WITHOUT_BACKUPS = 1 << 10; public static final int FILTER_APPS_WITH_KEYSTORE = 1 << 11; public static final int FILTER_APPS_WITH_SAF = 1 << 12; public static final int FILTER_APPS_WITH_SSAID = 1 << 13; public static final int FILTER_STOPPED_APPS = 1 << 14; public static final int FILTER_UNFROZEN_APPS = 1 << 15; // For now, just generate FilterItem @NonNull public static FilterItem getFilterItemFromFlags(int flags) { FilterItem filterItem = new FilterItem(); // Flags int appTypeWithFlags = 0; if ((flags & FILTER_USER_APPS) != 0) { appTypeWithFlags |= AppTypeOption.APP_TYPE_USER; } if ((flags & FILTER_SYSTEM_APPS) != 0) { appTypeWithFlags |= AppTypeOption.APP_TYPE_SYSTEM; } if ((flags & FILTER_FROZEN_APPS) != 0) { FreezeOption option = new FreezeOption(); option.setKeyValue("frozen", null); filterItem.addFilterOption(option); } if ((flags & FILTER_UNFROZEN_APPS) != 0) { FreezeOption option = new FreezeOption(); option.setKeyValue("unfrozen", null); filterItem.addFilterOption(option); } if ((flags & FILTER_APPS_WITH_RULES) != 0) { appTypeWithFlags |= AppTypeOption.APP_TYPE_WITH_RULES; } if ((flags & FILTER_APPS_WITH_ACTIVITIES) != 0) { ComponentsOption option = new ComponentsOption(); option.setKeyValue("with_type", String.valueOf(ComponentsOption.COMPONENT_TYPE_ACTIVITY)); filterItem.addFilterOption(option); } if ((flags & FILTER_APPS_WITH_BACKUPS) != 0) { BackupOption option = new BackupOption(); option.setKeyValue("backups", null); filterItem.addFilterOption(option); } if ((flags & FILTER_RUNNING_APPS) != 0) { RunningAppsOption option = new RunningAppsOption(); option.setKeyValue("running", null); filterItem.addFilterOption(option); } if ((flags & FILTER_APPS_WITH_SPLITS) != 0) { // TODO: 7/28/25 } if ((flags & FILTER_INSTALLED_APPS) != 0) { InstalledOption option = new InstalledOption(); option.setKeyValue("installed", null); filterItem.addFilterOption(option); } if ((flags & FILTER_UNINSTALLED_APPS) != 0) { InstalledOption option = new InstalledOption(); option.setKeyValue("uninstalled", null); filterItem.addFilterOption(option); } if ((flags & FILTER_APPS_WITHOUT_BACKUPS) != 0) { BackupOption option = new BackupOption(); option.setKeyValue("no_backups", null); filterItem.addFilterOption(option); } if ((flags & FILTER_APPS_WITH_KEYSTORE) != 0) { appTypeWithFlags |= AppTypeOption.APP_TYPE_KEYSTORE; } if ((flags & FILTER_APPS_WITH_SAF) != 0) { // TODO: 7/28/25 } if ((flags & FILTER_APPS_WITH_SSAID) != 0) { appTypeWithFlags |= AppTypeOption.APP_TYPE_SSAID; } if ((flags & FILTER_STOPPED_APPS) != 0) { appTypeWithFlags |= AppTypeOption.APP_TYPE_STOPPED; } if (appTypeWithFlags > 0) { AppTypeOption appTypeWithFlagsOption = new AppTypeOption(); appTypeWithFlagsOption.setKeyValue("with_flags", String.valueOf(appTypeWithFlags)); filterItem.addFilterOption(appTypeWithFlagsOption); } return filterItem; } private final List mProfileNames = new ArrayList<>(); private Future mProfileSuggestionsResult; @Nullable private SelectedArrayAdapter mAdapter; @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); MainActivity activity = (MainActivity) requireActivity(); profileNameSpinner.setOnItemClickListener((parent, view1, position, id) -> { if (mAdapter == null || activity.viewModel == null) { return; } if (position == 0) { // No profiles activity.viewModel.setFilterProfileName(null); } else { activity.viewModel.setFilterProfileName(mAdapter.getItem(position)); } }); mProfileSuggestionsResult = ThreadUtils.postOnBackgroundThread(() -> { mProfileNames.clear(); mProfileNames.add(getString(R.string.no_profiles)); mProfileNames.addAll(ProfileManager.getProfileNames()); mAdapter = new SelectedArrayAdapter<>(activity, io.github.muntashirakon.ui.R.layout.auto_complete_dropdown_item_small, mProfileNames); if (isDetached() || ThreadUtils.isInterrupted()) return; activity.runOnUiThread(() -> { profileNameSpinner.setAdapter(mAdapter); if (activity.viewModel != null) { String selectedProfile = activity.viewModel.getFilterProfileName(); if (TextUtils.isEmpty(selectedProfile)) { profileNameSpinner.setSelection(0); } else { int i = mProfileNames.indexOf(selectedProfile); if (i < 0) { i = 0; } profileNameSpinner.setSelection(i); } } }); }); selectUserView.setVisibility(Users.getUsersIds().length <= 1 ? View.GONE : View.VISIBLE); selectUserView.setOnClickListener(v -> { List userInfoList = Users.getUsers(); List userIdList = new ArrayList<>(userInfoList.size()); CharSequence[] userInfoReadable = new CharSequence[userInfoList.size()]; int i = 0; for (UserInfo userInfo : userInfoList) { userInfoReadable[i] = userInfo.toLocalizedString(requireContext()); userIdList.add(userInfo.id); ++i; } List selections; if (activity.viewModel != null) { int[] selectedUsers = activity.viewModel.getSelectedUsers(); if (selectedUsers != null) { selections = new ArrayList<>(); for (int userId : selectedUsers) { selections.add(userId); } } else selections = userIdList; } else selections = userIdList; new SearchableMultiChoiceDialogBuilder<>(requireContext(), userIdList, userInfoReadable) .setTitle(R.string.filter) .setNegativeButton(R.string.close, null) .addSelections(selections) .showSelectAll(true) .hideSearchBar(true) .setPositiveButton(R.string.filter, (dialog, which, selectedItems) -> { if (activity.viewModel != null) { if (selectedItems.size() == userInfoList.size()) { // All users activity.viewModel.setSelectedUsers(null); } else { activity.viewModel.setSelectedUsers(ArrayUtils.convertToIntArray(selectedItems)); } } }) .show(); }); } @Override public void onDestroy() { if (mProfileSuggestionsResult != null) { mProfileSuggestionsResult.cancel(true); } super.onDestroy(); } @Nullable @Override public LinkedHashMap getSortIdLocaleMap() { return new LinkedHashMap() {{ put(SORT_BY_DOMAIN, R.string.sort_by_domain); put(SORT_BY_APP_LABEL, R.string.sort_by_app_label); put(SORT_BY_PACKAGE_NAME, R.string.sort_by_package_name); put(SORT_BY_LAST_UPDATE, R.string.sort_by_last_update); put(SORT_BY_SHARED_ID, R.string.sort_by_shared_user_id); put(SORT_BY_TARGET_SDK, R.string.sort_by_target_sdk); put(SORT_BY_SHA, R.string.sort_by_sha); put(SORT_BY_FROZEN_APP, R.string.sort_by_frozen_app); put(SORT_BY_BLOCKED_COMPONENTS, R.string.sort_by_blocked_components); put(SORT_BY_BACKUP, R.string.sort_by_backup); put(SORT_BY_TRACKERS, R.string.trackers); put(SORT_BY_LAST_ACTION, R.string.last_actions); put(SORT_BY_INSTALLATION_DATE, R.string.sort_by_installation_date); if (FeatureController.isUsageAccessEnabled()) { put(SORT_BY_TOTAL_SIZE, R.string.sort_by_total_size); put(SORT_BY_DATA_USAGE, R.string.sort_by_data_usage); put(SORT_BY_OPEN_COUNT, R.string.sort_by_times_opened); put(SORT_BY_SCREEN_TIME, R.string.sort_by_screen_time); put(SORT_BY_LAST_USAGE_TIME, R.string.sort_by_last_used); } }}; } @Nullable @Override public LinkedHashMap getFilterFlagLocaleMap() { return new LinkedHashMap() {{ put(FILTER_USER_APPS, R.string.filter_user_apps); put(FILTER_SYSTEM_APPS, R.string.filter_system_apps); put(FILTER_FROZEN_APPS, R.string.filter_frozen_apps); put(FILTER_UNFROZEN_APPS, R.string.filter_unfrozen_apps); put(FILTER_STOPPED_APPS, R.string.filter_force_stopped_apps); put(FILTER_INSTALLED_APPS, R.string.installed_apps); put(FILTER_UNINSTALLED_APPS, R.string.uninstalled_apps); put(FILTER_APPS_WITH_RULES, R.string.filter_apps_with_rules); put(FILTER_APPS_WITH_ACTIVITIES, R.string.filter_apps_with_activities); put(FILTER_APPS_WITH_BACKUPS, R.string.filter_apps_with_backups); put(FILTER_APPS_WITHOUT_BACKUPS, R.string.filter_apps_without_backups); put(FILTER_RUNNING_APPS, R.string.filter_running_apps); put(FILTER_APPS_WITH_SPLITS, R.string.filter_apps_with_splits); if (Ops.isWorkingUidRoot()) { put(FILTER_APPS_WITH_KEYSTORE, R.string.filter_apps_with_keystore); put(FILTER_APPS_WITH_SAF, R.string.filter_apps_with_saf); put(FILTER_APPS_WITH_SSAID, R.string.filter_apps_with_ssaid); } }}; } @Nullable @Override public LinkedHashMap getOptionIdLocaleMap() { return null; } @Override public boolean enableProfileNameInput() { return true; } @Override public boolean enableSelectUser() { return true; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/main/MainRecyclerAdapter.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.main; import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.MATCH_UNINSTALLED_PACKAGES; import static io.github.muntashirakon.AppManager.utils.UIUtils.displayLongToast; import static io.github.muntashirakon.AppManager.utils.UIUtils.displayShortToast; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.graphics.Color; import android.net.Uri; import android.os.RemoteException; import android.os.UserHandleHidden; import android.text.Spannable; import android.text.SpannableString; import android.text.TextUtils; import android.text.style.RelativeSizeSpan; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.SectionIndexer; import android.widget.TextView; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.UiThread; import androidx.appcompat.widget.AppCompatImageView; import androidx.core.content.ContextCompat; import com.google.android.material.card.MaterialCardView; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.apk.installer.PackageInstallerActivity; import io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat; import io.github.muntashirakon.AppManager.backup.dialog.BackupRestoreDialogFragment; import io.github.muntashirakon.AppManager.compat.ApplicationInfoCompat; import io.github.muntashirakon.AppManager.compat.PackageManagerCompat; import io.github.muntashirakon.AppManager.db.entity.Backup; import io.github.muntashirakon.AppManager.details.AppDetailsActivity; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.self.imagecache.ImageLoader; import io.github.muntashirakon.AppManager.settings.FeatureController; import io.github.muntashirakon.AppManager.types.UserPackagePair; import io.github.muntashirakon.AppManager.users.UserInfo; import io.github.muntashirakon.AppManager.users.Users; import io.github.muntashirakon.AppManager.utils.ArrayUtils; import io.github.muntashirakon.AppManager.utils.DateUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.AppManager.utils.appearance.ColorCodes; import io.github.muntashirakon.dialog.SearchableItemsDialogBuilder; import io.github.muntashirakon.io.Paths; import io.github.muntashirakon.util.AccessibilityUtils; import io.github.muntashirakon.util.AdapterUtils; import io.github.muntashirakon.widget.MultiSelectionView; public class MainRecyclerAdapter extends MultiSelectionView.Adapter implements SectionIndexer { private static final String sSections = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; private final MainActivity mActivity; private String mSearchQuery; @GuardedBy("mAdapterList") private final List mAdapterList = new ArrayList<>(); private final int mColorGreen; private final int mColorOrange; private final int mColorPrimary; private final int mColorSecondary; private final int mQueryStringHighlight; MainRecyclerAdapter(@NonNull MainActivity activity) { super(); mActivity = activity; mColorGreen = ContextCompat.getColor(activity, io.github.muntashirakon.ui.R.color.stopped); mColorOrange = ContextCompat.getColor(activity, io.github.muntashirakon.ui.R.color.orange); mColorPrimary = ContextCompat.getColor(activity, io.github.muntashirakon.ui.R.color.textColorPrimary); mColorSecondary = ContextCompat.getColor(activity, io.github.muntashirakon.ui.R.color.textColorSecondary); mQueryStringHighlight = ColorCodes.getQueryStringHighlightColor(activity); } @GuardedBy("mAdapterList") @UiThread void setDefaultList(List list) { if (mActivity.viewModel == null) return; synchronized (mAdapterList) { mSearchQuery = mActivity.viewModel.getSearchQuery(); AdapterUtils.notifyDataSetChanged(this, mAdapterList, list); notifySelectionChange(); } } @GuardedBy("mAdapterList") @Override public void cancelSelection() { super.cancelSelection(); mActivity.viewModel.cancelSelection(); } @Override public int getSelectedItemCount() { if (mActivity.viewModel == null) return 0; return mActivity.viewModel.getSelectedPackages().size(); } @Override protected int getTotalItemCount() { if (mActivity.viewModel == null) return 0; return mActivity.viewModel.getApplicationItemCount(); } @GuardedBy("mAdapterList") @Override protected boolean isSelected(int position) { synchronized (mAdapterList) { return mAdapterList.get(position).isSelected; } } @GuardedBy("mAdapterList") @Override protected boolean select(int position) { synchronized (mAdapterList) { mAdapterList.set(position, mActivity.viewModel.select(mAdapterList.get(position))); return true; } } @GuardedBy("mAdapterList") @Override protected boolean deselect(int position) { synchronized (mAdapterList) { mAdapterList.set(position, mActivity.viewModel.deselect(mAdapterList.get(position))); return true; } } @GuardedBy("mAdapterList") @Override public void toggleSelection(int position) { synchronized (mAdapterList) { super.toggleSelection(position); } } @GuardedBy("mAdapterList") @Override public void selectAll() { synchronized (mAdapterList) { super.selectAll(); } } @GuardedBy("mAdapterList") @Override public void selectRange(int firstPosition, int secondPosition) { synchronized (mAdapterList) { super.selectRange(firstPosition, secondPosition); } } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { final View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_main, parent, false); return new ViewHolder(view); } @GuardedBy("mAdapterList") @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { final ApplicationItem item; synchronized (mAdapterList) { item = mAdapterList.get(position); } MaterialCardView cardView = holder.itemView; Context context = cardView.getContext(); // Add click listeners cardView.setOnClickListener(v -> { // If selection mode is on, select/deselect the current item instead of the default behaviour if (isInSelectionMode()) { toggleSelection(position); AccessibilityUtils.requestAccessibilityFocus(holder.itemView); return; } handleClick(item); }); cardView.setOnLongClickListener(v -> { // Long click listener: Select/deselect an app. // 1) Turn selection mode on if this is the first item in the selection list // 2) Select between last selection position and this position (inclusive) if selection mode is on synchronized (mAdapterList) { ApplicationItem lastSelectedItem = mActivity.viewModel.getLastSelectedPackage(); int lastSelectedItemPosition = lastSelectedItem == null ? -1 : mAdapterList.indexOf(lastSelectedItem); if (lastSelectedItemPosition >= 0) { // Select from last selection to this selection selectRange(lastSelectedItemPosition, position); } else { toggleSelection(position); AccessibilityUtils.requestAccessibilityFocus(holder.itemView); } } return true; }); holder.icon.setOnClickListener(v -> { toggleSelection(position); AccessibilityUtils.requestAccessibilityFocus(holder.itemView); }); // Box-stroke colors: uninstalled > disabled > force-stopped > regular if (!item.isInstalled) { cardView.setStrokeColor(ColorCodes.getAppUninstalledIndicatorColor(context)); } else if (item.isDisabled) { cardView.setStrokeColor(ColorCodes.getAppDisabledIndicatorColor(context)); } else if (item.isStopped) { cardView.setStrokeColor(ColorCodes.getAppForceStoppedIndicatorColor(context)); } else { cardView.setStrokeColor(Color.TRANSPARENT); } // Display yellow star if the app is in debug mode holder.debugIcon.setVisibility(item.debuggable ? View.VISIBLE : View.INVISIBLE); // Set date and (if available,) days between first installation and last update String lastUpdateDate = DateUtils.formatDate(context, item.lastUpdateTime); if (item.firstInstallTime == item.lastUpdateTime) { holder.date.setText(lastUpdateDate); } else { long days = item.diffInstallUpdateInDays; SpannableString ssDate = new SpannableString(context.getResources() .getQuantityString(R.plurals.main_list_date_days, (int) days, lastUpdateDate, days)); ssDate.setSpan(new RelativeSizeSpan(.8f), lastUpdateDate.length(), ssDate.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); holder.date.setText(ssDate); } // Set date color to orange if app can read logs (and accepted) holder.date.setTextColor(item.canReadLogs ? mColorOrange : mColorSecondary); if (item.isInstalled) { // Set UID if (item.uidOrAppIds != null) { holder.userId.setText(item.uidOrAppIds); } // Set UID text color to orange if the package is shared holder.userId.setTextColor(item.sharedUserId != null ? mColorOrange : mColorSecondary); } else holder.userId.setText(""); if (item.sha != null) { // Set issuer holder.issuer.setVisibility(View.VISIBLE); holder.issuer.setText(item.issuerShortName); // Set signature type holder.sha.setVisibility(View.VISIBLE); holder.sha.setText(item.sha.second); } else { holder.issuer.setVisibility(View.GONE); holder.sha.setVisibility(View.GONE); } // Load app icon holder.icon.setTag(item.packageName); ImageLoader.getInstance().displayImage(item.packageName, item, holder.icon); // Set app label if (!TextUtils.isEmpty(mSearchQuery) && item.label.toLowerCase(Locale.ROOT).contains(mSearchQuery)) { // Highlight searched query holder.label.setText(UIUtils.getHighlightedText(item.label, mSearchQuery, mQueryStringHighlight)); } else holder.label.setText(item.label); // Set app label color to red if clearing user data not allowed if (item.isInstalled && !item.allowClearingUserData) { holder.label.setTextColor(Color.RED); } else holder.label.setTextColor(mColorPrimary); // Set package name if (!TextUtils.isEmpty(mSearchQuery) && item.packageName.toLowerCase(Locale.ROOT).contains(mSearchQuery)) { // Highlight searched query holder.packageName.setText(UIUtils.getHighlightedText(item.packageName, mSearchQuery, mQueryStringHighlight)); } else holder.packageName.setText(item.packageName); // Set package name color to orange if the app has known tracker components if (item.trackerCount > 0) { holder.packageName.setTextColor(ColorCodes.getComponentTrackerIndicatorColor(context)); } else holder.packageName.setTextColor(mColorSecondary); // Set version (along with HW accelerated, debug and test only flags) holder.version.setText(item.versionTag); // Set version color to dark cyan if the app is inactive holder.version.setTextColor(item.isAppInactive ? mColorGreen : mColorSecondary); // Set app type: system or user app (along with large heap, suspended, multi-arch, // has code, vm safe mode) if (item.isInstalled) { String isSystemApp = context.getString(item.isSystem ? R.string.system : R.string.user) + item.appTypePostfix; holder.isSystemApp.setText(isSystemApp); } else { holder.isSystemApp.setText("-"); } // Set app type text color to magenta if the app is persistent holder.isSystemApp.setTextColor(item.isPersistent ? Color.MAGENTA : mColorSecondary); // Set SDK if (item.sdkString != null) { holder.size.setText(item.sdkString); } else holder.size.setText("-"); // Set SDK color to orange if the app is using cleartext (e.g. HTTP) traffic holder.size.setTextColor(item.usesCleartextTraffic ? mColorOrange : mColorSecondary); // Check for backup if (item.backup != null) { holder.backupIndicator.setVisibility(View.VISIBLE); holder.backupInfo.setVisibility(View.VISIBLE); holder.backupInfoExt.setVisibility(View.VISIBLE); holder.backupIndicator.setText(R.string.backup); int indicatorColor; if (item.isInstalled) { if (item.backup.versionCode >= item.versionCode) { // Up-to-date backup indicatorColor = ColorCodes.getBackupLatestIndicatorColor(context); } else { // Outdated backup indicatorColor = ColorCodes.getBackupOutdatedIndicatorColor(context); } } else { // App not installed indicatorColor = ColorCodes.getBackupUninstalledIndicatorColor(context); } holder.backupIndicator.setTextColor(indicatorColor); Backup backup = item.backup; long days = item.lastBackupDays; holder.backupInfo.setText(String.format("%s: %s, %s %s", context.getString(R.string.latest_backup), context.getResources() .getQuantityString(R.plurals.usage_days, (int) days, days), context.getString(R.string.version), backup.versionName)); holder.backupInfoExt.setText(item.backupFlagsStr); } else { holder.backupIndicator.setVisibility(View.GONE); holder.backupInfo.setVisibility(View.GONE); holder.backupInfoExt.setVisibility(View.GONE); } super.onBindViewHolder(holder, position); } @GuardedBy("mAdapterList") @Override public long getItemId(int position) { synchronized (mAdapterList) { return mAdapterList.get(position).hashCode(); } } @GuardedBy("mAdapterList") @Override public int getItemCount() { synchronized (mAdapterList) { return mAdapterList.size(); } } @GuardedBy("mAdapterList") @Override public int getPositionForSection(int section) { synchronized (mAdapterList) { for (int i = 0; i < getItemCount(); i++) { String item = mAdapterList.get(i).label; if (!item.isEmpty()) { if (item.charAt(0) == sSections.charAt(section)) return i; } } return 0; } } @Override public int getSectionForPosition(int i) { return 0; } @Override public Object[] getSections() { String[] sectionsArr = new String[sSections.length()]; for (int i = 0; i < sSections.length(); i++) sectionsArr[i] = String.valueOf(sSections.charAt(i)); return sectionsArr; } private void handleClick(@NonNull ApplicationItem item) { if (!item.isInstalled || item.userIds.length == 0) { // The app should not be installed. But make sure this is really true. (For current user only) ApplicationInfo info; try { info = PackageManagerCompat.getApplicationInfo(item.packageName, MATCH_UNINSTALLED_PACKAGES | PackageManagerCompat.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES, UserHandleHidden.myUserId()); } catch (RemoteException | PackageManager.NameNotFoundException e) { showBackupRestoreDialogOrAppNotInstalled(item); return; } // 1. Check if the app was really uninstalled. if (ApplicationInfoCompat.isInstalled(info)) { // The app is already installed, and we were wrong to assume that it was installed. // Update data before opening it. item.isInstalled = true; item.isOnlyDataInstalled = false; item.userIds = new int[]{UserHandleHidden.myUserId()}; Intent intent = AppDetailsActivity.getIntent(mActivity, item.packageName, UserHandleHidden.myUserId()); mActivity.startActivity(intent); return; } // 2. If the app can be installed, offer it to install again. if (FeatureController.isInstallerEnabled()) { if (ApplicationInfoCompat.isSystemApp(info) && SelfPermissions.canInstallExistingPackages()) { // Install existing app instead of installing as an update mActivity.startActivity(PackageInstallerActivity.getLaunchableInstance(mActivity, item.packageName)); return; } // Otherwise, try with APK files // FIXME: 1/4/23 Include splits if (Paths.exists(info.publicSourceDir)) { mActivity.startActivity(PackageInstallerActivity.getLaunchableInstance(mActivity, Uri.fromFile(new File(info.publicSourceDir)))); return; } } // 3. The app might be uninstalled without clearing data if (ApplicationInfoCompat.isSystemApp(info)) { // The app is a system app, there's no point in asking to uninstall it again showBackupRestoreDialogOrAppNotInstalled(item); return; } new MaterialAlertDialogBuilder(mActivity) .setTitle(mActivity.getString(R.string.uninstall_app, item.label)) .setMessage(R.string.uninstall_app_again_message) .setNegativeButton(R.string.no, null) .setPositiveButton(R.string.yes, (dialog, which) -> ThreadUtils.postOnBackgroundThread(() -> { PackageInstallerCompat installer = PackageInstallerCompat.getNewInstance(); installer.setAppLabel(item.label); boolean uninstalled = installer.uninstall(item.packageName, UserHandleHidden.myUserId(), false); ThreadUtils.postOnMainThread(() -> { if (uninstalled) { displayLongToast(R.string.uninstalled_successfully, item.label); } else { displayLongToast(R.string.failed_to_uninstall, item.label); } }); })) .show(); return; } // The app is installed if (item.userIds.length == 1) { int[] userHandles = Users.getUsersIds(); if (ArrayUtils.contains(userHandles, item.userIds[0])) { Intent intent = AppDetailsActivity.getIntent(mActivity, item.packageName, item.userIds[0]); mActivity.startActivity(intent); return; } // Outside our jurisdiction showBackupRestoreDialogOrAppNotInstalled(item); return; } // More than a user, ask the user to select one CharSequence[] userNames = new String[item.userIds.length]; List users = Users.getUsers(); for (UserInfo info : users) { for (int i = 0; i < item.userIds.length; ++i) { if (info.id == item.userIds[i]) { userNames[i] = info.toLocalizedString(mActivity); } } } new SearchableItemsDialogBuilder<>(mActivity, userNames) .setTitle(R.string.select_user) .setOnItemClickListener((dialog, which, item1) -> { Intent intent = AppDetailsActivity.getIntent(mActivity, item.packageName, item.userIds[which]); mActivity.startActivity(intent); dialog.dismiss(); }) .setNegativeButton(R.string.cancel, null) .show(); } private void showBackupRestoreDialogOrAppNotInstalled(@NonNull ApplicationItem item) { if (item.backup == null) { // No backups displayShortToast(R.string.app_not_installed); return; } // Has backups BackupRestoreDialogFragment fragment = BackupRestoreDialogFragment.getInstance( Collections.singletonList(new UserPackagePair( item.packageName, UserHandleHidden.myUserId()))); fragment.setOnActionBeginListener(mode -> mActivity.showProgressIndicator(true)); fragment.setOnActionCompleteListener((mode, failedPackages) -> mActivity.showProgressIndicator(false)); fragment.show(mActivity.getSupportFragmentManager(), BackupRestoreDialogFragment.TAG); } public static class ViewHolder extends MultiSelectionView.ViewHolder { MaterialCardView itemView; AppCompatImageView icon; AppCompatImageView debugIcon; TextView label; TextView packageName; TextView version; TextView isSystemApp; TextView date; TextView size; TextView userId; TextView issuer; TextView sha; TextView backupIndicator; TextView backupInfo; TextView backupInfoExt; public ViewHolder(@NonNull View itemView) { super(itemView); this.itemView = (MaterialCardView) itemView; icon = itemView.findViewById(R.id.icon); debugIcon = itemView.findViewById(R.id.favorite_icon); label = itemView.findViewById(R.id.label); packageName = itemView.findViewById(R.id.packageName); version = itemView.findViewById(R.id.version); isSystemApp = itemView.findViewById(R.id.isSystem); date = itemView.findViewById(R.id.date); size = itemView.findViewById(R.id.size); userId = itemView.findViewById(R.id.shareid); issuer = itemView.findViewById(R.id.issuer); sha = itemView.findViewById(R.id.sha); backupIndicator = itemView.findViewById(R.id.backup_indicator); backupInfo = itemView.findViewById(R.id.backup_info); backupInfoExt = itemView.findViewById(R.id.backup_info_ext); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/main/MainViewModel.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.main; import android.app.ActivityManager; import android.app.Application; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.RemoteException; import android.os.UserHandleHidden; import android.text.TextUtils; import android.util.Pair; import androidx.annotation.AnyThread; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import org.json.JSONException; import java.io.BufferedWriter; import java.io.IOException; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; import java.text.Collator; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.concurrent.Future; import io.github.muntashirakon.AppManager.apk.list.ListExporter; import io.github.muntashirakon.AppManager.backup.BackupUtils; import io.github.muntashirakon.AppManager.compat.ActivityManagerCompat; import io.github.muntashirakon.AppManager.compat.PackageManagerCompat; import io.github.muntashirakon.AppManager.db.entity.App; import io.github.muntashirakon.AppManager.db.utils.AppDb; import io.github.muntashirakon.AppManager.filters.FilterItem; import io.github.muntashirakon.AppManager.filters.options.FilterOption; import io.github.muntashirakon.AppManager.filters.options.PackageNameOption; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.misc.AdvancedSearchView; import io.github.muntashirakon.AppManager.misc.ListOptions; import io.github.muntashirakon.AppManager.profiles.ProfileManager; import io.github.muntashirakon.AppManager.profiles.struct.AppsFilterProfile; import io.github.muntashirakon.AppManager.profiles.struct.AppsProfile; import io.github.muntashirakon.AppManager.profiles.struct.BaseProfile; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.settings.FeatureController; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.types.PackageChangeReceiver; import io.github.muntashirakon.AppManager.types.UserPackagePair; import io.github.muntashirakon.AppManager.usage.AppUsageStatsManager; import io.github.muntashirakon.AppManager.usage.PackageUsageInfo; import io.github.muntashirakon.AppManager.usage.TimeInterval; import io.github.muntashirakon.AppManager.usage.UsageUtils; import io.github.muntashirakon.AppManager.users.Users; import io.github.muntashirakon.AppManager.utils.ArrayUtils; import io.github.muntashirakon.AppManager.utils.ExUtils; import io.github.muntashirakon.AppManager.utils.MultithreadedExecutor; import io.github.muntashirakon.AppManager.utils.PackageUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.Utils; import io.github.muntashirakon.io.Path; public class MainViewModel extends AndroidViewModel implements ListOptions.ListOptionActions { private final PackageManager mPackageManager; private final PackageIntentReceiver mPackageObserver; @MainListOptions.SortOrder private int mSortBy; private boolean mReverseSort; @MainListOptions.Filter private int mFilterFlags; @Nullable private String mFilterProfileName; @Nullable private int[] mSelectedUsers; private String mSearchQuery; @AdvancedSearchView.SearchType private int mSearchType; private Future mFilterResult; private final Map mSelectedPackageApplicationItemMap = Collections.synchronizedMap(new LinkedHashMap<>()); final MultithreadedExecutor executor = MultithreadedExecutor.getNewInstance(); public MainViewModel(@NonNull Application application) { super(application); Log.d("MVM", "New instance created"); mPackageManager = application.getPackageManager(); mPackageObserver = new PackageIntentReceiver(this); mSortBy = Prefs.MainPage.getSortOrder(); mReverseSort = Prefs.MainPage.isReverseSort(); mFilterFlags = Prefs.MainPage.getFilters(); mFilterProfileName = Prefs.MainPage.getFilteredProfileName(); mSelectedUsers = null; // TODO: 5/6/23 Load from prefs? if ("".equals(mFilterProfileName)) mFilterProfileName = null; } private final MutableLiveData mOperationStatus = new MutableLiveData<>(); @NonNull private final MutableLiveData> mApplicationItemsLiveData = new MutableLiveData<>(); private final List mApplicationItems = new ArrayList<>(); public int getApplicationItemCount() { return mApplicationItems.size(); } @NonNull public LiveData> getApplicationItems() { if (mApplicationItemsLiveData.getValue() == null) { loadApplicationItems(); } return mApplicationItemsLiveData; } public LiveData getOperationStatus() { return mOperationStatus; } @GuardedBy("applicationItems") public ApplicationItem deselect(@NonNull ApplicationItem item) { synchronized (mApplicationItems) { int i = mApplicationItems.indexOf(item); if (i == -1) return item; item = mApplicationItems.get(i); mSelectedPackageApplicationItemMap.remove(item.packageName); item.isSelected = false; mApplicationItems.set(i, item); return item; } } @GuardedBy("applicationItems") public ApplicationItem select(@NonNull ApplicationItem item) { synchronized (mApplicationItems) { int i = mApplicationItems.indexOf(item); if (i == -1) return item; item = mApplicationItems.get(i); // Removal is needed because LinkedHashMap insertion-oriented mSelectedPackageApplicationItemMap.remove(item.packageName); mSelectedPackageApplicationItemMap.put(item.packageName, item); item.isSelected = true; mApplicationItems.set(i, item); return item; } } public void cancelSelection() { synchronized (mApplicationItems) { for (ApplicationItem item : getSelectedApplicationItems()) { int i = mApplicationItems.indexOf(item); if (i != -1) { mApplicationItems.get(i).isSelected = false; } } mSelectedPackageApplicationItemMap.clear(); } } @Nullable public ApplicationItem getLastSelectedPackage() { // Last selected package is the same as the last added package. Iterator it = mSelectedPackageApplicationItemMap.values().iterator(); ApplicationItem lastItem = null; while (it.hasNext()) { lastItem = it.next(); } return lastItem; } public Map getSelectedPackages() { return mSelectedPackageApplicationItemMap; } @NonNull public ArrayList getSelectedPackagesWithUsers() { ArrayList userPackagePairs = new ArrayList<>(); int myUserId = UserHandleHidden.myUserId(); int[] userIds = Users.getUsersIds(); for (String packageName : mSelectedPackageApplicationItemMap.keySet()) { int[] userIds1 = Objects.requireNonNull(mSelectedPackageApplicationItemMap.get(packageName)).userIds; if (userIds1.length == 0) { // Could be a backup only item // Assign current user in it userPackagePairs.add(new UserPackagePair(packageName, myUserId)); } else { for (int userHandle : userIds1) { if (!ArrayUtils.contains(userIds, userHandle)) continue; userPackagePairs.add(new UserPackagePair(packageName, userHandle)); } } } return userPackagePairs; } public Collection getSelectedApplicationItems() { return mSelectedPackageApplicationItemMap.values(); } public String getSearchQuery() { return mSearchQuery; } public void setSearchQuery(String searchQuery, @AdvancedSearchView.SearchType int searchType) { this.mSearchQuery = searchType != AdvancedSearchView.SEARCH_TYPE_REGEX ? searchQuery.toLowerCase(Locale.ROOT) : searchQuery; this.mSearchType = searchType; cancelIfRunning(); mFilterResult = executor.submit(this::filterItemsByFlags); } @Override public int getSortBy() { return mSortBy; } @Override public void setReverseSort(boolean reverseSort) { cancelIfRunning(); mFilterResult = executor.submit(() -> { sortApplicationList(mSortBy, mReverseSort); filterItemsByFlags(); }); mReverseSort = reverseSort; Prefs.MainPage.setReverseSort(mReverseSort); } @Override public boolean isReverseSort() { return mReverseSort; } @Override public void setSortBy(int sortBy) { if (mSortBy != sortBy) { cancelIfRunning(); mFilterResult = executor.submit(() -> { sortApplicationList(sortBy, mReverseSort); filterItemsByFlags(); }); } mSortBy = sortBy; Prefs.MainPage.setSortOrder(mSortBy); } @Override public boolean hasFilterFlag(@MainListOptions.Filter int flag) { return (mFilterFlags & flag) != 0; } @Override public void addFilterFlag(@MainListOptions.Filter int filterFlag) { mFilterFlags |= filterFlag; Prefs.MainPage.setFilters(mFilterFlags); cancelIfRunning(); mFilterResult = executor.submit(this::filterItemsByFlags); } @Override public void removeFilterFlag(@MainListOptions.Filter int filterFlag) { mFilterFlags &= ~filterFlag; Prefs.MainPage.setFilters(mFilterFlags); cancelIfRunning(); mFilterResult = executor.submit(this::filterItemsByFlags); } public void setFilterProfileName(@Nullable String filterProfileName) { if (mFilterProfileName == null) { if (filterProfileName == null) return; } else if (mFilterProfileName.equals(filterProfileName)) return; mFilterProfileName = filterProfileName; Prefs.MainPage.setFilteredProfileName(filterProfileName); cancelIfRunning(); mFilterResult = executor.submit(this::filterItemsByFlags); } @Nullable public String getFilterProfileName() { return mFilterProfileName; } public void setSelectedUsers(@Nullable int[] selectedUsers) { if (selectedUsers == null) { if (mSelectedUsers == null) { // No change return; } } else if (mSelectedUsers != null) { if (mSelectedUsers.length == selectedUsers.length) { boolean differs = false; for (int user : selectedUsers) { if (!ArrayUtils.contains(mSelectedUsers, user)) { differs = true; break; } } if (!differs) { // No change detected return; } } } mSelectedUsers = selectedUsers; // TODO: 5/6/23 Store value to prefs cancelIfRunning(); mFilterResult = executor.submit(this::filterItemsByFlags); } @Nullable public int[] getSelectedUsers() { return mSelectedUsers; } @AnyThread public void onResume() { if ((mFilterFlags & MainListOptions.FILTER_RUNNING_APPS) != 0) { // Reload filters to get running apps again cancelIfRunning(); mFilterResult = executor.submit(this::filterItemsByFlags); } } public void saveExportedAppList(@ListExporter.ExportType int exportType, @NonNull Path path) { executor.submit(() -> { try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(path.openOutputStream(), StandardCharsets.UTF_8))) { List packageInfoList = new ArrayList<>(); for (String packageName : getSelectedPackages().keySet()) { int[] userIds = Objects.requireNonNull(getSelectedPackages().get(packageName)).userIds; for (int userId : userIds) { packageInfoList.add(PackageManagerCompat.getPackageInfo(packageName, PackageManagerCompat.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES, userId)); break; } } ListExporter.export(getApplication(), writer, exportType, packageInfoList); mOperationStatus.postValue(true); } catch (IOException | RemoteException | PackageManager.NameNotFoundException e) { e.printStackTrace(); mOperationStatus.postValue(false); } }); } @GuardedBy("applicationItems") public void loadApplicationItems() { cancelIfRunning(); mFilterResult = executor.submit(() -> { List updatedApplicationItems = PackageUtils .getInstalledOrBackedUpApplicationsFromDb(getApplication(), true, true); synchronized (mApplicationItems) { mApplicationItems.clear(); mApplicationItems.addAll(updatedApplicationItems); // select apps again for (ApplicationItem item : getSelectedApplicationItems()) { select(item); } sortApplicationList(mSortBy, mReverseSort); filterItemsByFlags(); } }); } private void cancelIfRunning() { if (mFilterResult != null) { mFilterResult.cancel(true); } } @WorkerThread private void filterItemsByQuery(@NonNull List applicationItems) { List filteredApplicationItems; if (mSearchType == AdvancedSearchView.SEARCH_TYPE_REGEX) { filteredApplicationItems = AdvancedSearchView.matches(mSearchQuery, applicationItems, (AdvancedSearchView.ChoicesGenerator) item -> new ArrayList() {{ add(item.packageName); add(item.label); }}, AdvancedSearchView.SEARCH_TYPE_REGEX); mApplicationItemsLiveData.postValue(filteredApplicationItems); return; } // Others filteredApplicationItems = new ArrayList<>(); for (ApplicationItem item : applicationItems) { if (ThreadUtils.isInterrupted()) { return; } if (AdvancedSearchView.matches(mSearchQuery, item.packageName.toLowerCase(Locale.ROOT), mSearchType)) { filteredApplicationItems.add(item); } else if (mSearchType == AdvancedSearchView.SEARCH_TYPE_CONTAINS) { if (Utils.containsOrHasInitials(mSearchQuery, item.label)) { filteredApplicationItems.add(item); } } else if (AdvancedSearchView.matches(mSearchQuery, item.label.toLowerCase(Locale.ROOT), mSearchType)) { filteredApplicationItems.add(item); } } mApplicationItemsLiveData.postValue(filteredApplicationItems); } @WorkerThread @GuardedBy("applicationItems") private void filterItemsByFlags() { synchronized (mApplicationItems) { List candidateApplicationItems = new ArrayList<>(); List profileFilterOptions = new ArrayList<>(); if (mFilterProfileName != null) { String profileId = ProfileManager.getProfileIdCompat(mFilterProfileName); Path profilePath = ProfileManager.findProfilePathById(profileId); try { BaseProfile profile = BaseProfile.fromPath(profilePath); if (profile instanceof AppsProfile) { AppsProfile appsProfile = (AppsProfile) profile; PackageNameOption option = new PackageNameOption(); option.setKeyValue("eq_any", TextUtils.join("\n", appsProfile.packages)); profileFilterOptions.add(option); } else if (profile instanceof AppsFilterProfile) { AppsFilterProfile filterProfile = (AppsFilterProfile) profile; FilterItem filterItem = filterProfile.getFilterItem(); for (int i = 0; i < filterItem.getSize(); ++i) { profileFilterOptions.add(filterItem.getFilterOptionAt(i)); } } } catch (IOException | JSONException e) { e.printStackTrace(); } } for (ApplicationItem item : mApplicationItems) { if (ThreadUtils.isInterrupted()) { return; } if (isAmongSelectedUsers(item)) { candidateApplicationItems.add(item); } } // Other filters if (profileFilterOptions.isEmpty() && mFilterFlags == MainListOptions.FILTER_NO_FILTER) { if (!TextUtils.isEmpty(mSearchQuery)) { filterItemsByQuery(candidateApplicationItems); } else { mApplicationItemsLiveData.postValue(candidateApplicationItems); } } else { List filteredApplicationItems = new ArrayList<>(); FilterItem filterItem = MainListOptions.getFilterItemFromFlags(mFilterFlags); for (FilterOption filterOption : profileFilterOptions) { filterItem.addFilterOption(filterOption); } Map packageUsageInfoList = new HashMap<>(); if (filterItem.getTimesUsageInfoUsed() > 0) { boolean hasUsageAccess = FeatureController.isUsageAccessEnabled() && SelfPermissions.checkUsageStatsPermission(); if (hasUsageAccess) { TimeInterval interval = UsageUtils.getLastWeek(); for (int userId : Users.getUsersIds()) { List usageInfoList; usageInfoList = ExUtils.exceptionAsNull(() -> AppUsageStatsManager .getInstance().getUsageStats(interval, userId)); if (usageInfoList != null) { for (PackageUsageInfo info : usageInfoList) { if (ThreadUtils.isInterrupted()) return; PackageUsageInfo oldInfo = packageUsageInfoList.get(info.packageName); if (oldInfo != null) { oldInfo.screenTime += info.screenTime; oldInfo.lastUsageTime += info.lastUsageTime; oldInfo.timesOpened += info.timesOpened; oldInfo.mobileData = AppUsageStatsManager.DataUsage.fromDataUsage(oldInfo.mobileData, info.mobileData); oldInfo.wifiData = AppUsageStatsManager.DataUsage.fromDataUsage(oldInfo.wifiData, info.wifiData); if (info.entries != null) { if (oldInfo.entries == null) { oldInfo.entries = info.entries; } else oldInfo.entries.addAll(info.entries); } } else packageUsageInfoList.put(info.packageName, info); } } } } } HashSet runningPackages = new HashSet<>(); if (filterItem.getTimesRunningOptionUsed() > 0) { for (ActivityManager.RunningAppProcessInfo info : ActivityManagerCompat.getRunningAppProcesses()) { if (info.pkgList != null) { runningPackages.addAll(Arrays.asList(info.pkgList)); } } } for (ApplicationItem item : candidateApplicationItems) { item.setPackageUsageInfo(packageUsageInfoList.get(item.packageName)); item.setRunning(runningPackages.contains(item.packageName)); } List> result = filterItem.getFilteredList(candidateApplicationItems); for (FilterItem.FilteredItemInfo item : result) { if ((mFilterFlags & MainListOptions.FILTER_APPS_WITH_SPLITS) != 0 && !item.info.hasSplits) { continue; } if ((mFilterFlags & MainListOptions.FILTER_APPS_WITH_SAF) != 0 && !item.info.usesSaf) { continue; } filteredApplicationItems.add(item.info); } if (!TextUtils.isEmpty(mSearchQuery)) { filterItemsByQuery(filteredApplicationItems); } else { mApplicationItemsLiveData.postValue(filteredApplicationItems); } } } } private boolean isAmongSelectedUsers(@NonNull ApplicationItem applicationItem) { if (mSelectedUsers == null) { // All users return true; } for (int userId : mSelectedUsers) { if (ArrayUtils.contains(applicationItem.userIds, userId)) { return true; } } return false; } @GuardedBy("applicationItems") private void sortApplicationList(@MainListOptions.SortOrder int sortBy, boolean reverse) { synchronized (mApplicationItems) { if (sortBy != MainListOptions.SORT_BY_APP_LABEL) { sortApplicationList(MainListOptions.SORT_BY_APP_LABEL, false); } int mode = reverse ? -1 : 1; Collator collator = Collator.getInstance(); Collections.sort(mApplicationItems, (o1, o2) -> { switch (sortBy) { case MainListOptions.SORT_BY_APP_LABEL: return mode * collator.compare(o1.label, o2.label); case MainListOptions.SORT_BY_PACKAGE_NAME: return mode * o1.packageName.compareTo(o2.packageName); case MainListOptions.SORT_BY_DOMAIN: boolean isSystem1 = (o1.flags & ApplicationInfo.FLAG_SYSTEM) != 0; boolean isSystem2 = (o2.flags & ApplicationInfo.FLAG_SYSTEM) != 0; return mode * Boolean.compare(isSystem1, isSystem2); case MainListOptions.SORT_BY_LAST_UPDATE: // Sort in decreasing order return -mode * o1.lastUpdateTime.compareTo(o2.lastUpdateTime); case MainListOptions.SORT_BY_TOTAL_SIZE: // Sort in decreasing order return -mode * o1.totalSize.compareTo(o2.totalSize); case MainListOptions.SORT_BY_DATA_USAGE: // Sort in decreasing order return -mode * o1.dataUsage.compareTo(o2.dataUsage); case MainListOptions.SORT_BY_OPEN_COUNT: // Sort in decreasing order return -mode * Integer.compare(o1.openCount, o2.openCount); case MainListOptions.SORT_BY_INSTALLATION_DATE: // Sort in decreasing order return -mode * Long.compare(o1.firstInstallTime, o2.firstInstallTime); case MainListOptions.SORT_BY_SCREEN_TIME: // Sort in decreasing order return -mode * Long.compare(o1.screenTime, o2.screenTime); case MainListOptions.SORT_BY_LAST_USAGE_TIME: // Sort in decreasing order return -mode * Long.compare(o1.lastUsageTime, o2.lastUsageTime); case MainListOptions.SORT_BY_TARGET_SDK: // null on top if (o1.targetSdk == null) return -mode; else if (o2.targetSdk == null) return +mode; return mode * o1.targetSdk.compareTo(o2.targetSdk); case MainListOptions.SORT_BY_SHARED_ID: return mode * Integer.compare(o1.uid, o2.uid); case MainListOptions.SORT_BY_SHA: // null on top if (o1.sha == null) { return -mode; } else if (o2.sha == null) { return +mode; } else { // Both aren't null int i = o1.sha.first.compareToIgnoreCase(o2.sha.first); if (i == 0) { return mode * o1.sha.second.compareToIgnoreCase(o2.sha.second); } else return mode * i; } case MainListOptions.SORT_BY_BLOCKED_COMPONENTS: return -mode * o1.blockedCount.compareTo(o2.blockedCount); case MainListOptions.SORT_BY_FROZEN_APP: return -mode * Boolean.compare(o1.isDisabled, o2.isDisabled); case MainListOptions.SORT_BY_BACKUP: return -mode * Boolean.compare(o1.backup != null, o2.backup != null); case MainListOptions.SORT_BY_LAST_ACTION: return -mode * o1.lastActionTime.compareTo(o2.lastActionTime); case MainListOptions.SORT_BY_TRACKERS: return -mode * o1.trackerCount.compareTo(o2.trackerCount); } return 0; }); } } @WorkerThread private void updateInfoForUid(int uid, String action) { Log.d("updateInfoForUid", "Uid: %d", uid); String[] packages; if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) packages = getPackagesForUid(uid); else packages = mPackageManager.getPackagesForUid(uid); updateInfoForPackages(packages, action); } @WorkerThread private void updateInfoForPackages(@Nullable String[] packages, @NonNull String action) { Log.d("updateInfoForPackages", "packages: %s", Arrays.toString(packages)); if (packages == null || packages.length == 0) return; boolean modified = false; switch (action) { case PackageChangeReceiver.ACTION_DB_PACKAGE_REMOVED: case PackageChangeReceiver.ACTION_DB_PACKAGE_ALTERED: case PackageChangeReceiver.ACTION_DB_PACKAGE_ADDED: { AppDb appDb = new AppDb(); for (String packageName : packages) { ApplicationItem item = getNewApplicationItem(packageName, appDb.getAllApplications(packageName)); modified |= item != null ? insertOrAddApplicationItem(item) : deleteApplicationItem(packageName); } break; } case PackageChangeReceiver.ACTION_PACKAGE_REMOVED: case PackageChangeReceiver.ACTION_PACKAGE_ALTERED: case PackageChangeReceiver.ACTION_PACKAGE_ADDED: // case BatchOpsService.ACTION_BATCH_OPS_COMPLETED: case Intent.ACTION_PACKAGE_REMOVED: case Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE: case Intent.ACTION_PACKAGE_ADDED: case Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE: case Intent.ACTION_PACKAGE_CHANGED: { List appList = new AppDb().updateApplications(getApplication(), packages); for (String packageName : packages) { ApplicationItem item = getNewApplicationItem(packageName, appList); modified |= item != null ? insertOrAddApplicationItem(item) : deleteApplicationItem(packageName); } break; } default: return; } if (modified) { sortApplicationList(mSortBy, mReverseSort); filterItemsByFlags(); } } @GuardedBy("applicationItems") private boolean insertOrAddApplicationItem(@Nullable ApplicationItem item) { if (item == null) return false; synchronized (mApplicationItems) { if (insertApplicationItem(item)) { return true; } boolean inserted = mApplicationItems.add(item); if (mSelectedPackageApplicationItemMap.containsKey(item.packageName)) { select(item); } return inserted; } } @GuardedBy("applicationItems") private boolean insertApplicationItem(@NonNull ApplicationItem item) { synchronized (mApplicationItems) { boolean isInserted = false; for (int i = 0; i < mApplicationItems.size(); ++i) { if (item.equals(mApplicationItems.get(i))) { mApplicationItems.set(i, item); isInserted = true; if (mSelectedPackageApplicationItemMap.containsKey(item.packageName)) { select(item); } } } return isInserted; } } private boolean deleteApplicationItem(@NonNull String packageName) { synchronized (mApplicationItems) { ListIterator it = mApplicationItems.listIterator(); while (it.hasNext()) { ApplicationItem item = it.next(); if (item.packageName.equals(packageName)) { mSelectedPackageApplicationItemMap.remove(packageName); it.remove(); return true; } } return false; } } @WorkerThread @Nullable private ApplicationItem getNewApplicationItem(@NonNull String packageName, @NonNull List apps) { ApplicationItem item = new ApplicationItem(); int thisUser = UserHandleHidden.myUserId(); for (App app : apps) { if (!packageName.equals(app.packageName)) { // Package name didn't match continue; } if (app.isInstalled) { boolean newItem = item.packageName == null || !item.isInstalled; if (item.packageName == null) { item.packageName = app.packageName; } item.userIds = ArrayUtils.appendInt(item.userIds, app.userId); item.isInstalled = true; item.isOnlyDataInstalled = false; item.openCount += app.openCount; item.screenTime += app.screenTime; if (item.lastUsageTime == 0L || item.lastUsageTime < app.lastUsageTime) { item.lastUsageTime = app.lastUsageTime; } item.hasKeystore |= app.hasKeystore; item.usesSaf |= app.usesSaf; if (app.ssaid != null) { item.ssaid = app.ssaid; } item.totalSize += app.codeSize + app.dataSize; item.dataUsage += app.wifiDataUsage + app.mobileDataUsage; if (!newItem && app.userId != thisUser) { // This user has the highest priority continue; } } else { // App not installed but may be installed in other profiles if (item.packageName != null) { // Item exists, use the previous status continue; } else { item.packageName = app.packageName; item.isInstalled = false; item.isOnlyDataInstalled = app.isOnlyDataInstalled; item.hasKeystore |= app.hasKeystore; } } item.flags = app.flags; item.uid = app.uid; item.debuggable = app.isDebuggable(); item.isUser = !app.isSystemApp(); item.isDisabled = !app.isEnabled; item.label = app.packageLabel; item.targetSdk = app.sdk; item.versionName = app.versionName; item.versionCode = app.versionCode; item.sharedUserId = app.sharedUserId; item.sha = new Pair<>(app.certName, app.certAlgo); item.firstInstallTime = app.firstInstallTime; item.lastUpdateTime = app.lastUpdateTime; item.hasActivities = app.hasActivities; item.hasSplits = app.hasSplits; item.blockedCount = app.rulesCount; item.trackerCount = app.trackerCount; item.lastActionTime = app.lastActionTime; if (item.backup == null) { item.backup = BackupUtils.getLatestBackupMetadataFromDbNoLockValidate(packageName); } item.generateOtherInfo(); } if (item.packageName == null) { return null; } return item; } @GuardedBy("applicationItems") @NonNull private String[] getPackagesForUid(int uid) { synchronized (mApplicationItems) { List packages = new LinkedList<>(); for (ApplicationItem item : mApplicationItems) { if (item.uid == uid) packages.add(item.packageName); } return packages.toArray(new String[0]); } } @Override protected void onCleared() { if (mPackageObserver != null) getApplication().unregisterReceiver(mPackageObserver); executor.shutdownNow(); super.onCleared(); } public static class PackageIntentReceiver extends PackageChangeReceiver { private final MainViewModel mModel; public PackageIntentReceiver(@NonNull MainViewModel model) { super(model.getApplication()); mModel = model; } @Override @WorkerThread protected void onPackageChanged(Intent intent, @Nullable Integer uid, @Nullable String[] packages) { mModel.cancelIfRunning(); if (uid != null) { mModel.updateInfoForUid(uid, intent.getAction()); } else if (packages != null) { mModel.updateInfoForPackages(packages, intent.getAction()); } else { mModel.loadApplicationItems(); } } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/main/SplashActivity.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.main; import static io.github.muntashirakon.AppManager.BaseActivity.ASKED_PERMISSIONS; import android.annotation.SuppressLint; import android.app.KeyguardManager; import android.content.Intent; import android.os.Build; import android.os.Bundle; import android.view.Menu; import android.widget.TextView; import androidx.activity.EdgeToEdge; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.CallSuper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.view.menu.MenuBuilder; import androidx.biometric.BiometricPrompt; import androidx.core.content.ContextCompat; import androidx.core.splashscreen.SplashScreen; import androidx.lifecycle.ViewModelProvider; import com.google.android.material.color.DynamicColors; import java.util.ArrayList; import java.util.List; import java.util.Locale; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.compat.BiometricAuthenticatorsCompat; import io.github.muntashirakon.AppManager.crypto.ks.KeyStoreActivity; import io.github.muntashirakon.AppManager.crypto.ks.KeyStoreManager; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.self.life.BuildExpiryChecker; import io.github.muntashirakon.AppManager.settings.Ops; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.settings.SecurityAndOpsViewModel; import io.github.muntashirakon.AppManager.utils.UIUtils; @SuppressLint("CustomSplashScreen") public class SplashActivity extends AppCompatActivity { public static final String TAG = SplashActivity.class.getSimpleName(); @Nullable private TextView mStateNameView; private SecurityAndOpsViewModel mViewModel; private BiometricPrompt mBiometricPrompt; private final ActivityResultLauncher mKeyStoreActivity = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { // Need authentication and/or verify mode of operation ensureSecurityAndModeOfOp(); }); private final ActivityResultLauncher mPermissionCheckActivity = registerForActivityResult( new ActivityResultContracts.RequestMultiplePermissions(), permissionStatusMap -> { // Run authentication doAuthenticate(); }); @Override protected final void onCreate(@Nullable Bundle savedInstanceState) { setTheme(Prefs.Appearance.isPureBlackTheme() ? R.style.AppTheme_Splash_Black : R.style.AppTheme_Splash); SplashScreen.installSplashScreen(this); EdgeToEdge.enable(this); super.onCreate(savedInstanceState); DynamicColors.applyToActivityIfAvailable(this); setContentView(R.layout.activity_authentication); ((TextView) findViewById(R.id.version)).setText(String.format(Locale.ROOT, "%s (%d)", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)); mStateNameView = findViewById(R.id.state_name); if (Ops.isAuthenticated()) { Log.d(TAG, "Already authenticated."); startActivity(new Intent(this, MainActivity.class)); finish(); return; } if (Boolean.TRUE.equals(BuildExpiryChecker.buildExpired())) { // Build has expired BuildExpiryChecker.getBuildExpiredDialog(this, (dialog, which) -> doAuthenticate()).show(); return; } // Init permission checks if (!initPermissionChecks()) { // Run authentication doAuthenticate(); } } @CallSuper @SuppressLint("RestrictedApi") @Override public boolean onCreateOptionsMenu(Menu menu) { if (menu instanceof MenuBuilder) { ((MenuBuilder) menu).setOptionalIconsVisible(true); } return super.onCreateOptionsMenu(menu); } private void doAuthenticate() { mViewModel = new ViewModelProvider(this).get(SecurityAndOpsViewModel.class); mBiometricPrompt = new BiometricPrompt(this, ContextCompat.getMainExecutor(this), new BiometricPrompt.AuthenticationCallback() { @Override public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) { super.onAuthenticationError(errorCode, errString); finishAndRemoveTask(); } @Override public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) { super.onAuthenticationSucceeded(result); handleMigrationAndModeOfOp(); } @Override public void onAuthenticationFailed() { super.onAuthenticationFailed(); } }); Log.d(TAG, "Waiting to be authenticated."); mViewModel.authenticationStatus().observe(this, status -> { switch (status) { case Ops.STATUS_AUTO_CONNECT_WIRELESS_DEBUGGING: Log.d(TAG, "Try auto-connecting to wireless debugging."); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { mViewModel.autoConnectWirelessDebugging(); return; } // fall-through case Ops.STATUS_WIRELESS_DEBUGGING_CHOOSER_REQUIRED: Log.d(TAG, "Display wireless debugging chooser (pair or connect)"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { Ops.connectWirelessDebugging(this, mViewModel); return; } // fall-through case Ops.STATUS_ADB_CONNECT_REQUIRED: Log.d(TAG, "Display connect dialog."); Ops.connectAdbInput(this, mViewModel); return; case Ops.STATUS_ADB_PAIRING_REQUIRED: Log.d(TAG, "Display pairing dialog."); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { Ops.pairAdbInput(this, mViewModel); return; } // fall-through case Ops.STATUS_FAILURE_ADB_NEED_MORE_PERMS: Ops.displayIncompleteUsbDebuggingMessage(this); case Ops.STATUS_SUCCESS: case Ops.STATUS_FAILURE: Log.d(TAG, "Authentication completed."); mViewModel.setAuthenticating(false); Ops.setAuthenticated(this, true); startActivity(new Intent(this, MainActivity.class)); finish(); } }); if (!mViewModel.isAuthenticating()) { mViewModel.setAuthenticating(true); // Check KeyStore if (KeyStoreManager.hasKeyStorePassword()) { // We already have a working keystore password. // Only need authentication and/or verify mode of operation. ensureSecurityAndModeOfOp(); return; } Intent keyStoreIntent = new Intent(this, KeyStoreActivity.class) .putExtra(KeyStoreActivity.EXTRA_KS, true); mKeyStoreActivity.launch(keyStoreIntent); } } private void ensureSecurityAndModeOfOp() { if (!Prefs.Privacy.isScreenLockEnabled()) { // No security enabled handleMigrationAndModeOfOp(); return; } Log.d(TAG, "Security enabled."); KeyguardManager keyguardManager = (KeyguardManager) getSystemService(KEYGUARD_SERVICE); if (keyguardManager.isKeyguardSecure()) { // Screen lock enabled BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder() .setTitle(getString(R.string.unlock_app_manager)) .setAllowedAuthenticators(new BiometricAuthenticatorsCompat.Builder().allowEverything(true).build()) .build(); mBiometricPrompt.authenticate(promptInfo); } else { // Screen lock disabled UIUtils.displayLongToast(R.string.screen_lock_not_enabled); finishAndRemoveTask(); } } private void handleMigrationAndModeOfOp() { // Authentication was successful Log.d(TAG, "Authenticated"); if (mStateNameView != null) { mStateNameView.setText(R.string.initializing); } // Set mode of operation if (mViewModel != null) { mViewModel.setModeOfOps(); } } private boolean initPermissionChecks() { List permissionsToBeAsked = new ArrayList<>(ASKED_PERMISSIONS.size()); for (String permission : ASKED_PERMISSIONS.keySet()) { if (!SelfPermissions.checkSelfPermission(permission)) { permissionsToBeAsked.add(permission); } } if (!permissionsToBeAsked.isEmpty()) { // Ask required permissions mPermissionCheckActivity.launch(permissionsToBeAsked.toArray(new String[0])); return true; } return false; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/misc/AMExceptionHandler.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.misc; import android.app.Notification; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.os.Build; import androidx.annotation.NonNull; import androidx.core.app.NotificationCompat; import androidx.core.app.PendingIntentCompat; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.utils.NotificationUtils; public class AMExceptionHandler implements Thread.UncaughtExceptionHandler { private static final String E = "am4android@riseup.net"; private final Thread.UncaughtExceptionHandler mDefaultExceptionHandler; private final Context mContext; public AMExceptionHandler(Context context) { mDefaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler(); mContext = context; } public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) { // Collect info StackTraceElement[] arr = e.getStackTrace(); StringBuilder report = new StringBuilder(e + "\n"); for (StackTraceElement traceElement : arr) { report.append(" at ").append(traceElement.toString()).append("\n"); } Throwable cause = e; while((cause = cause.getCause()) != null) { report.append(" Caused by: ").append(cause).append("\n"); arr = cause.getStackTrace(); for (StackTraceElement stackTraceElement : arr) { report.append(" at ").append(stackTraceElement.toString()).append("\n"); } } report.append("\nDevice Info:\n"); report.append(new DeviceInfo(mContext)); // Send notification Intent i = new Intent(Intent.ACTION_SEND); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { i.setIdentifier(String.valueOf(System.currentTimeMillis())); } i.setType("text/plain"); i.putExtra(Intent.EXTRA_EMAIL, new String[]{E}); i.putExtra(Intent.EXTRA_SUBJECT, "App Manager: Crash report"); String body = report.toString(); i.putExtra(Intent.EXTRA_TEXT, body); PendingIntent pendingIntent = PendingIntentCompat.getActivity(mContext, 0, Intent.createChooser(i, mContext.getText(R.string.send_crash_report)), PendingIntent.FLAG_ONE_SHOT, true); NotificationCompat.Builder builder = NotificationUtils.getHighPriorityNotificationBuilder(mContext) .setAutoCancel(true) .setDefaults(Notification.DEFAULT_ALL) .setWhen(System.currentTimeMillis()) .setSmallIcon(R.drawable.ic_launcher_foreground) .setTicker(mContext.getText(R.string.app_name)) .setContentTitle(mContext.getText(R.string.am_crashed)) .setContentText(mContext.getText(R.string.tap_to_submit_crash_report)) .setContentIntent(pendingIntent); NotificationUtils.displayHighPriorityNotification(mContext, builder.build()); // Manage the rests via the default handler mDefaultExceptionHandler.uncaughtException(t, e); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/misc/AdvancedSearchView.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.misc; import static com.google.android.material.theme.overlay.MaterialThemeOverlay.wrap; import android.annotation.SuppressLint; import android.app.SearchableInfo; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Parcel; import android.os.Parcelable; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.style.ImageSpan; import android.util.AttributeSet; import android.view.Menu; import android.view.View; import android.widget.ImageView; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.PopupMenu; import androidx.customview.view.AbsSavedState; import com.google.android.material.internal.ThemeEnforcement; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.util.UiUtils; import io.github.muntashirakon.widget.SearchView; public class AdvancedSearchView extends SearchView { @IntDef(flag = true, value = { SEARCH_TYPE_CONTAINS, SEARCH_TYPE_PREFIX, SEARCH_TYPE_SUFFIX, SEARCH_TYPE_REGEX}) @Retention(RetentionPolicy.SOURCE) public @interface SearchType { } /** * Search using {@link String#contains(CharSequence)}. */ public static final int SEARCH_TYPE_CONTAINS = 1; /** * Search using {@link String#startsWith(String)}. */ public static final int SEARCH_TYPE_PREFIX = 1 << 1; /** * Search using {@link String#endsWith(String)}. */ public static final int SEARCH_TYPE_SUFFIX = 1 << 2; /** * Search using {@link String#matches(String)} or {@link java.util.regex.Pattern}. */ public static final int SEARCH_TYPE_REGEX = 1 << 3; private static final int DEF_STYLE_RES = io.github.muntashirakon.ui.R.style.Widget_AppTheme_SearchView; @SearchType private int mType = SEARCH_TYPE_CONTAINS; @SearchType private int mEnabledTypes = SEARCH_TYPE_CONTAINS | SEARCH_TYPE_PREFIX | SEARCH_TYPE_SUFFIX | SEARCH_TYPE_REGEX; private CharSequence mQueryHint; private final ImageView mSearchTypeSelectionButton; private final SearchAutoComplete mSearchSrcTextView; private final Drawable mSearchHintIcon; @Nullable private OnQueryTextListener mOnQueryTextListener; @Nullable private OnClickListener mOnSearchIconClickListener; private final OnClickListener mOnSearchIconClickListenerSuper; @Nullable private OnFocusChangeListener mOnQueryTextFocusChangeListener; private final OnFocusChangeListener mOnQueryTextFocusChangeListenerSuper; private final SearchView.OnQueryTextListener mOnQueryTextListenerSuper = new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { if (mOnQueryTextListener != null) { return mOnQueryTextListener.onQueryTextSubmit(query, mType); } return false; } @Override public boolean onQueryTextChange(String newText) { if (mOnQueryTextListener != null) { return mOnQueryTextListener.onQueryTextChange(newText, mType); } return false; } }; private final View.OnClickListener onClickSearchIcon = v -> { PopupMenu popupMenu = new PopupMenu(getContext(), v); popupMenu.inflate(R.menu.view_advanced_search_type_selections); popupMenu.setOnMenuItemClickListener(item -> { int id = item.getItemId(); if (id == R.id.action_search_type_contains) { mType = SEARCH_TYPE_CONTAINS; } else if (id == R.id.action_search_type_prefix) { mType = SEARCH_TYPE_PREFIX; } else if (id == R.id.action_search_type_suffix) { mType = SEARCH_TYPE_SUFFIX; } else if (id == R.id.action_search_type_regex) { mType = SEARCH_TYPE_REGEX; } if (mOnQueryTextListener != null) { mOnQueryTextListener.onQueryTextChange(getQuery().toString(), mType); } updateQueryHint(); return true; }); Menu menu = popupMenu.getMenu(); if ((mEnabledTypes & SEARCH_TYPE_CONTAINS) == 0) { menu.findItem(R.id.action_search_type_contains).setVisible(false); } if ((mEnabledTypes & SEARCH_TYPE_PREFIX) == 0) { menu.findItem(R.id.action_search_type_prefix).setVisible(false); } if ((mEnabledTypes & SEARCH_TYPE_SUFFIX) == 0) { menu.findItem(R.id.action_search_type_suffix).setVisible(false); } if ((mEnabledTypes & SEARCH_TYPE_REGEX) == 0) { menu.findItem(R.id.action_search_type_regex).setVisible(false); } popupMenu.show(); }; public AdvancedSearchView(@NonNull Context context) { this(context, null); } public AdvancedSearchView(@NonNull Context context, @Nullable AttributeSet attrs) { this(context, attrs, androidx.appcompat.R.attr.searchViewStyle); } @SuppressLint("RestrictedApi") public AdvancedSearchView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(wrap(context, attrs, defStyleAttr, DEF_STYLE_RES), attrs, defStyleAttr); context = getContext(); mSearchSrcTextView = findViewById(androidx.appcompat.R.id.search_src_text); mSearchTypeSelectionButton = findViewById(androidx.appcompat.R.id.search_mag_icon); mSearchTypeSelectionButton.setImageResource(R.drawable.ic_filter_menu); mSearchTypeSelectionButton.setBackground(UiUtils.getDrawable(context, android.R.attr.selectableItemBackgroundBorderless)); mSearchTypeSelectionButton.setOnClickListener(onClickSearchIcon); final TypedArray a = ThemeEnforcement.obtainStyledAttributes( context, attrs, io.github.muntashirakon.ui.R.styleable.SearchView, defStyleAttr, DEF_STYLE_RES); mQueryHint = a.getText(io.github.muntashirakon.ui.R.styleable.SearchView_queryHint); mSearchHintIcon = a.getDrawable(io.github.muntashirakon.ui.R.styleable.SearchView_searchHintIcon); a.recycle(); setIconified(isIconified()); updateQueryHint(); mOnQueryTextFocusChangeListenerSuper = (v, hasFocus) -> { v.postDelayed(() -> { // This has to be like this because the {@link SearchAutoComplete#onFocusChanged(boolean, int, Rect)} // has an issue. // FIXME: 29/11/21 Override SearchAutoComplete and create a new search layout from the original to // include the overridden class if (!isIconified()) { mSearchTypeSelectionButton.setVisibility(VISIBLE); } }, 1); if (mOnQueryTextFocusChangeListener != null) { mOnQueryTextFocusChangeListener.onFocusChange(v, hasFocus); } }; mOnSearchIconClickListenerSuper = v -> { mSearchTypeSelectionButton.setVisibility(VISIBLE); if (mOnSearchIconClickListener != null) { mOnSearchIconClickListener.onClick(v); } }; mSearchSrcTextView.setOnFocusChangeListener(mOnQueryTextFocusChangeListenerSuper); super.setOnSearchClickListener(mOnSearchIconClickListenerSuper); } protected static class SavedState extends AbsSavedState { int type; int enabledTypes; SavedState(@NonNull Parcelable superState) { super(superState); } public SavedState(@NonNull Parcel source, @Nullable ClassLoader loader) { super(source, loader); type = source.readInt(); enabledTypes = source.readInt(); } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeInt(type); dest.writeInt(enabledTypes); } @NonNull @Override public String toString() { return "AdvancedSearchView.SavedState{" + Integer.toHexString(System.identityHashCode(this)) + " type=" + type + " enabledTypes=" + enabledTypes + "}"; } public static final Creator CREATOR = new ClassLoaderCreator() { @Override public SavedState createFromParcel(Parcel in, ClassLoader loader) { return new SavedState(in, loader); } @Override public SavedState createFromParcel(Parcel in) { return new SavedState(in, null); } @Override public SavedState[] newArray(int size) { return new SavedState[size]; } }; } @Override protected Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); SavedState ss = new SavedState(superState); ss.type = mType; ss.enabledTypes = mEnabledTypes; return ss; } @Override protected void onRestoreInstanceState(Parcelable state) { if (state instanceof SavedState) { SavedState ss = (SavedState) state; super.onRestoreInstanceState(ss.getSuperState()); mType = ss.type; mEnabledTypes = ss.enabledTypes; } else super.onRestoreInstanceState(state); if (!isIconified()) mSearchTypeSelectionButton.setVisibility(VISIBLE); updateQueryHint(); requestLayout(); } @Override public boolean requestFocus(int direction, Rect previouslyFocusedRect) { boolean result = super.requestFocus(direction, previouslyFocusedRect); if (result && !isIconified()) mSearchTypeSelectionButton.setVisibility(VISIBLE); return result; } @Override public void setOnQueryTextFocusChangeListener(OnFocusChangeListener listener) { mOnQueryTextFocusChangeListener = listener; } public void setOnQueryTextListener(@Nullable OnQueryTextListener listener) { if (listener == null) return; mOnQueryTextListener = listener; super.setOnQueryTextListener(mOnQueryTextListenerSuper); } /** * @deprecated This method is ignored. Use {@link #setOnQueryTextListener(OnQueryTextListener)} instead. */ @Override public final void setOnQueryTextListener(SearchView.OnQueryTextListener listener) { throw new UnsupportedOperationException("Wrong function. Use the other function by the same name."); } @Override public void setOnSearchClickListener(OnClickListener listener) { mOnSearchIconClickListener = listener; } @Override public void setIconifiedByDefault(boolean iconified) { super.setIconifiedByDefault(iconified); updateQueryHint(); } @Override public void setSearchableInfo(SearchableInfo searchable) { super.setSearchableInfo(searchable); if (!isIconified()) mSearchTypeSelectionButton.setVisibility(VISIBLE); } @Override public void setSubmitButtonEnabled(boolean enabled) { super.setSubmitButtonEnabled(enabled); if (!isIconified()) mSearchTypeSelectionButton.setVisibility(VISIBLE); } @Override public void setQueryHint(@Nullable CharSequence hint) { super.setQueryHint(hint); mQueryHint = hint; } public void setEnabledTypes(@SearchType int enabledTypes) { this.mEnabledTypes = enabledTypes; if (this.mEnabledTypes == 0) { mEnabledTypes = SEARCH_TYPE_CONTAINS; } } public void addEnabledTypes(@SearchType int enabledTypes) { this.mEnabledTypes |= enabledTypes; } public void removeEnabledTypes(@SearchType int enabledTypes) { this.mEnabledTypes &= ~enabledTypes; if (this.mEnabledTypes == 0) { mEnabledTypes = SEARCH_TYPE_CONTAINS; } } public static boolean matches(@NonNull String query, @NonNull String text, @SearchType int type) { switch (type) { case SEARCH_TYPE_CONTAINS: return text.contains(query); case SEARCH_TYPE_PREFIX: return text.startsWith(query); case SEARCH_TYPE_SUFFIX: return text.endsWith(query); case SEARCH_TYPE_REGEX: return text.matches(query); } return false; } public interface ChoiceGenerator { @Nullable String getChoice(T object); } public interface ChoicesGenerator { List getChoices(T object); } public static List matches(@NonNull String query, @Nullable Collection choices, @NonNull ChoiceGenerator generator, @SearchType int type) { if (choices == null) return null; if (choices.size() == 0) return Collections.emptyList(); List results = new ArrayList<>(choices.size()); if (type == SEARCH_TYPE_REGEX) { Pattern p; try { p = Pattern.compile(query); for (T choice : choices) { String text = generator.getChoice(choice); if (text == null) continue; if (p.matcher(text).find()) { results.add(choice); } } } catch (PatternSyntaxException ignore) { } return results; } // Rests are typical for (T choice : choices) { String text = generator.getChoice(choice); if (text == null) continue; if (matches(query, text, type)) { results.add(choice); } } return results; } public static List matches(@NonNull String query, @Nullable Collection choices, @NonNull ChoicesGenerator generator, @SearchType int type) { if (choices == null) return null; if (choices.isEmpty()) return Collections.emptyList(); List results = new ArrayList<>(choices.size()); if (type == SEARCH_TYPE_REGEX) { Pattern p; try { p = Pattern.compile(query); for (T choice : choices) { if (ThreadUtils.isInterrupted()) { return Collections.emptyList(); } List texts = generator.getChoices(choice); for (String text : texts) { if (text == null) continue; if (p.matcher(text).find()) { results.add(choice); break; // Only a single match is enough } } } } catch (PatternSyntaxException ignore) { } return results; } // Rests are typical for (T choice : choices) { if (ThreadUtils.isInterrupted()) { return Collections.emptyList(); } List texts = generator.getChoices(choice); for (String text : texts) { if (text == null) continue; if (matches(query, text, type)) { results.add(choice); break; // Only a single match is enough } } } return results; } private void updateQueryHint() { CharSequence hintText = mQueryHint + " (" + getQueryHint(mType) + ")"; if (!isIconfiedByDefault() && mSearchHintIcon != null) { // Search icon isn't displayed when it is iconified by default. final int textSize = (int) (mSearchSrcTextView.getTextSize() * 1.25); mSearchHintIcon.setBounds(0, 0, textSize, textSize); final SpannableStringBuilder ssb = new SpannableStringBuilder(" "); ssb.setSpan(new ImageSpan(mSearchHintIcon), 1, 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); ssb.append(hintText); super.setQueryHint(ssb); return; } super.setQueryHint(hintText); } @NonNull private CharSequence getQueryHint(@SearchType int type) { switch (type) { default: case SEARCH_TYPE_CONTAINS: return getContext().getString(R.string.search_type_contains); case SEARCH_TYPE_PREFIX: return getContext().getString(R.string.search_type_prefix); case SEARCH_TYPE_REGEX: return getContext().getString(R.string.search_type_regular_expressions); case SEARCH_TYPE_SUFFIX: return getContext().getString(R.string.search_type_suffix); } } public interface OnQueryTextListener { boolean onQueryTextChange(String newText, @SearchType int type); boolean onQueryTextSubmit(String query, @SearchType int type); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/misc/DeviceInfo.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.misc; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Build; import androidx.annotation.IntRange; import androidx.annotation.NonNull; import androidx.core.content.pm.PackageInfoCompat; import java.util.Arrays; import java.util.Locale; import io.github.muntashirakon.AppManager.settings.Ops; import io.github.muntashirakon.AppManager.settings.Prefs; public class DeviceInfo { public final String[] abis = Build.SUPPORTED_ABIS; public final String[] abis32Bits = Build.SUPPORTED_32_BIT_ABIS; public final String[] abis64Bits = Build.SUPPORTED_64_BIT_ABIS; public final String brand = Build.BRAND; public final String buildID = Build.DISPLAY; public final String buildVersion = Build.VERSION.INCREMENTAL; public final String device = Build.DEVICE; public final String hardware = Build.HARDWARE; public final String manufacturer = Build.MANUFACTURER; public final String model = Build.MODEL; public final String product = Build.PRODUCT; public final String releaseVersion = Build.VERSION.RELEASE; @IntRange(from = 0) public final int sdkVersion = Build.VERSION.SDK_INT; public final long versionCode; public final String versionName; public final CharSequence inferredMode; public DeviceInfo(@NonNull Context context) { PackageInfo packageInfo; try { packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); } catch (PackageManager.NameNotFoundException e) { packageInfo = null; } if (packageInfo != null) { versionCode = PackageInfoCompat.getLongVersionCode(packageInfo); versionName = packageInfo.versionName; } else { versionCode = -1; versionName = null; } inferredMode = Ops.getInferredMode(context); } @NonNull @Override public String toString() { return "App version: " + versionName + "\n" + "App version code: " + versionCode + "\n" + "Android build version: " + buildVersion + "\n" + "Android release version: " + releaseVersion + "\n" + "Android SDK version: " + sdkVersion + "\n" + "Android build ID: " + buildID + "\n" + "Device brand: " + brand + "\n" + "Device manufacturer: " + manufacturer + "\n" + "Device name: " + device + "\n" + "Device model: " + model + "\n" + "Device product name: " + product + "\n" + "Device hardware name: " + hardware + "\n" + "ABIs: " + Arrays.toString(abis) + "\n" + "ABIs (32bit): " + Arrays.toString(abis32Bits) + "\n" + "ABIs (64bit): " + Arrays.toString(abis64Bits) + "\n" + "System language: " + Locale.getDefault().toLanguageTag() + "\n" + "In-App Language: " + Prefs.Appearance.getLanguage() + "\n" + "Mode: " + Ops.getMode() + "\n" + "Inferred Mode: " + inferredMode; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/misc/DeviceInfo2.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.misc; import static io.github.muntashirakon.AppManager.utils.UIUtils.getStyledKeyValue; import static io.github.muntashirakon.AppManager.utils.UIUtils.getTitleText; import android.app.ActivityManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.pm.FeatureInfo; import android.content.pm.PackageManager; import android.opengl.GLES20; import android.os.BatteryManager; import android.os.Build; import android.os.Bundle; import android.os.SELinux; import android.os.UserHandleHidden; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.text.format.DateFormat; import android.text.format.Formatter; import android.util.DisplayMetrics; import android.view.Display; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.StringRes; import androidx.annotation.WorkerThread; import androidx.collection.ArrayMap; import androidx.core.os.LocaleListCompat; import androidx.core.util.Pair; import androidx.fragment.app.FragmentActivity; import com.android.internal.os.PowerProfile; import java.security.Provider; import java.security.Security; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.StaticDataset; import io.github.muntashirakon.AppManager.compat.PackageManagerCompat; import io.github.muntashirakon.AppManager.misc.gles.EglCore; import io.github.muntashirakon.AppManager.misc.gles.OffscreenSurface; import io.github.muntashirakon.AppManager.runner.Runner; import io.github.muntashirakon.AppManager.runner.RunnerUtils; import io.github.muntashirakon.AppManager.users.UserInfo; import io.github.muntashirakon.AppManager.users.Users; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.CpuUtils; import io.github.muntashirakon.AppManager.utils.TextUtilsCompat; import io.github.muntashirakon.AppManager.utils.Utils; import io.github.muntashirakon.proc.ProcFs; import io.github.muntashirakon.util.LocalizedString; public class DeviceInfo2 implements LocalizedString { public final String osVersion = Build.VERSION.RELEASE; public final String bootloader = Build.BOOTLOADER; public final String vm = getVmVersion(); public final String kernel = getKernel(); public final String brandName = Build.BRAND; public final String model = Build.MODEL; public final String board = Build.BOARD; public final String manufacturer = Build.MANUFACTURER; // SDK public final int maxSdk = Build.VERSION.SDK_INT; public final int minSdk = SystemProperties.getInt("ro.build.version.min_supported_target_sdk", 0); // Security public boolean hasRoot; public int selinux; public String encryptionStatus; public String dmVerity; // enforcing, disabled, eio, ... @Nullable public String verifiedBootState; // green (verified), yellow (self-signed), orange (unverified), red (failed) ?: orange (unverified) public String verifiedBootStateString; public String avbVersion; public String bootloaderState; public boolean debuggable; public final String patchLevel; public final Provider[] securityProviders = Security.getProviders(); public final String hardwareBackedFeatures; public final String strongBoxBackedFeatures; // CPU Info @Nullable public String cpuHardware; public final String[] supportedAbis = Build.SUPPORTED_ABIS; public int availableProcessors; public String openGlEsVersion; @Nullable public String vulkanVersion; // Memory public final ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo(); // Battery public boolean batteryPresent; public double batteryCapacityMAh; public double batteryCapacityMAhAlt; @Nullable public String batteryTechnology; public int batteryCycleCount; public String batteryHealth; // Display public final int displayDensityDpi = StaticDataset.DEVICE_DENSITY; public final String displayDensity = getDensity(); public float scalingFactor; public int actualWidthPx; public int actualHeightPx; public int windowWidthPx; public int windowHeightPx; public float refreshRate; // Locales public final LocaleListCompat systemLocales = LocaleListCompat.getDefault(); // Users @Nullable public List users; // Packages public ArrayMap> userPackages = new ArrayMap<>(1); // Features public FeatureInfo[] features; private final FragmentActivity mActivity; private final ActivityManager mActivityManager; private final PackageManager mPm; private final Display mDisplay; public DeviceInfo2(@NonNull FragmentActivity activity) { this.mActivity = activity; mActivityManager = (ActivityManager) activity.getSystemService(Context.ACTIVITY_SERVICE); mPm = activity.getPackageManager(); mDisplay = getDisplay(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { patchLevel = getSecurityPatch(); } else patchLevel = null; features = mPm.getSystemAvailableFeatures(); hardwareBackedFeatures = getHardwareBackedFeatures(); strongBoxBackedFeatures = getStrongBoxBackedFeatures(); } @WorkerThread public void loadInfo() { hasRoot = RunnerUtils.isRootAvailable(); selinux = getSelinuxStatus(); encryptionStatus = getEncryptionStatus(); if (mPm.hasSystemFeature(PackageManager.FEATURE_VERIFIED_BOOT)) { verifiedBootState = SystemProperties.get("ro.boot.verifiedbootstate", ""); verifiedBootStateString = getVerifiedBootStateString(verifiedBootState); dmVerity = SystemProperties.get("ro.boot.veritymode", ""); avbVersion = SystemProperties.get("ro.boot.avb_version", ""); bootloaderState = SystemProperties.get("ro.boot.vbmeta.device_state", ""); } else verifiedBootState = null; debuggable = "1".equals(SystemProperties.get("ro.debuggable", "0")); cpuHardware = getCpuHardware(); availableProcessors = Runtime.getRuntime().availableProcessors(); openGlEsVersion = Utils.getGlEsVersion(mActivityManager.getDeviceConfigurationInfo().reqGlEsVersion); vulkanVersion = Utils.getVulkanVersion(mPm); mActivityManager.getMemoryInfo(memoryInfo); getBatteryStats(mActivity); DisplayMetrics displayMetrics = new DisplayMetrics(); // Actual size mDisplay.getRealMetrics(displayMetrics); scalingFactor = displayMetrics.density; actualWidthPx = displayMetrics.widthPixels; actualHeightPx = displayMetrics.heightPixels; // Window size mDisplay.getMetrics(displayMetrics); windowWidthPx = displayMetrics.widthPixels; windowHeightPx = displayMetrics.heightPixels; refreshRate = mDisplay.getRefreshRate(); users = Users.getAllUsers(); for (UserInfo info : users) { userPackages.put(info.id, getPackageStats(info.id)); } } @Override @NonNull public CharSequence toLocalizedString(@NonNull Context ctx) { SpannableStringBuilder builder = new SpannableStringBuilder(); // Android platform info builder.append(getStyledKeyValue(ctx, R.string.os_version, osVersion)).append(", ") .append(getStyledKeyValue(ctx, "Build", Build.DISPLAY)).append("\n") .append(getStyledKeyValue(ctx, R.string.bootloader, bootloader)).append(", ") .append(getStyledKeyValue(ctx, "VM", vm)).append("\n") .append(getStyledKeyValue(ctx, R.string.kernel, kernel)).append("\n") .append(getStyledKeyValue(ctx, R.string.brand_name, brandName)).append(", ") .append(getStyledKeyValue(ctx, R.string.model, model)).append("\n") .append(getStyledKeyValue(ctx, R.string.board_name, board)).append(", ") .append(getStyledKeyValue(ctx, R.string.manufacturer, manufacturer)).append("\n"); // SDK builder.append("\n").append(getTitleText(ctx, R.string.sdk)).append("\n") .append(getStyledKeyValue(ctx, R.string.sdk_max, String.format(Locale.getDefault(), "%d", maxSdk))); if (minSdk != 0) { builder.append(", ").append(getStyledKeyValue(ctx, R.string.sdk_min, String.format(Locale.getDefault(), "%d", minSdk))); } builder.append("\n"); // Security builder.append("\n").append(getTitleText(ctx, R.string.security)).append("\n"); if (patchLevel != null) { builder.append(getStyledKeyValue(ctx, R.string.patch_level, patchLevel)).append("\n"); } builder.append(getStyledKeyValue(ctx, R.string.root, String.valueOf(hasRoot))).append(", ") .append(getStyledKeyValue(ctx, R.string.debuggable, String.valueOf(debuggable))) .append("\n"); if (selinux != 2) { builder.append(getStyledKeyValue(ctx, R.string.selinux, getString(selinux == 1 ? R.string.enforcing : R.string.permissive))).append(", "); } builder.append(getStyledKeyValue(ctx, R.string.encryption, encryptionStatus)).append("\n"); boolean verifiedBoot = false; if (!TextUtils.isEmpty(verifiedBootState)) { verifiedBoot = true; builder.append(getStyledKeyValue(ctx, R.string.verified_boot, verifiedBootState)) .append(" (").append(verifiedBootStateString).append(")"); } if (!TextUtils.isEmpty(avbVersion)) { if (verifiedBoot) { builder.append(", "); } builder.append(getStyledKeyValue(ctx, R.string.android_verified_bootloader_version, avbVersion)).append("\n"); } else if (verifiedBoot) { builder.append("\n"); } boolean isDmVerity = false; if (!TextUtils.isEmpty(dmVerity)) { isDmVerity = true; builder.append(getStyledKeyValue(ctx, "dm-verity", dmVerity)); } if (!TextUtils.isEmpty(bootloaderState)) { if (isDmVerity) { builder.append(", "); } builder.append(getStyledKeyValue(ctx, R.string.bootloader, bootloaderState)).append("\n"); } else if (isDmVerity) { builder.append("\n"); } List securityProviders = new ArrayList<>(); boolean hasAndroidKeyStore = false; for (Provider provider : this.securityProviders) { if ("AndroidKeyStore".equals(provider.getName())) { hasAndroidKeyStore = true; } securityProviders.add(provider.getName() + " (v" + provider.getVersion() + ")"); } builder.append(getStyledKeyValue(ctx, R.string.security_providers, TextUtilsCompat.joinSpannable(", ", securityProviders))).append(".\n"); // Android KeyStore if (hasAndroidKeyStore) { builder.append("\n").append(getTitleText(ctx, "Android KeyStore")).append("\n"); } StringBuilder sb = new StringBuilder("Software"); if (hardwareBackedFeatures != null) { sb.append(", Hardware"); } if (strongBoxBackedFeatures != null) { sb.append(", StrongBox"); } builder.append(getStyledKeyValue(ctx, R.string.features, sb)).append("\n"); if (hardwareBackedFeatures != null) { builder.append(" ").append(getStyledKeyValue(ctx, "Hardware", hardwareBackedFeatures)).append("\n"); } if (strongBoxBackedFeatures != null) { builder.append(" ").append(getStyledKeyValue(ctx, "StrongBox", strongBoxBackedFeatures)).append("\n"); } // CPU info builder.append("\n").append(getTitleText(ctx, R.string.cpu)).append("\n"); if (cpuHardware != null) { builder.append(getStyledKeyValue(ctx, R.string.hardware, cpuHardware)).append("\n"); } builder.append(getStyledKeyValue(ctx, R.string.support_architectures, TextUtils.join(", ", supportedAbis))).append("\n") .append(getStyledKeyValue(ctx, R.string.no_of_cores, String.format(Locale.getDefault(), "%d", availableProcessors))).append("\n"); // GPU info builder.append("\n").append(getTitleText(ctx, R.string.graphics)).append("\n") .append(getGlInfo(ctx)) .append(getStyledKeyValue(ctx, R.string.gles_version, openGlEsVersion)).append("\n"); if (vulkanVersion != null) { builder.append(getStyledKeyValue(ctx, R.string.vulkan_version, vulkanVersion)).append("\n"); } // RAM info builder.append("\n").append(getTitleText(ctx, R.string.memory)).append("\n") .append(Formatter.formatFileSize(ctx, memoryInfo.totalMem)).append("\n"); // Battery info if (batteryPresent || batteryCapacityMAh > 0) { builder.append("\n").append(getTitleText(ctx, R.string.battery)).append("\n"); if (batteryTechnology != null) { builder.append(getStyledKeyValue(ctx, R.string.battery_technology, batteryTechnology)) .append("\n"); } if (batteryCapacityMAh > 0) { builder.append(getStyledKeyValue(ctx, R.string.battery_capacity, String.valueOf(batteryCapacityMAh))) .append(" mAh"); if (batteryCapacityMAhAlt > 0) { builder.append(" (est. ") .append(String.format(Locale.ROOT, "%.1f", batteryCapacityMAhAlt)) .append(" mAh)"); } builder.append("\n"); } else if (batteryCapacityMAhAlt > 0) { builder.append(getStyledKeyValue(ctx, R.string.battery_capacity, String.format(Locale.ROOT, "%.1f", batteryCapacityMAhAlt))) .append(" mAh (est.)").append("\n"); } if (batteryHealth != null) { builder.append(getStyledKeyValue(ctx, R.string.battery_health, batteryHealth)); if (batteryCycleCount > 0) { builder.append(" (").append(String.valueOf(batteryCycleCount)).append(" cycles)"); } builder.append("\n"); } } // Screen resolution builder.append("\n").append(getTitleText(ctx, R.string.screen)).append("\n") .append(getStyledKeyValue(ctx, R.string.density, String.format(Locale.getDefault(), "%s (%d DPI)", displayDensity, displayDensityDpi))).append("\n"); // Actual size builder.append(getStyledKeyValue(ctx, R.string.scaling_factor, String.valueOf(scalingFactor))).append("\n") .append(getStyledKeyValue(ctx, R.string.size, actualWidthPx + "px × " + actualHeightPx + "px\n")); // Window size builder.append(getStyledKeyValue(ctx, R.string.window_size, windowWidthPx + "px × " + windowHeightPx + "px\n")); // Refresh rate builder.append(getStyledKeyValue(ctx, R.string.refresh_rate, String.format(Locale.getDefault(), "%.1f Hz", refreshRate))).append("\n"); // List system locales List localeStrings = new ArrayList<>(systemLocales.size()); for (int i = 0; i < systemLocales.size(); ++i) { localeStrings.add(Objects.requireNonNull(systemLocales.get(i)).getDisplayName()); } builder.append("\n").append(getTitleText(ctx, R.string.languages)) .append("\n").append(TextUtilsCompat.joinSpannable(", ", localeStrings)) .append("\n"); if (users != null) { // Users builder.append("\n").append(getTitleText(ctx, R.string.users)) .append("\n"); List userNames = new ArrayList<>(); for (UserInfo user : users) { userNames.add(user.name != null ? user.name : String.valueOf(user.id)); } builder.append(String.format(Locale.getDefault(), "%d", users.size())).append(" (") .append(TextUtilsCompat.joinSpannable(", ", userNames)) .append(")\n"); // App stats per user builder.append("\n").append(getTitleText(ctx, R.string.apps)).append("\n"); for (UserInfo user : users) { Pair packageSizes = userPackages.get(user.id); if (packageSizes == null) continue; if (packageSizes.first + packageSizes.second == 0) continue; builder.append(getStyledKeyValue(ctx, R.string.user, user.toLocalizedString(ctx))).append("\n ") .append(getStyledKeyValue(ctx, R.string.total_size, String.format(Locale.getDefault(), "%d", packageSizes.first + packageSizes.second))).append(", ") .append(getStyledKeyValue(ctx, R.string.user, String.format(Locale.getDefault(), "%d", packageSizes.first))).append(", ") .append(getStyledKeyValue(ctx, R.string.system, String.format(Locale.getDefault(), "%d", packageSizes.second))) .append("\n"); } } else { builder.append("\n").append(getTitleText(ctx, R.string.apps)).append("\n"); Pair packageSizes = userPackages.get(UserHandleHidden.myUserId()); if (packageSizes != null) { builder.append(getStyledKeyValue(ctx, R.string.total_size, String.format(Locale.getDefault(), "%d", packageSizes.first + packageSizes.second))).append(", ") .append(getStyledKeyValue(ctx, R.string.user, String.format(Locale.getDefault(), "%d", packageSizes.first))).append(", ") .append(getStyledKeyValue(ctx, R.string.system, String.format(Locale.getDefault(), "%d", packageSizes.second))) .append("\n"); } } // List available hardware/features builder.append("\n").append(getTitleText(ctx, R.string.features)).append("\n"); List featureStrings = new ArrayList<>(features.length); for (FeatureInfo info : features) { if (info.name != null) { // It's a feature if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && info.version != 0) { featureStrings.add(info.name + " (v" + info.version + ")"); } else featureStrings.add(info.name); } } Collections.sort(featureStrings, (o1, o2) -> o1.toString().compareToIgnoreCase(o2.toString())); builder.append(TextUtilsCompat.joinSpannable("\n", featureStrings)).append("\n"); return builder; } /** * Queries EGL/GL for information, then formats it all into one giant string. */ @NonNull private Spannable getGlInfo(@NonNull Context ctx) { // We need a GL context to examine, which means we need an EGL surface. Create a 1x1 pbuffer. EglCore eglCore = new EglCore(); OffscreenSurface surface = new OffscreenSurface(eglCore, 1, 1); surface.makeCurrent(); String gpu = GLES20.glGetString(GLES20.GL_VENDOR) + " " + GLES20.glGetString(GLES20.GL_RENDERER); SpannableStringBuilder sb = new SpannableStringBuilder(); sb.append(getStyledKeyValue(ctx, "GPU", gpu)).append("\n"); // sb.append(formatExtensions(GLES20.glGetString(GLES20.GL_EXTENSIONS))); surface.release(); eglCore.release(); return sb; } // private String formatExtensions(@NonNull String ext) { // String[] values = ext.split(" "); // Arrays.sort(values); // StringBuilder sb = new StringBuilder(); // for (String value : values) { // sb.append(" "); // sb.append(value); // sb.append("\n"); // } // return sb.toString(); // } private CountDownLatch mBatteryStatusLock; @Nullable private Bundle mBatteryStatusBundle; private final BroadcastReceiver mBatteryStatusReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (mBatteryStatusLock != null) { mBatteryStatusLock.countDown(); } mBatteryStatusBundle = intent.getExtras(); } }; @WorkerThread private void getBatteryStats(Context ctx) { batteryCapacityMAh = new PowerProfile(ContextUtils.getContext()).getBatteryCapacity(); IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); Intent data = ctx.registerReceiver(mBatteryStatusReceiver, filter); if (data != null) { mBatteryStatusBundle = data.getExtras(); } if (mBatteryStatusBundle == null) { // fallback to old method mBatteryStatusLock = new CountDownLatch(1); try { if (!mBatteryStatusLock.await(10, TimeUnit.SECONDS)) { throw new InterruptedException(); } } catch (InterruptedException e) { throw new RuntimeException(e); } } ctx.unregisterReceiver(mBatteryStatusReceiver); if (mBatteryStatusBundle != null) { batteryPresent = mBatteryStatusBundle.getBoolean(BatteryManager.EXTRA_PRESENT); batteryTechnology = mBatteryStatusBundle.getString(BatteryManager.EXTRA_TECHNOLOGY); int batteryCapacityUAh = mBatteryStatusBundle.getInt("charge_counter", 0); if (batteryCapacityUAh != 0) { batteryCapacityMAhAlt = batteryCapacityUAh / 1000.; // This is the current capacity, calculate the actual capacity using the battery // percentage int level = mBatteryStatusBundle.getInt(BatteryManager.EXTRA_LEVEL, 0); int scale = mBatteryStatusBundle.getInt(BatteryManager.EXTRA_SCALE, 0); double batteryPercent = scale > 0 ? (level * 100. / scale) : 0; if (batteryPercent > 0) { batteryCapacityMAhAlt = (batteryCapacityMAhAlt * 100. / batteryPercent); } } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { batteryCycleCount = mBatteryStatusBundle.getInt(BatteryManager.EXTRA_CYCLE_COUNT, 0); } int health = mBatteryStatusBundle.getInt(BatteryManager.EXTRA_HEALTH, BatteryManager.BATTERY_HEALTH_UNKNOWN); switch (health) { case BatteryManager.BATTERY_HEALTH_GOOD: batteryHealth = "Good"; break; case BatteryManager.BATTERY_HEALTH_OVERHEAT: batteryHealth = "Overheat"; break; case BatteryManager.BATTERY_HEALTH_DEAD: batteryHealth = "Dead"; break; case BatteryManager.BATTERY_HEALTH_OVER_VOLTAGE: batteryHealth = "Over voltage"; break; case BatteryManager.BATTERY_HEALTH_UNSPECIFIED_FAILURE: batteryHealth = "failure"; break; case BatteryManager.BATTERY_HEALTH_COLD: batteryHealth = "Cold"; break; default: batteryHealth = "Unknown"; } } } @NonNull private Display getDisplay() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { return Objects.requireNonNull(mActivity.getDisplay()); } else { //noinspection deprecation return mActivity.getWindowManager().getDefaultDisplay(); } } private String getDensity() { int dpi = StaticDataset.DEVICE_DENSITY; int smallestDiff = Integer.MAX_VALUE; String density = StaticDataset.XXXHDPI; // Find the smallest for (int i = 0; i < StaticDataset.DENSITY_NAME_TO_DENSITY.size(); ++i) { int diff = Math.abs(dpi - StaticDataset.DENSITY_NAME_TO_DENSITY.valueAt(i)); if (diff < smallestDiff) { smallestDiff = diff; density = StaticDataset.DENSITY_NAME_TO_DENSITY.keyAt(i); } } return density; } @NonNull private String getVmVersion() { String vm = "Dalvik"; String vmVersion = System.getProperty("java.vm.version"); if (vmVersion != null && vmVersion.startsWith("2")) { vm = "ART"; } return vm; } @NonNull private String getKernel() { String kernel = System.getProperty("os.version"); if (kernel == null) return ""; else return kernel; } @Nullable @RequiresApi(Build.VERSION_CODES.M) public static String getSecurityPatch() { String patch = Build.VERSION.SECURITY_PATCH; if (!"".equals(patch)) { try { SimpleDateFormat template = new SimpleDateFormat("yyyy-MM-dd", Locale.ROOT); Date patchDate = template.parse(patch); String format = DateFormat.getBestDateTimePattern(Locale.getDefault(), "dMMMMyyyy"); patch = DateFormat.format(format, patchDate).toString(); } catch (ParseException e) { // broken parse; fall through and use the raw string } return patch; } else { return null; } } @WorkerThread private int getSelinuxStatus() { if (SELinux.isSELinuxEnabled()) { // if (SELinux.isSELinuxEnforced()) { // return 1; // } Runner.Result result = Runner.runCommand("getenforce"); if (result.isSuccessful() && result.getOutput().trim().equals("Permissive")) { return 0; } // SELinux enabled, but cannot access result means it is "Enforcing" return 1; } // Disabled return 2; } @NonNull private String getEncryptionStatus() { String state = SystemProperties.get("ro.crypto.state", ""); if ("encrypted".equals(state)) { String encryptedMsg = getString(R.string.encrypted); String type = SystemProperties.get("ro.crypto.type", ""); if ("file".equals(type)) return encryptedMsg + " (FBE)"; else if ("block".equals(type)) return encryptedMsg + " (FDE)"; else return encryptedMsg; } else if ("unencrypted".equals(state)) { return getString(R.string.unencrypted); } else return getString(R.string.state_unknown); } @NonNull private String getVerifiedBootStateString(@NonNull String color) { switch (color) { case "green": return "verified"; case "yellow": return "self-signed"; case "red": return "failed"; case "orange": default: return "unverified"; } } @Nullable private String getHardwareBackedFeatures() { // We use string instead of PackageManager.FEATURE_HARDWARE_KEYSTORE because it may present // in older devices. FeatureInfo f = getFeature("android.hardware.hardware_keystore"); if (f == null) { return null; } int version; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { version = f.version; } else version = 0; if (version < 40) { return getString(R.string.state_unknown); } StringBuilder sb = new StringBuilder(); sb.append("AES, HMAC, ECDSA, RSA"); if (version >= 100) { sb.append(", ECDH"); } if (version >= 200) { sb.append(", Curve 25519"); } return sb.toString(); } @Nullable private String getStrongBoxBackedFeatures() { // We use string instead of PackageManager.FEATURE_STRONGBOX_KEYSTORE because it may present // in older devices. FeatureInfo f = getFeature("android.hardware.strongbox_keystore"); if (f == null) { return null; } int version; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { version = f.version; } else version = 0; if (version < 40) { return getString(R.string.state_unknown); } StringBuilder sb = new StringBuilder(); sb.append("AES, HMAC, ECDSA, RSA"); if (version >= 100) { sb.append(", ECDH"); } return sb.toString(); } @Nullable private FeatureInfo getFeature(@NonNull String feature) { for (FeatureInfo info : features) { if (feature.equals(info.name)) { return info; } } return null; } @Nullable private String getCpuHardware() { String model = CpuUtils.getCpuModel(); if (model == null) { // ARM: fallback to /proc/cpuinfo model = ProcFs.getInstance().getCpuInfoHardware(); } if (model == null) { // fallback to Android properties String part1 = SystemProperties.get("ro.soc.manufacturer", ""); String part2 = SystemProperties.get("ro.soc.model", ""); if (!part2.isEmpty()) { return part1 + (!part1.isEmpty() ? " " : "") + part2; } model = SystemProperties.get("ro.board.platform", ""); } return !model.isEmpty() ? model : null; } // User + System apps @NonNull private Pair getPackageStats(int userHandle) { int systemApps = 0; int userApps = 0; try { List applicationInfoList = PackageManagerCompat.getInstalledApplications(PackageManagerCompat.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES, userHandle); for (ApplicationInfo info : applicationInfoList) { if ((info.flags & ApplicationInfo.FLAG_SYSTEM) == 1) { ++systemApps; } else ++userApps; } } catch (Throwable e) { e.printStackTrace(); } return new Pair<>(userApps, systemApps); } private String getString(@StringRes int strRes) { return mActivity.getString(strRes); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/misc/HelpActivity.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.misc; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.webkit.WebResourceRequest; import android.webkit.WebView; import android.widget.Button; import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.LinearLayoutCompat; import androidx.appcompat.widget.SearchView; import androidx.transition.Transition; import androidx.transition.TransitionManager; import androidx.webkit.WebViewClientCompat; import com.google.android.material.transition.MaterialSharedAxis; import io.github.muntashirakon.AppManager.BaseActivity; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.utils.ResourceUtil; import io.github.muntashirakon.AppManager.utils.appearance.AppearanceUtils; import io.github.muntashirakon.util.UiUtils; import me.zhanghai.android.fastscroll.FastScrollerBuilder; public class HelpActivity extends BaseActivity implements SearchView.OnQueryTextListener { private LinearLayoutCompat mContainer; private WebView mWebView; private LinearLayoutCompat mSearchContainer; private SearchView mSearchView; private final OnBackPressedCallback mOnBackPressedCallback = new OnBackPressedCallback(false) { @Override public void handleOnBackPressed() { if (mWebView.canGoBack()) { mWebView.goBack(); return; } setEnabled(false); getOnBackPressedDispatcher().onBackPressed(); } }; @Override protected void onAuthenticated(@Nullable Bundle savedInstanceState) { try { setContentView(R.layout.activity_help); } catch (Throwable th) { openDocsSite(); return; } setSupportActionBar(findViewById(R.id.toolbar)); getOnBackPressedDispatcher().addCallback(this, mOnBackPressedCallback); ActionBar actionBar = getSupportActionBar(); if (actionBar != null) actionBar.setTitle(R.string.user_manual); findViewById(R.id.progress_linear).setVisibility(View.GONE); // Check if docs are available if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_WEBVIEW) || ResourceUtil.getRawDataId(this, "index") == 0) { // Docs split not installed openDocsSite(); return; } mContainer = findViewById(R.id.container); mWebView = findViewById(R.id.webview); UiUtils.applyWindowInsetsAsPaddingNoTop(mContainer); // Fix locale issue due to WebView (https://issuetracker.google.com/issues/37113860) AppearanceUtils.applyOnlyLocale(this); mWebView.setWebViewClient(new WebViewClientImpl()); mWebView.setNetworkAvailable(false); mWebView.getSettings().setAllowContentAccess(false); mWebView.loadUrl("file:///android_res/raw/index.html"); mSearchContainer = findViewById(R.id.search_container); Button nextButton = findViewById(R.id.next_button); Button previousButton = findViewById(R.id.previous_button); mSearchView = findViewById(R.id.search_bar); mSearchView.findViewById(androidx.appcompat.R.id.search_close_btn).setOnClickListener(v -> { mWebView.clearMatches(); mSearchView.setQuery(null, false); Transition sharedAxis = new MaterialSharedAxis(MaterialSharedAxis.Y, true); TransitionManager.beginDelayedTransition(mContainer, sharedAxis); mSearchContainer.setVisibility(View.GONE); }); mSearchView.setOnQueryTextListener(this); nextButton.setOnClickListener(v -> mWebView.findNext(true)); previousButton.setOnClickListener(v -> mWebView.findNext(false)); new FastScrollerBuilder(mWebView).useMd2Style().build(); } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); getMenuInflater().inflate(R.menu.activity_help_actions, menu); return true; } @Override public boolean onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); return true; } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int id = item.getItemId(); if (id == android.R.id.home) { finish(); } else if (id == R.id.action_search) { if (mSearchContainer.getVisibility() == View.VISIBLE) { mSearchView.setQuery(null, false); Transition sharedAxis = new MaterialSharedAxis(MaterialSharedAxis.Y, true); TransitionManager.beginDelayedTransition(mContainer, sharedAxis); mSearchContainer.setVisibility(View.GONE); } else { Transition sharedAxis = new MaterialSharedAxis(MaterialSharedAxis.Y, false); TransitionManager.beginDelayedTransition(mContainer, sharedAxis); mSearchContainer.setVisibility(View.VISIBLE); } return true; } return super.onOptionsItemSelected(item); } @Override public boolean onQueryTextSubmit(String query) { return false; } @Override public boolean onQueryTextChange(String newText) { mWebView.findAllAsync(newText); return true; } private void openDocsSite() { Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.website_message))); startActivity(intent); finish(); } class WebViewClientImpl extends WebViewClientCompat { @Override public boolean shouldOverrideUrlLoading(@NonNull WebView view, @NonNull WebResourceRequest request) { Uri uri = request.getUrl(); if (uri.toString().startsWith("file:///android_res")) { return false; } Intent intent = new Intent(Intent.ACTION_VIEW, uri); startActivity(intent); return true; } @Override public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) { mOnBackPressedCallback.setEnabled(view.canGoBack()); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/misc/LabsActivity.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.misc; import android.content.Context; import android.content.Intent; import android.content.res.ColorStateList; import android.os.Bundle; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.ViewGroup; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import com.google.android.material.button.MaterialButton; import io.github.muntashirakon.AppManager.BaseActivity; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.accessibility.activity.LeadingActivityTrackerActivity; import io.github.muntashirakon.AppManager.editor.CodeEditorActivity; import io.github.muntashirakon.AppManager.fm.FmActivity; import io.github.muntashirakon.AppManager.history.ops.OpHistoryActivity; import io.github.muntashirakon.AppManager.intercept.ActivityInterceptor; import io.github.muntashirakon.AppManager.logcat.LogViewerActivity; import io.github.muntashirakon.AppManager.terminal.TermActivity; import io.github.muntashirakon.AppManager.settings.FeatureController; import io.github.muntashirakon.AppManager.sysconfig.SysConfigActivity; import io.github.muntashirakon.AppManager.utils.appearance.ColorCodes; import io.github.muntashirakon.widget.FlowLayout; public class LabsActivity extends BaseActivity { @Override protected void onAuthenticated(@Nullable Bundle savedInstanceState) { setContentView(R.layout.activity_labs); setSupportActionBar(findViewById(R.id.toolbar)); FlowLayout flowLayout = findViewById(R.id.action_container); if (FeatureController.isLogViewerEnabled()) { addAction(this, flowLayout, R.string.log_viewer, R.drawable.ic_view_list) .setOnClickListener(v -> { Intent intent = new Intent(this, LogViewerActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); }); } addAction(this, flowLayout, R.string.sys_config, R.drawable.ic_hammer_wrench) .setOnClickListener(v -> { Intent intent = new Intent(this, SysConfigActivity.class); startActivity(intent); }); if (FeatureController.isTerminalEnabled()) { addAction(this, flowLayout, R.string.title_terminal_emulator, R.drawable.ic_frost_termux) .setOnClickListener(v -> { Intent intent = new Intent(this, TermActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); }); } addAction(this, flowLayout, R.string.files, R.drawable.ic_file_document_multiple) .setOnClickListener(v -> { Intent intent = new Intent(this, FmActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); }); addAction(this, flowLayout, R.string.title_ui_tracker, R.drawable.ic_cursor_default_click) .setOnClickListener(v -> { Intent intent = new Intent(this, LeadingActivityTrackerActivity.class); startActivity(intent); }); if (FeatureController.isInterceptorEnabled()) { addAction(this, flowLayout, R.string.interceptor, R.drawable.ic_transit_connection) .setOnClickListener(v -> { Intent intent = new Intent(this, ActivityInterceptor.class); startActivity(intent); }); } if (FeatureController.isCodeEditorEnabled()) { addAction(this, flowLayout, R.string.title_code_editor, R.drawable.ic_code) .setOnClickListener(v -> { Intent intent = new Intent(this, CodeEditorActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); }); } addAction(this, flowLayout, R.string.op_history, R.drawable.ic_history) .setOnClickListener(v -> { Intent intent = new Intent(this, OpHistoryActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); }); } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == android.R.id.home) { finish(); return true; } return super.onOptionsItemSelected(item); } @NonNull private static MaterialButton addAction(@NonNull Context context, @NonNull ViewGroup parent, @StringRes int stringResId, @DrawableRes int iconResId) { MaterialButton button = (MaterialButton) LayoutInflater.from(context).inflate(R.layout.item_app_info_action, parent, false); button.setBackgroundTintList(ColorStateList.valueOf(ColorCodes.getListItemColor1(context))); button.setText(stringResId); button.setIconResource(iconResId); parent.addView(button); return button; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/misc/ListOptions.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.misc; import android.app.Application; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.CallSuper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.appcompat.widget.LinearLayoutCompat; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.ViewModelProvider; import com.google.android.material.button.MaterialButton; import com.google.android.material.checkbox.MaterialCheckBox; import com.google.android.material.chip.Chip; import com.google.android.material.chip.ChipGroup; import com.google.android.material.materialswitch.MaterialSwitch; import java.util.LinkedHashMap; import java.util.Objects; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.dialog.CapsuleBottomSheetDialogFragment; import io.github.muntashirakon.widget.MaterialSpinner; public abstract class ListOptions extends CapsuleBottomSheetDialogFragment { public static final String TAG = ListOptions.class.getSimpleName(); public interface ListOptionActions { default void setReverseSort(boolean reverseSort) { } default boolean isReverseSort() { return false; } default void setSortBy(int sortBy) { } default int getSortBy() { return 0; } default boolean hasFilterFlag(int flag) { return false; } default void addFilterFlag(int flag) { } default void removeFilterFlag(int flag) { } default boolean isOptionSelected(int option) { return false; } default void onOptionSelected(int option, boolean selected) { } } private TextView mSortText; private ChipGroup mSortGroup; private MaterialCheckBox mReverseSort; private TextView mFilterText; private ChipGroup mFilterOptions; private TextView mOptionsText; private LinearLayoutCompat mOptionsView; @Nullable private ListOptionActions mListOptionActions; @Nullable private ListOptionsViewModel mListOptionsViewModel; protected MaterialSpinner profileNameSpinner; protected MaterialButton selectUserView; public void setListOptionActions(@Nullable ListOptionActions listOptionActions) { if (mListOptionsViewModel != null) { mListOptionsViewModel.setListOptionActions(listOptionActions); } else mListOptionActions = listOptionActions; } @CallSuper @NonNull @Override public View initRootView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.dialog_list_options, container, false); } @CallSuper @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); mListOptionsViewModel = new ViewModelProvider(this).get(ListOptionsViewModel.class); mSortText = view.findViewById(R.id.sort_text); mSortGroup = view.findViewById(R.id.sort_options); mReverseSort = view.findViewById(R.id.reverse_sort); mFilterText = view.findViewById(R.id.filter_text); mFilterOptions = view.findViewById(R.id.filter_options); mOptionsText = view.findViewById(R.id.options_text); mOptionsView = view.findViewById(R.id.options); profileNameSpinner = view.findViewById(R.id.spinner); selectUserView = view.findViewById(R.id.user); init(false); } @Nullable public abstract LinkedHashMap getSortIdLocaleMap(); @Nullable public abstract LinkedHashMap getFilterFlagLocaleMap(); @Nullable public abstract LinkedHashMap getOptionIdLocaleMap(); public boolean enableProfileNameInput() { return false; } public boolean enableSelectUser() { return false; } public void reloadUi() { init(true); } @NonNull private ListOptionActions requireListOptionActions() { if (mListOptionsViewModel == null) { throw new NullPointerException("ViewModel is not initialized."); } if (mListOptionActions != null) { mListOptionsViewModel.setListOptionActions(mListOptionActions); mListOptionActions = null; } ListOptionActions actions = mListOptionsViewModel.getListOptionActions(); if (actions == null) { throw new NullPointerException("ListOptionsActions must be set before calling init."); } return actions; } private void init(boolean reinit) { // Enable sorting LinkedHashMap sortIdLocaleMap = getSortIdLocaleMap(); boolean sortingEnabled = sortIdLocaleMap != null; mSortText.setVisibility(sortingEnabled ? View.VISIBLE : View.GONE); mSortGroup.setVisibility(sortingEnabled ? View.VISIBLE : View.GONE); mReverseSort.setVisibility(sortingEnabled ? View.VISIBLE : View.GONE); if (sortingEnabled) { int i = 0; for (int sortId : sortIdLocaleMap.keySet()) { int sortStringRes = Objects.requireNonNull(sortIdLocaleMap.get(sortId)); mSortGroup.addView(getRadioChip(sortId, sortStringRes), i); ++i; } mSortGroup.check(requireListOptionActions().getSortBy()); mSortGroup.setOnCheckedStateChangeListener((group, checkedIds) -> requireListOptionActions().setSortBy(mSortGroup.getCheckedChipId())); mReverseSort.setChecked(requireListOptionActions().isReverseSort()); mReverseSort.setOnCheckedChangeListener((buttonView, isChecked) -> requireListOptionActions().setReverseSort(isChecked)); } // Enable filtering LinkedHashMap filterFlagLocaleMap = getFilterFlagLocaleMap(); boolean filteringEnabled = filterFlagLocaleMap != null; mFilterText.setVisibility(filteringEnabled ? View.VISIBLE : View.GONE); mFilterOptions.setVisibility(filteringEnabled ? View.VISIBLE : View.GONE); if (filteringEnabled) { int i = 0; for (int flag : filterFlagLocaleMap.keySet()) { int flagStringRes = Objects.requireNonNull(filterFlagLocaleMap.get(flag)); mFilterOptions.addView(getFilterChip(flag, flagStringRes), i); ++i; } } // Enable options LinkedHashMap optionIdLocaleMap = getOptionIdLocaleMap(); boolean optionsEnabled = optionIdLocaleMap != null; mOptionsText.setVisibility(optionsEnabled ? View.VISIBLE : View.GONE); mOptionsView.setVisibility(optionsEnabled ? View.VISIBLE : View.GONE); if (optionsEnabled) { int i = 0; for (int option : optionIdLocaleMap.keySet()) { int optionStringRes = Objects.requireNonNull(optionIdLocaleMap.get(option)); mOptionsView.addView(getOption(option, optionStringRes), i); ++i; } } // Profile boolean profileEnabled = enableProfileNameInput(); profileNameSpinner.setVisibility(profileEnabled ? View.VISIBLE : View.GONE); // User boolean selectUserEnabled = enableSelectUser(); selectUserView.setVisibility(selectUserEnabled ? View.VISIBLE : View.GONE); if (reinit) { return; } if (sortingEnabled && mSortGroup.getChildCount() > 0) { mSortGroup.getChildAt(0).requestFocus(); } else if (filteringEnabled && mFilterOptions.getChildCount() > 0) { mFilterOptions.getChildAt(0).requestFocus(); } else if (optionsEnabled && mOptionsView.getChildCount() > 0) { mOptionsView.getChildAt(0).requestFocus(); } } @NonNull private MaterialSwitch getOption(int option, @StringRes int strRes) { MaterialSwitch materialSwitch = (MaterialSwitch) View.inflate(mOptionsView.getContext(), R.layout.item_switch, null); materialSwitch.setFocusable(true); materialSwitch.setId(option); materialSwitch.setText(strRes); materialSwitch.setChecked(requireListOptionActions().isOptionSelected(option)); materialSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> requireListOptionActions().onOptionSelected(option, isChecked)); return materialSwitch; } @NonNull private Chip getFilterChip(int flag, @StringRes int strRes) { Chip chip = new Chip(mFilterOptions.getContext()); chip.setFocusable(true); chip.setCloseIconVisible(false); chip.setId(flag); chip.setText(strRes); chip.setChecked(requireListOptionActions().hasFilterFlag(flag)); chip.setOnCheckedChangeListener((buttonView, isChecked) -> { if (isChecked) { requireListOptionActions().addFilterFlag(flag); } else { requireListOptionActions().removeFilterFlag(flag); } }); return chip; } @NonNull private Chip getRadioChip(int sortOrder, @StringRes int strRes) { Chip chip = new Chip(mSortGroup.getContext()); chip.setFocusable(true); chip.setCloseIconVisible(false); chip.setId(sortOrder); chip.setText(strRes); return chip; } public static class ListOptionsViewModel extends AndroidViewModel { @Nullable private ListOptionActions mListOptionActions; public ListOptionsViewModel(@NonNull Application application) { super(application); } public void setListOptionActions(@Nullable ListOptionActions listOptionActions) { mListOptionActions = listOptionActions; } @Nullable public ListOptionActions getListOptionActions() { return mListOptionActions; } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/misc/NoOps.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.misc; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import io.github.muntashirakon.AppManager.settings.Ops; import static java.lang.annotation.ElementType.CONSTRUCTOR; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.SOURCE; /** * Denotes that the method, constructor or class does not contain any checks from {@link Ops}. This is useful to prevent * cycles when checking for root, ADB, etc. *

* TODO: Build a annotation detector */ @Documented @Retention(SOURCE) @Target({METHOD, CONSTRUCTOR, TYPE}) public @interface NoOps { /** * Whether any {@link Ops} checks have been used. */ boolean used() default false; } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/misc/OidMap.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.misc; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.util.HashMap; import java.util.Map; /** * @see android.sun.security.x509.OIDMap */ public class OidMap { private static final Map oidNameMap = new HashMap() {{ put("2.5.29.9", "subjectDirectoryAttributes"); put("2.5.29.14", "subjectKeyIdentifier"); put("2.5.29.15", "keyUsage"); put("2.5.29.16", "privateKeyUsagePeriod"); put("2.5.29.17", "subjectAltName"); put("2.5.29.18", "issuerAltName"); put("2.5.29.19", "basicConstraints"); put("2.5.29.20", "cRLNumber"); put("2.5.29.21", "reasonCode"); put("2.5.29.23", "instructionCode"); put("2.5.29.24", "invalidityDate"); put("2.5.29.27", "deltaCRLIndicator"); put("2.5.29.28", "issuingDistributionPoint"); put("2.5.29.29", "certificateIssuer"); put("2.5.29.30", "nameConstraints"); put("2.5.29.31", "cRLDistributionPoints"); put("2.5.29.32", "certificatePolicies"); put("2.5.29.33", "policyMappings"); put("2.5.29.35", "authorityKeyIdentifier"); put("2.5.29.36", "policyConstraints"); put("2.5.29.37", "extKeyUsage"); put("2.5.29.38", "authorityAttributeIdentifier"); put("2.5.29.39", "roleSpecCertIdentifier"); put("2.5.29.40", "cRLStreamIdentifier"); put("2.5.29.41", "basicAttConstraints"); put("2.5.29.42", "delegatedNameConstraints"); put("2.5.29.43", "timeSpecification"); put("2.5.29.44", "cRLScope"); put("2.5.29.45", "statusReferrals"); put("2.5.29.46", "freshestCRL"); put("2.5.29.47", "orderedList"); put("2.5.29.48", "attributeDescriptor"); put("2.5.29.49", "userNotice"); put("2.5.29.50", "sOAIdentifier"); put("2.5.29.51", "baseUpdateTime"); put("2.5.29.52", "acceptableCertPolicies"); put("2.5.29.53", "deltaInfo"); put("2.5.29.54", "inhibitAnyPolicy"); put("2.5.29.55", "targetInformation"); put("2.5.29.56", "noRevAvail"); put("2.5.29.57", "acceptablePrivilegePolicies"); put("2.5.29.61", "indirectIssuer"); put("1.3.6.1.5.5.7.1.1", "AuthorityInfoAccess"); put("1.3.6.1.5.5.7.1.11", "SubjectInfoAccess"); put("1.3.6.1.5.5.7.48.1.5", "OCSPNoCheck"); }}; @Nullable public static String getName(@NonNull String oid) { return oidNameMap.get(oid); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/misc/OsEnvironment.java ================================================ // SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.misc; import android.annotation.UserIdInt; import android.os.UserHandleHidden; import android.os.storage.StorageManagerHidden; import android.os.storage.StorageVolume; import android.os.storage.StorageVolumeHidden; import android.text.TextUtils; import android.util.SparseArray; import androidx.annotation.NonNull; import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.Objects; import dev.rikka.tools.refine.Refine; import io.github.muntashirakon.AppManager.compat.StorageManagerCompat; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; // Keep this in sync with https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Environment.java // Last snapshot https://cs.android.com/android/_/android/platform/frameworks/base/+/bc3d8b9071d4f0b2903d6836770d974e70366290 @SuppressWarnings("unused") public final class OsEnvironment { private static final String TAG = "OsEnvironment"; private static final String ENV_EXTERNAL_STORAGE = "EXTERNAL_STORAGE"; private static final String ENV_ANDROID_ROOT = "ANDROID_ROOT"; private static final String ENV_ANDROID_DATA = "ANDROID_DATA"; private static final String ENV_ANDROID_EXPAND = "ANDROID_EXPAND"; private static final String ENV_ANDROID_STORAGE = "ANDROID_STORAGE"; private static final String ENV_DOWNLOAD_CACHE = "DOWNLOAD_CACHE"; private static final String ENV_OEM_ROOT = "OEM_ROOT"; private static final String ENV_ODM_ROOT = "ODM_ROOT"; private static final String ENV_VENDOR_ROOT = "VENDOR_ROOT"; private static final String ENV_PRODUCT_ROOT = "PRODUCT_ROOT"; private static final String ENV_SYSTEM_EXT_ROOT = "SYSTEM_EXT_ROOT"; private static final String ENV_APEX_ROOT = "APEX_ROOT"; public static final String DIR_ANDROID = "Android"; private static final String DIR_DATA = "data"; private static final String DIR_MEDIA = "media"; private static final String DIR_OBB = "obb"; private static final String DIR_FILES = "files"; private static final String DIR_CACHE = "cache"; private static final String DIR_ANDROID_ROOT = getDirectory(ENV_ANDROID_ROOT, "/system"); private static final String DIR_ANDROID_DATA = getDirectory(ENV_ANDROID_DATA, "/data"); private static final String DIR_ANDROID_EXPAND = getDirectory(ENV_ANDROID_EXPAND, "/mnt/expand"); private static final String DIR_ANDROID_STORAGE = getDirectory(ENV_ANDROID_STORAGE, "/storage"); private static final String DIR_DOWNLOAD_CACHE = getDirectory(ENV_DOWNLOAD_CACHE, "/cache"); private static final String DIR_OEM_ROOT = getDirectory(ENV_OEM_ROOT, "/oem"); private static final String DIR_ODM_ROOT = getDirectory(ENV_ODM_ROOT, "/odm"); private static final String DIR_VENDOR_ROOT = getDirectory(ENV_VENDOR_ROOT, "/vendor"); private static final String DIR_PRODUCT_ROOT = getDirectory(ENV_PRODUCT_ROOT, "/product"); private static final String DIR_SYSTEM_EXT_ROOT = getDirectory(ENV_SYSTEM_EXT_ROOT, "/system_ext"); private static final String DIR_APEX_ROOT = getDirectory(ENV_APEX_ROOT, "/apex"); private static final UserEnvironment sCurrentUser; private static boolean sUserRequired; private static final SparseArray sUserEnvironmentCache = new SparseArray<>(2); static { sCurrentUser = new UserEnvironment(UserHandleHidden.myUserId()); sUserEnvironmentCache.put(sCurrentUser.mUserHandle, sCurrentUser); } @NonNull public static UserEnvironment getUserEnvironment(@UserIdInt int userHandle) { UserEnvironment ue = sUserEnvironmentCache.get(userHandle); if (ue != null) return ue; ue = new UserEnvironment(userHandle); sUserEnvironmentCache.put(userHandle, ue); return ue; } public static class UserEnvironment { @UserIdInt private final int mUserHandle; public UserEnvironment(@UserIdInt int userHandle) { mUserHandle = userHandle; } @NonNull public Path[] getExternalDirs() { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { final StorageVolume[] volumes = StorageManagerCompat.getVolumeList(ContextUtils.getContext(), mUserHandle, StorageManagerHidden.FLAG_FOR_WRITE); final List files = new ArrayList<>(); File tmpFile; for (@NonNull StorageVolume volume : volumes) { StorageVolumeHidden vol = Refine.unsafeCast(volume); tmpFile = vol.getPathFile(); if (tmpFile != null) { files.add(Paths.get(tmpFile.getAbsolutePath())); } } return files.toArray(new Path[0]); } String rawExternalStorage = System.getenv(ENV_EXTERNAL_STORAGE); String rawEmulatedTarget = System.getenv("EMULATED_STORAGE_TARGET"); List externalForApp = new ArrayList<>(); if (!TextUtils.isEmpty(rawEmulatedTarget)) { // Device has emulated storage; external storage paths should have // userId burned into them. final String rawUserId = Integer.toString(mUserHandle); //noinspection ConstantConditions final File emulatedTargetBase = new File(rawEmulatedTarget); // /storage/emulated/0 externalForApp.add(Paths.build(emulatedTargetBase, rawUserId)); } else { // Device has physical external storage; use plain paths. if (TextUtils.isEmpty(rawExternalStorage)) { Log.w(TAG, "EXTERNAL_STORAGE undefined; falling back to default"); rawExternalStorage = "/storage/sdcard0"; } // /storage/sdcard0 externalForApp.add(Paths.get(rawExternalStorage)); } return externalForApp.toArray(new Path[0]); } @Deprecated public Path getExternalStorageDirectory() { return getExternalDirs()[0]; } @Deprecated public Path getExternalStoragePublicDirectory(String type) { return buildExternalStoragePublicDirs(type)[0]; } public Path[] buildExternalStoragePublicDirs(String type) { return Paths.build(getExternalDirs(), type); } public Path[] buildExternalStorageAndroidDataDirs() { return Paths.build(getExternalDirs(), DIR_ANDROID, DIR_DATA); } public Path[] buildExternalStorageAndroidObbDirs() { return Paths.build(getExternalDirs(), DIR_ANDROID, DIR_OBB); } public Path[] buildExternalStorageAppDataDirs(String packageName) { return Paths.build(getExternalDirs(), DIR_ANDROID, DIR_DATA, packageName); } public Path[] buildExternalStorageAppMediaDirs(String packageName) { return Paths.build(getExternalDirs(), DIR_ANDROID, DIR_MEDIA, packageName); } public Path[] buildExternalStorageAppObbDirs(String packageName) { return Paths.build(getExternalDirs(), DIR_ANDROID, DIR_OBB, packageName); } public Path[] buildExternalStorageAppFilesDirs(String packageName) { return Paths.build(getExternalDirs(), DIR_ANDROID, DIR_DATA, packageName, DIR_FILES); } public Path[] buildExternalStorageAppCacheDirs(String packageName) { return Paths.build(getExternalDirs(), DIR_ANDROID, DIR_DATA, packageName, DIR_CACHE); } } /** * Return root of the "system" partition holding the core Android OS. * Always present and mounted read-only. */ @NonNull public static Path getRootDirectory() { return Paths.get(DIR_ANDROID_ROOT); } /** * Return root directory of the "oem" partition holding OEM customizations, * if any. If present, the partition is mounted read-only. */ @NonNull public static Path getOemDirectory() { return Paths.get(DIR_OEM_ROOT); } /** * Return root directory of the "odm" partition holding ODM customizations, * if any. If present, the partition is mounted read-only. */ @NonNull public static Path getOdmDirectory() { return Paths.get(DIR_ODM_ROOT); } /** * Return root directory of the "vendor" partition that holds vendor-provided * software that should persist across simple reflashing of the "system" partition. */ @NonNull public static Path getVendorDirectory() { return Paths.get(DIR_VENDOR_ROOT); } @NonNull public static String getVendorDirectoryRaw() { return DIR_VENDOR_ROOT; } /** * Return root directory of the "product" partition holding product-specific * customizations if any. If present, the partition is mounted read-only. */ @NonNull public static Path getProductDirectory() { return Paths.get(DIR_PRODUCT_ROOT); } @NonNull public static String getProductDirectoryRaw() { return DIR_PRODUCT_ROOT; } /** * Return root directory of the "system_ext" partition holding system partition's extension * If present, the partition is mounted read-only. */ @NonNull public static Path getSystemExtDirectory() { return Paths.get(DIR_SYSTEM_EXT_ROOT); } /** * Return the user data directory. */ @NonNull public static Path getDataDirectory() { return Paths.get(DIR_ANDROID_DATA); } @NonNull public static String getDataDirectoryRaw() { return DIR_ANDROID_DATA; } @NonNull public static Path getDataSystemDirectory() { return Objects.requireNonNull(Paths.build(getDataDirectory(), "system")); } @NonNull public static Path getDataAppDirectory() { return Objects.requireNonNull(Paths.build(getDataDirectory(), "app")); } @NonNull public static Path getDataDataDirectory() { return Objects.requireNonNull(Paths.build(getDataDirectory(), "data")); } @NonNull public static Path getUserSystemDirectory(int userId) { return Objects.requireNonNull(Paths.build(getDataSystemDirectory(), "users", Integer.toString(userId))); } /** * Returns the path for android-specific data on the SD card. */ public static Path[] buildExternalStorageAndroidDataDirs() { throwIfUserRequired(); return sCurrentUser.buildExternalStorageAndroidDataDirs(); } /** * Generates the raw path to an application's data */ public static Path[] buildExternalStorageAppDataDirs(String packageName) { throwIfUserRequired(); return sCurrentUser.buildExternalStorageAppDataDirs(packageName); } /** * Generates the raw path to an application's media */ public static Path[] buildExternalStorageAppMediaDirs(String packageName) { throwIfUserRequired(); return sCurrentUser.buildExternalStorageAppMediaDirs(packageName); } /** * Generates the raw path to an application's OBB files */ public static Path[] buildExternalStorageAppObbDirs(String packageName) { throwIfUserRequired(); return sCurrentUser.buildExternalStorageAppObbDirs(packageName); } /** * Generates the path to an application's files. */ public static Path[] buildExternalStorageAppFilesDirs(String packageName) { throwIfUserRequired(); return sCurrentUser.buildExternalStorageAppFilesDirs(packageName); } /** * Generates the path to an application's cache. */ public static Path[] buildExternalStorageAppCacheDirs(String packageName) { throwIfUserRequired(); return sCurrentUser.buildExternalStorageAppCacheDirs(packageName); } public static Path[] buildExternalStoragePublicDirs(@NonNull String dirType) { throwIfUserRequired(); return sCurrentUser.buildExternalStoragePublicDirs(dirType); } public static Path[] buildExternalStoragePublicDirs() { throwIfUserRequired(); return sCurrentUser.getExternalDirs(); } @NonNull static String getDirectory(String variableName, String defaultPath) { String path = System.getenv(variableName); return path == null ? defaultPath : path; } public static void setUserRequired(boolean userRequired) { sUserRequired = userRequired; } private static void throwIfUserRequired() { if (sUserRequired) { Log.e(TAG, "Path requests must specify a user by using UserEnvironment", new Throwable()); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/misc/ScreenLockChecker.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.misc; import android.app.KeyguardManager; import android.content.Context; import android.os.PowerManager; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import java.util.Locale; import java.util.Timer; import java.util.TimerTask; import io.github.muntashirakon.AppManager.logs.Log; public final class ScreenLockChecker { public static final String TAG = ScreenLockChecker.class.getSimpleName(); private static final int SECOND = 1000; private static final int MINUTE = 60 * SECOND; // This tracks the deltas between the actual options of 5s, 15s, 30s, 1m, 2m, 5m, 10m // It also includes an initial offset and some extra times (for safety) private static final int[] sCheckLockDelays = new int[]{SECOND, 5 * SECOND, 10 * SECOND, 20 * SECOND, 30 * SECOND, MINUTE, 3 * MINUTE, 5 * MINUTE, 10 * MINUTE, 30 * MINUTE}; private final Context mContext; private final Timer mTimer = new Timer(); @Nullable private final Runnable mRunnable; @Nullable private CheckLockTask mCheckLockTask; public ScreenLockChecker(@NonNull Context context, @Nullable Runnable runnable) { mContext = context.getApplicationContext(); mRunnable = runnable; } public void checkLock() { checkLock(-1); } @WorkerThread private void checkLock(int delayIndex) { KeyguardManager keyguardManager = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE); PowerManager powerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); final boolean isProtected = keyguardManager.isKeyguardSecure(); final boolean isLocked = keyguardManager.isKeyguardLocked(); final boolean isInteractive = powerManager.isInteractive(); delayIndex = getSafeCheckLockDelay(delayIndex); Log.i(TAG, "checkLock: isProtected=%b, isLocked=%b, isInteractive=%b, delay=%d", isProtected, isLocked, isInteractive, sCheckLockDelays[delayIndex]); if (mCheckLockTask != null) { Log.i(TAG, "checkLock: cancelling CheckLockTask[%x]", System.identityHashCode(mCheckLockTask)); mCheckLockTask.cancel(); } if (isProtected && !isLocked && !isInteractive) { mCheckLockTask = new CheckLockTask(delayIndex); Log.i(TAG, "checkLock: scheduling CheckLockTask[%x] for %d ms", System.identityHashCode(mCheckLockTask), sCheckLockDelays[delayIndex]); mTimer.schedule(mCheckLockTask, sCheckLockDelays[delayIndex]); } else { Log.d(TAG, "checkLock: no need to schedule CheckLockTask"); if (isProtected && isLocked) { if (mRunnable != null) { mRunnable.run(); } } } } private static int getSafeCheckLockDelay(final int delayIndex) { final int safeDelayIndex; if (delayIndex >= sCheckLockDelays.length) { safeDelayIndex = sCheckLockDelays.length - 1; } else safeDelayIndex = Math.max(delayIndex, 0); Log.v(TAG, "getSafeCheckLockDelay(%d) returns %d", delayIndex, safeDelayIndex); return safeDelayIndex; } private class CheckLockTask extends TimerTask { final int delayIndex; CheckLockTask(final int delayIndex) { this.delayIndex = delayIndex; } @Override public void run() { Log.i(TAG, "CLT.run [%x]: redirect intent to LockMonitor", System.identityHashCode(this)); checkLock(getSafeCheckLockDelay(delayIndex + 1)); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/misc/SystemProperties.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.misc; import androidx.annotation.NonNull; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.runner.Runner; public final class SystemProperties { @NonNull public static String get(@NonNull String key, @NonNull String defaultVal) { try { return android.os.SystemProperties.get(key, defaultVal); } catch (Exception e) { Log.w("SystemProperties", "Unable to use SystemProperties.get", e); Runner.Result result = Runner.runCommand(new String[]{"getprop", key, defaultVal}); if (result.isSuccessful()) return result.getOutput().trim(); else return defaultVal; } } public static boolean getBoolean(@NonNull String key, boolean defaultVal) { String val = get(key, String.valueOf(defaultVal)); if ("1".equals(val)) return true; return Boolean.parseBoolean(val); } public static int getInt(@NonNull String key, int defaultVal) { String val = get(key, String.valueOf(defaultVal)); try { return Integer.parseInt(val); } catch (NumberFormatException e) { return defaultVal; } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/misc/VMRuntime.java ================================================ // SPDX-License-Identifier: Apache-2.0 package io.github.muntashirakon.AppManager.misc; import java.util.HashMap; import java.util.Map; import androidx.annotation.NonNull; // Keep this in sync with https://cs.android.com/android/platform/superproject/+/master:libcore/libart/src/main/java/dalvik/system/VMRuntime.java public final class VMRuntime { public static final String ABI_ARMEABI = "armeabi"; public static final String ABI_ARMEABI_V7A = "armeabi-v7a"; public static final String ABI_MIPS = "mips"; public static final String ABI_MIPS64 = "mips64"; public static final String ABI_X86 = "x86"; public static final String ABI_X86_64 = "x86_64"; public static final String ABI_ARM64_V8A = "arm64-v8a"; public static final String ABI_ARM64_V8A_HWASAN = "arm64-v8a-hwasan"; public static final String INSTRUCTION_SET_ARM = "arm"; public static final String INSTRUCTION_SET_MIPS = "mips"; public static final String INSTRUCTION_SET_MIPS64 = "mips64"; public static final String INSTRUCTION_SET_X86 = "x86"; public static final String INSTRUCTION_SET_X86_64 = "x86_64"; public static final String INSTRUCTION_SET_ARM64 = "arm64"; private static final Map ABI_TO_INSTRUCTION_SET_MAP = new HashMap<>(16); static { ABI_TO_INSTRUCTION_SET_MAP.put(ABI_ARMEABI, INSTRUCTION_SET_ARM); ABI_TO_INSTRUCTION_SET_MAP.put(ABI_ARMEABI_V7A, INSTRUCTION_SET_ARM); ABI_TO_INSTRUCTION_SET_MAP.put(ABI_MIPS, INSTRUCTION_SET_MIPS); ABI_TO_INSTRUCTION_SET_MAP.put(ABI_MIPS64, INSTRUCTION_SET_MIPS64); ABI_TO_INSTRUCTION_SET_MAP.put(ABI_X86, INSTRUCTION_SET_X86); ABI_TO_INSTRUCTION_SET_MAP.put(ABI_X86_64, INSTRUCTION_SET_X86_64); ABI_TO_INSTRUCTION_SET_MAP.put(ABI_ARM64_V8A, INSTRUCTION_SET_ARM64); ABI_TO_INSTRUCTION_SET_MAP.put(ABI_ARM64_V8A_HWASAN, INSTRUCTION_SET_ARM64); } @NonNull public static String getInstructionSet(String abi) { final String instructionSet = ABI_TO_INSTRUCTION_SET_MAP.get(abi); if (instructionSet == null) { throw new IllegalArgumentException("Unsupported ABI: " + abi); } return instructionSet; } public static boolean is64BitInstructionSet(String instructionSet) { return INSTRUCTION_SET_ARM64.equals(instructionSet) || INSTRUCTION_SET_X86_64.equals(instructionSet) || INSTRUCTION_SET_MIPS64.equals(instructionSet); } public static boolean is64BitAbi(String abi) { return is64BitInstructionSet(getInstructionSet(abi)); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/misc/XposedModuleInfo.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.misc; import static io.github.muntashirakon.AppManager.utils.UIUtils.getBoldString; import static io.github.muntashirakon.AppManager.utils.UIUtils.getStyledKeyValue; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.text.SpannableStringBuilder; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Properties; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.util.LocalizedString; import io.github.muntashirakon.util.UiUtils; // Source: https://github.com/LSPosed/LSPosed/blob/1c586fe41f22fac84c46d33db61e3a04ad528409/app/src/main/java/org/lsposed/manager/util/ModuleUtil.java#L88 // Copyright 2020 EdXposed Contributors // Copyright 2021 LSPosed Contributors // Copyright 2023 Muntashir Al-Islam public class XposedModuleInfo implements LocalizedString { public static final String TAG = XposedModuleInfo.class.getSimpleName(); @Nullable public static Boolean isXposedModule(@NonNull ApplicationInfo app, @NonNull ZipFile zipFile) { if (app.metaData != null && app.metaData.containsKey("xposedminversion")) { return null; } return zipFile.getEntry("META-INF/xposed/module.prop") != null; } public final String packageName; public final boolean legacy; public final int minVersion; public final int targetVersion; public final boolean staticScope; private final ApplicationInfo mApp; private CharSequence mAppLabel; private CharSequence mDescription; private List mScopeList; public XposedModuleInfo(@NonNull ApplicationInfo applicationInfo, @Nullable ZipFile modernModuleApk) { mApp = applicationInfo; packageName = mApp.packageName; legacy = modernModuleApk == null; if (legacy) { Object minVersionRaw = mApp.metaData.get("xposedminversion"); if (minVersionRaw instanceof Integer) { minVersion = (Integer) minVersionRaw; } else if (minVersionRaw instanceof String) { minVersion = extractIntPart((String) minVersionRaw); } else { minVersion = 0; } targetVersion = minVersion; // legacy modules don't have a target version staticScope = false; } else { int minVersion = 100; int targetVersion = 100; boolean staticScope = false; try { ZipEntry propEntry = modernModuleApk.getEntry("META-INF/xposed/module.prop"); if (propEntry != null) { Properties prop = new Properties(); prop.load(modernModuleApk.getInputStream(propEntry)); minVersion = extractIntPart(prop.getProperty("minApiVersion")); targetVersion = extractIntPart(prop.getProperty("targetApiVersion")); staticScope = TextUtils.equals(prop.getProperty("staticScope"), "true"); } ZipEntry scopeEntry = modernModuleApk.getEntry("META-INF/xposed/scope.list"); if (scopeEntry != null) { try (BufferedReader reader = new BufferedReader(new InputStreamReader(modernModuleApk.getInputStream(scopeEntry)))) { mScopeList = new ArrayList<>(); String line; while ((line = reader.readLine()) != null) { mScopeList.add(line); } } } else { mScopeList = Collections.emptyList(); } } catch (IOException | OutOfMemoryError e) { Log.e(TAG, "Error while reading modern module APK", e); } this.minVersion = minVersion; this.targetVersion = targetVersion; this.staticScope = staticScope; } } public CharSequence getAppLabel(@NonNull PackageManager pm) { if (mAppLabel == null) mAppLabel = mApp.loadLabel(pm); return mAppLabel; } public CharSequence getDescription(@NonNull PackageManager pm) { if (mDescription != null) return mDescription; CharSequence descriptionTmp = ""; if (legacy) { Object descriptionRaw = mApp.metaData.get("xposeddescription"); if (descriptionRaw instanceof String) { descriptionTmp = ((String) descriptionRaw).trim(); } else if (descriptionRaw instanceof Integer) { try { int resId = (Integer) descriptionRaw; if (resId != 0) descriptionTmp = pm.getResourcesForApplication(mApp).getString(resId).trim(); } catch (Exception ignored) { } } } else { CharSequence des = mApp.loadDescription(pm); if (des != null) { descriptionTmp = des; } } mDescription = descriptionTmp; return mDescription; } public List getScopeList(@NonNull PackageManager pm) { if (mScopeList != null) return mScopeList; List list = null; try { int scopeListResourceId = mApp.metaData.getInt("xposedscope"); if (scopeListResourceId != 0) { list = Arrays.asList(pm.getResourcesForApplication(mApp).getStringArray(scopeListResourceId)); } else { String scopeListString = mApp.metaData.getString("xposedscope"); if (scopeListString != null) list = Arrays.asList(scopeListString.split(";")); } } catch (Exception ignored) { } if (list != null) { // For historical reasons, legacy modules use the opposite name. // https://github.com/rovo89/XposedBridge/commit/6b49688c929a7768f3113b4c65b429c7a7032afa list.replaceAll(s -> { switch (s) { case "android": return "system"; case "system": return "android"; default: return s; } }); mScopeList = list; } return mScopeList; } @NonNull @Override public CharSequence toLocalizedString(@NonNull Context context) { PackageManager pm = context.getPackageManager(); SpannableStringBuilder sb = new SpannableStringBuilder() .append(getStyledKeyValue(context, R.string.module_name, getAppLabel(pm))).append("\n") .append(getStyledKeyValue(context, R.string.title_description, getDescription(pm))).append("\n") .append(getStyledKeyValue(context, R.string.type, legacy ? "Legacy" : "Modern")).append("\n"); if (legacy) { sb.append(getStyledKeyValue(context, "Xposed Minimum API", String.valueOf(minVersion))).append("\n"); } else { sb.append(getStyledKeyValue(context, "Xposed API", "")).append("\n") .append(getStyledKeyValue(context, " Min", String.valueOf(minVersion))).append(", ") .append(getStyledKeyValue(context, "Target", String.valueOf(targetVersion))).append("\n") .append(getStyledKeyValue(context, "Scope", staticScope ? "Static" : "Dynamic")).append("\n"); } List scopeList = getScopeList(pm); if (scopeList != null && !scopeList.isEmpty()) { sb.append(getBoldString("Scopes")).append("\n").append(UiUtils.getOrderedList(scopeList)); } return sb; } public static int extractIntPart(@NonNull String str) { int result = 0, length = str.length(); for (int offset = 0; offset < length; offset++) { char c = str.charAt(offset); if ('0' <= c && c <= '9') result = result * 10 + (c - '0'); else break; } return result; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/misc/gles/EglCore.java ================================================ // SPDX-License-Identifier: Apache-2.0 package io.github.muntashirakon.AppManager.misc.gles; import android.graphics.SurfaceTexture; import android.opengl.EGL14; import android.opengl.EGLConfig; import android.opengl.EGLContext; import android.opengl.EGLDisplay; import android.opengl.EGLExt; import android.opengl.EGLSurface; import android.util.Log; import android.view.Surface; import androidx.annotation.Nullable; /** * Core EGL state (display, context, config). *

* The EGLContext must only be attached to one thread at a time. This class is not thread-safe. */ public final class EglCore { private static final String TAG = EglCore.class.getSimpleName(); /** * Constructor flag: surface must be recordable. This discourages EGL from using a * pixel format that cannot be converted efficiently to something usable by the video * encoder. */ public static final int FLAG_RECORDABLE = 0x01; // Android-specific extension. private static final int EGL_RECORDABLE_ANDROID = 0x3142; private EGLDisplay mEGLDisplay = EGL14.EGL_NO_DISPLAY; private EGLContext mEGLContext = EGL14.EGL_NO_CONTEXT; private EGLConfig mEGLConfig = null; private int mGlVersion = -1; /** * Prepares EGL display and context. *

* Equivalent to EglCore(null, 0). */ public EglCore() { this(null, 0); } /** * Prepares EGL display and context. *

* * @param sharedContext The context to share, or null if sharing is not desired. * @param flags Configuration bit flags, e.g. FLAG_RECORDABLE. */ public EglCore(@Nullable EGLContext sharedContext, int flags) { if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) { throw new RuntimeException("EGL already set up"); } if (sharedContext == null) { sharedContext = EGL14.EGL_NO_CONTEXT; } mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) { throw new RuntimeException("unable to get EGL14 display"); } int[] version = new int[2]; if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) { mEGLDisplay = null; throw new RuntimeException("unable to initialize EGL14"); } // Try to get a GLES3 context { EGLConfig config = getConfig(flags, 3); if (config != null) { int[] attrib3_list = { EGL14.EGL_CONTEXT_CLIENT_VERSION, 3, EGL14.EGL_NONE }; EGLContext context = EGL14.eglCreateContext(mEGLDisplay, config, sharedContext, attrib3_list, 0); if (EGL14.eglGetError() == EGL14.EGL_SUCCESS) { mEGLConfig = config; mEGLContext = context; mGlVersion = 3; } } } if (mEGLContext == EGL14.EGL_NO_CONTEXT) { // GLES 3 attempt failed EGLConfig config = getConfig(flags, 2); if (config == null) { throw new RuntimeException("Unable to find a suitable EGLConfig"); } int[] attrib2_list = { EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE }; EGLContext context = EGL14.eglCreateContext(mEGLDisplay, config, sharedContext, attrib2_list, 0); checkEglError("eglCreateContext"); mEGLConfig = config; mEGLContext = context; mGlVersion = 2; } // Confirm with query. int[] values = new int[1]; EGL14.eglQueryContext(mEGLDisplay, mEGLContext, EGL14.EGL_CONTEXT_CLIENT_VERSION, values, 0); Log.d(TAG, "EGLContext created, client version " + values[0]); } /** * Finds a suitable EGLConfig. * * @param flags Bit flags from constructor. * @param version Must be 2 or 3. */ @Nullable private EGLConfig getConfig(int flags, int version) { int renderableType = EGL14.EGL_OPENGL_ES2_BIT; if (version >= 3) { renderableType |= EGLExt.EGL_OPENGL_ES3_BIT_KHR; } // The actual surface is generally RGBA or RGBX, so situationally omitting alpha // doesn't really help. It can also lead to a huge performance hit on glReadPixels() // when reading into a GL_RGBA buffer. int[] attribList = { EGL14.EGL_RED_SIZE, 8, EGL14.EGL_GREEN_SIZE, 8, EGL14.EGL_BLUE_SIZE, 8, EGL14.EGL_ALPHA_SIZE, 8, //EGL14.EGL_DEPTH_SIZE, 16, //EGL14.EGL_STENCIL_SIZE, 8, EGL14.EGL_RENDERABLE_TYPE, renderableType, EGL14.EGL_NONE, 0, // placeholder for recordable [@-3] EGL14.EGL_NONE }; if ((flags & FLAG_RECORDABLE) != 0) { attribList[attribList.length - 3] = EGL_RECORDABLE_ANDROID; attribList[attribList.length - 2] = 1; } EGLConfig[] configs = new EGLConfig[1]; int[] numConfigs = new int[1]; if (!EGL14.eglChooseConfig(mEGLDisplay, attribList, 0, configs, 0, configs.length, numConfigs, 0)) { Log.w(TAG, "unable to find RGB8888 / " + version + " EGLConfig"); return null; } return configs[0]; } /** * Discards all resources held by this class, notably the EGL context. This must be * called from the thread where the context was created. *

* On completion, no context will be current. */ public void release() { if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) { // Android is unusual in that it uses a reference-counted EGLDisplay. So for // every eglInitialize() we need an eglTerminate(). EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT); EGL14.eglDestroyContext(mEGLDisplay, mEGLContext); EGL14.eglReleaseThread(); EGL14.eglTerminate(mEGLDisplay); } mEGLDisplay = EGL14.EGL_NO_DISPLAY; mEGLContext = EGL14.EGL_NO_CONTEXT; mEGLConfig = null; } @Override protected void finalize() throws Throwable { try { if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) { // We're limited here -- finalizers don't run on the thread that holds // the EGL state, so if a surface or context is still current on another // thread we can't fully release it here. Exceptions thrown from here // are quietly discarded. Complain in the log file. Log.w(TAG, "WARNING: EglCore was not explicitly released -- state may be leaked"); release(); } } finally { super.finalize(); } } /** * Destroys the specified surface. Note the EGLSurface won't actually be destroyed if it's * still current in a context. */ public void releaseSurface(EGLSurface eglSurface) { EGL14.eglDestroySurface(mEGLDisplay, eglSurface); } /** * Creates an EGL surface associated with a Surface. *

* If this is destined for MediaCodec, the EGLConfig should have the "recordable" attribute. */ public EGLSurface createWindowSurface(Object surface) { if (!(surface instanceof Surface) && !(surface instanceof SurfaceTexture)) { throw new RuntimeException("invalid surface: " + surface); } // Create a window surface, and attach it to the Surface we received. int[] surfaceAttribs = { EGL14.EGL_NONE }; EGLSurface eglSurface = EGL14.eglCreateWindowSurface(mEGLDisplay, mEGLConfig, surface, surfaceAttribs, 0); checkEglError("eglCreateWindowSurface"); if (eglSurface == null) { throw new RuntimeException("surface was null"); } return eglSurface; } /** * Creates an EGL surface associated with an offscreen buffer. */ public EGLSurface createOffscreenSurface(int width, int height) { int[] surfaceAttribs = { EGL14.EGL_WIDTH, width, EGL14.EGL_HEIGHT, height, EGL14.EGL_NONE }; EGLSurface eglSurface = EGL14.eglCreatePbufferSurface(mEGLDisplay, mEGLConfig, surfaceAttribs, 0); checkEglError("eglCreatePbufferSurface"); if (eglSurface == null) { throw new RuntimeException("surface was null"); } return eglSurface; } /** * Makes our EGL context current, using the supplied surface for both "draw" and "read". */ public void makeCurrent(EGLSurface eglSurface) { if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) { // called makeCurrent() before create? Log.d(TAG, "NOTE: makeCurrent w/o display"); } if (!EGL14.eglMakeCurrent(mEGLDisplay, eglSurface, eglSurface, mEGLContext)) { throw new RuntimeException("eglMakeCurrent failed"); } } /** * Makes our EGL context current, using the supplied "draw" and "read" surfaces. */ public void makeCurrent(EGLSurface drawSurface, EGLSurface readSurface) { if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) { // called makeCurrent() before create? Log.d(TAG, "NOTE: makeCurrent w/o display"); } if (!EGL14.eglMakeCurrent(mEGLDisplay, drawSurface, readSurface, mEGLContext)) { throw new RuntimeException("eglMakeCurrent(draw,read) failed"); } } /** * Makes no context current. */ public void makeNothingCurrent() { if (!EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT)) { throw new RuntimeException("eglMakeCurrent failed"); } } /** * Calls eglSwapBuffers. Use this to "publish" the current frame. * * @return false on failure */ public boolean swapBuffers(EGLSurface eglSurface) { return EGL14.eglSwapBuffers(mEGLDisplay, eglSurface); } /** * Sends the presentation time stamp to EGL. Time is expressed in nanoseconds. */ public void setPresentationTime(EGLSurface eglSurface, long nsecs) { EGLExt.eglPresentationTimeANDROID(mEGLDisplay, eglSurface, nsecs); } /** * Returns true if our context and the specified surface are current. */ public boolean isCurrent(EGLSurface eglSurface) { return mEGLContext.equals(EGL14.eglGetCurrentContext()) && eglSurface.equals(EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW)); } /** * Performs a simple surface query. */ public int querySurface(EGLSurface eglSurface, int what) { int[] value = new int[1]; EGL14.eglQuerySurface(mEGLDisplay, eglSurface, what, value, 0); return value[0]; } /** * Queries a string value. */ public String queryString(int what) { return EGL14.eglQueryString(mEGLDisplay, what); } /** * Returns the GLES version this context is configured for (currently 2 or 3). */ public int getGlVersion() { return mGlVersion; } /** * Writes the current display, context, and surface to the log. */ public static void logCurrent(String msg) { EGLDisplay display; EGLContext context; EGLSurface surface; display = EGL14.eglGetCurrentDisplay(); context = EGL14.eglGetCurrentContext(); surface = EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW); Log.i(TAG, "Current EGL (" + msg + "): display=" + display + ", context=" + context + ", surface=" + surface); } /** * Checks for EGL errors. Throws an exception if an error has been raised. */ private void checkEglError(String msg) { int error; if ((error = EGL14.eglGetError()) != EGL14.EGL_SUCCESS) { throw new RuntimeException(msg + ": EGL error: 0x" + Integer.toHexString(error)); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/misc/gles/EglSurfaceBase.java ================================================ // SPDX-License-Identifier: Apache-2.0 package io.github.muntashirakon.AppManager.misc.gles; import android.graphics.Bitmap; import android.opengl.EGL14; import android.opengl.EGLSurface; import android.opengl.GLES20; import android.util.Log; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; /** * Common base class for EGL surfaces. *

* There can be multiple surfaces associated with a single context. */ // Copyright 2014 Google Inc. public class EglSurfaceBase { protected static final String TAG = EglSurfaceBase.class.getSimpleName(); // EglCore object we're associated with. It may be associated with multiple surfaces. protected EglCore mEglCore; private EGLSurface mEGLSurface = EGL14.EGL_NO_SURFACE; private int mWidth = -1; private int mHeight = -1; protected EglSurfaceBase(EglCore eglCore) { mEglCore = eglCore; } /** * Creates a window surface. *

* * @param surface May be a Surface or SurfaceTexture. */ public void createWindowSurface(Object surface) { if (mEGLSurface != EGL14.EGL_NO_SURFACE) { throw new IllegalStateException("surface already created"); } mEGLSurface = mEglCore.createWindowSurface(surface); // Don't cache width/height here, because the size of the underlying surface can change // out from under us (see e.g. HardwareScalerActivity). //mWidth = mEglCore.querySurface(mEGLSurface, EGL14.EGL_WIDTH); //mHeight = mEglCore.querySurface(mEGLSurface, EGL14.EGL_HEIGHT); } /** * Creates an off-screen surface. */ public void createOffscreenSurface(int width, int height) { if (mEGLSurface != EGL14.EGL_NO_SURFACE) { throw new IllegalStateException("surface already created"); } mEGLSurface = mEglCore.createOffscreenSurface(width, height); mWidth = width; mHeight = height; } /** * Returns the surface's width, in pixels. *

* If this is called on a window surface, and the underlying surface is in the process * of changing size, we may not see the new size right away (e.g. in the "surfaceChanged" * callback). The size should match after the next buffer swap. */ public int getWidth() { if (mWidth < 0) { return mEglCore.querySurface(mEGLSurface, EGL14.EGL_WIDTH); } else { return mWidth; } } /** * Returns the surface's height, in pixels. */ public int getHeight() { if (mHeight < 0) { return mEglCore.querySurface(mEGLSurface, EGL14.EGL_HEIGHT); } else { return mHeight; } } /** * Release the EGL surface. */ public void releaseEglSurface() { mEglCore.releaseSurface(mEGLSurface); mEGLSurface = EGL14.EGL_NO_SURFACE; mWidth = mHeight = -1; } /** * Makes our EGL context and surface current. */ public void makeCurrent() { mEglCore.makeCurrent(mEGLSurface); } /** * Makes our EGL context and surface current for drawing, using the supplied surface * for reading. */ public void makeCurrentReadFrom(EglSurfaceBase readSurface) { mEglCore.makeCurrent(mEGLSurface, readSurface.mEGLSurface); } /** * Calls eglSwapBuffers. Use this to "publish" the current frame. * * @return false on failure */ public boolean swapBuffers() { boolean result = mEglCore.swapBuffers(mEGLSurface); if (!result) { Log.d(TAG, "WARNING: swapBuffers() failed"); } return result; } /** * Sends the presentation time stamp to EGL. * * @param nsecs Timestamp, in nanoseconds. */ public void setPresentationTime(long nsecs) { mEglCore.setPresentationTime(mEGLSurface, nsecs); } /** * Saves the EGL surface to a file. *

* Expects that this object's EGL surface is current. */ public void saveFrame(File file) throws IOException { if (!mEglCore.isCurrent(mEGLSurface)) { throw new RuntimeException("Expected EGL context/surface is not current"); } // glReadPixels fills in a "direct" ByteBuffer with what is essentially big-endian RGBA // data (i.e. a byte of red, followed by a byte of green...). While the Bitmap // constructor that takes an int[] wants little-endian ARGB (blue/red swapped), the // Bitmap "copy pixels" method wants the same format GL provides. // // Ideally we'd have some way to re-use the ByteBuffer, especially if we're calling // here often. // // Making this even more interesting is the upside-down nature of GL, which means // our output will look upside down relative to what appears on screen if the // typical GL conventions are used. String filename = file.toString(); int width = getWidth(); int height = getHeight(); ByteBuffer buf = ByteBuffer.allocateDirect(width * height * 4); buf.order(ByteOrder.LITTLE_ENDIAN); GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buf); GlUtil.checkGlError("glReadPixels"); buf.rewind(); BufferedOutputStream bos = null; try { bos = new BufferedOutputStream(new FileOutputStream(filename)); Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); bmp.copyPixelsFromBuffer(buf); bmp.compress(Bitmap.CompressFormat.PNG, 90, bos); bmp.recycle(); } finally { if (bos != null) bos.close(); } Log.d(TAG, "Saved " + width + "x" + height + " frame as '" + filename + "'"); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/misc/gles/GlUtil.java ================================================ // SPDX-License-Identifier: Apache-2.0 package io.github.muntashirakon.AppManager.misc.gles; import android.opengl.GLES20; import android.util.Log; /** * Some OpenGL utility functions. */ // Copyright 2014 Google Inc. public final class GlUtil { public static final String TAG = GlUtil.class.getSimpleName(); private GlUtil() {} // do not instantiate /** * Checks to see if a GLES error has been raised. */ public static void checkGlError(String op) { int error = GLES20.glGetError(); if (error != GLES20.GL_NO_ERROR) { String msg = op + ": glError 0x" + Integer.toHexString(error); Log.e(TAG, msg); throw new RuntimeException(msg); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/misc/gles/OffscreenSurface.java ================================================ // SPDX-License-Identifier: Apache-2.0 package io.github.muntashirakon.AppManager.misc.gles; /** * Off-screen EGL surface (pbuffer). *

* It's good practice to explicitly release() the surface, preferably from a "finally" block. */ // Copyright 2014 Google Inc. public class OffscreenSurface extends EglSurfaceBase { /** * Creates an off-screen surface with the specified width and height. */ public OffscreenSurface(EglCore eglCore, int width, int height) { super(eglCore); createOffscreenSurface(width, height); } /** * Releases any resources associated with the surface. */ public void release() { releaseEglSurface(); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/misc/gles/package.html ================================================ Source: https://github.com/google/grafika ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/miui/MiuiVersionInfo.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.miui; import androidx.annotation.NonNull; import androidx.annotation.Nullable; // https://www.xiaomist.com/2020/08/what-do-letters-and-numbers-that.html public class MiuiVersionInfo { @NonNull public final String version; @Nullable public final String letters; public final boolean isBeta; public MiuiVersionInfo(@NonNull String version, @Nullable String letters, boolean isBeta) { this.version = version; this.letters = letters; this.isBeta = isBeta || letters == null; } @NonNull public String getVersion() { return version; } @Nullable public String getMiuiVersion() { if (isBeta) { return null; } String[] splits = version.split("\\."); return splits[0] + '.' + splits[1]; } @Nullable public String getRomVersion() { if (isBeta) { return null; } String[] splits = version.split("\\."); return splits[2] + '.' + splits[3]; } @Nullable public String getAndroidVersionCodeName() { if (letters == null) { return null; } String[] splits = letters.split("\\."); return splits[0]; } @Nullable public String getTargetDevice() { if (letters == null) { return null; } String[] splits = letters.split("\\."); return splits[1] + splits[2]; } @Nullable public String getRegion() { if (letters == null) { return null; } String[] splits = letters.split("\\."); return splits[3] + splits[4]; } @Nullable public String getOrigin() { if (letters == null) { return null; } String[] splits = letters.split("\\."); return splits[5] + splits[6]; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/oneclickops/AppOpCount.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.oneclickops; import java.util.Collection; public class AppOpCount extends ItemCount { public Collection appOps; } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/oneclickops/BackupTasksDialogFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.oneclickops; import android.app.Dialog; import android.os.Bundle; import android.os.PowerManager; import android.text.SpannableStringBuilder; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.fragment.app.DialogFragment; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.io.IOException; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.Future; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.backup.BackupManager; import io.github.muntashirakon.AppManager.backup.dialog.BackupRestoreDialogFragment; import io.github.muntashirakon.AppManager.db.entity.Backup; import io.github.muntashirakon.AppManager.main.ApplicationItem; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.settings.FeatureController; import io.github.muntashirakon.AppManager.usage.AppUsageStatsManager; import io.github.muntashirakon.AppManager.usage.TimeInterval; import io.github.muntashirakon.AppManager.utils.CpuUtils; import io.github.muntashirakon.AppManager.utils.LangUtils; import io.github.muntashirakon.AppManager.utils.PackageUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.dialog.SearchableMultiChoiceDialogBuilder; import io.github.muntashirakon.io.DirectoryUtils; import io.github.muntashirakon.io.Paths; public class BackupTasksDialogFragment extends DialogFragment { public static final String TAG = "BackupTasksDialogFragment"; private OneClickOpsActivity mActivity; private Future mFuture; @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { mActivity = (OneClickOpsActivity) requireActivity(); View view = View.inflate(requireContext(), R.layout.dialog_backup_tasks, null); // Backup all installed apps view.findViewById(R.id.backup_all).setOnClickListener(v -> { mActivity.progressIndicator.show(); if (mFuture != null) { mFuture.cancel(true); } WeakReference wakeLockRef = new WeakReference<>(mActivity.wakeLock); mFuture = ThreadUtils.postOnBackgroundThread(() -> { PowerManager.WakeLock wakeLock = wakeLockRef.get(); if (wakeLock != null && !wakeLock.isHeld()) { wakeLock.acquire(); } try { List applicationItems = new ArrayList<>(); List applicationLabels = new ArrayList<>(); for (ApplicationItem item : PackageUtils.getInstalledOrBackedUpApplicationsFromDb(requireContext(), false, true)) { if (ThreadUtils.isInterrupted()) return; if (item.isInstalled) { applicationItems.add(item); applicationLabels.add(item.label); } } if (ThreadUtils.isInterrupted()) return; ThreadUtils.postOnMainThread(() -> runMultiChoiceDialog(applicationItems, applicationLabels)); } finally { CpuUtils.releaseWakeLock(wakeLock); } }); }); // Redo existing backups for the installed apps view.findViewById(R.id.redo_existing_backups).setOnClickListener(v -> { mActivity.progressIndicator.show(); if (mFuture != null) { mFuture.cancel(true); } WeakReference wakeLockRef = new WeakReference<>(mActivity.wakeLock); mFuture = ThreadUtils.postOnBackgroundThread(() -> { PowerManager.WakeLock wakeLock = wakeLockRef.get(); if (wakeLock != null && !wakeLock.isHeld()) { wakeLock.acquire(); } try { List applicationItems = new ArrayList<>(); List applicationLabels = new ArrayList<>(); for (ApplicationItem item : PackageUtils.getInstalledOrBackedUpApplicationsFromDb(requireContext(), false, true)) { if (ThreadUtils.isInterrupted()) return; if (item.isInstalled && item.backup != null) { applicationItems.add(item); applicationLabels.add(item.label); } } if (ThreadUtils.isInterrupted()) return; ThreadUtils.postOnMainThread(() -> runMultiChoiceDialog(applicationItems, applicationLabels)); } finally { CpuUtils.releaseWakeLock(wakeLock); } }); }); // Backup apps without any previous backups view.findViewById(R.id.backup_apps_without_backup).setOnClickListener(v -> { mActivity.progressIndicator.show(); if (mFuture != null) { mFuture.cancel(true); } WeakReference wakeLockRef = new WeakReference<>(mActivity.wakeLock); mFuture = ThreadUtils.postOnBackgroundThread(() -> { PowerManager.WakeLock wakeLock = wakeLockRef.get(); if (wakeLock != null && !wakeLock.isHeld()) { wakeLock.acquire(); } try { List applicationItems = new ArrayList<>(); List applicationLabels = new ArrayList<>(); for (ApplicationItem item : PackageUtils.getInstalledOrBackedUpApplicationsFromDb(requireContext(), false, true)) { if (ThreadUtils.isInterrupted()) return; if (item.isInstalled && item.backup == null) { applicationItems.add(item); applicationLabels.add(item.label); } } if (ThreadUtils.isInterrupted()) return; ThreadUtils.postOnMainThread(() -> runMultiChoiceDialog(applicationItems, applicationLabels)); } finally { CpuUtils.releaseWakeLock(wakeLock); } }); }); view.findViewById(R.id.verify_and_redo_backups).setOnClickListener(v -> { mActivity.progressIndicator.show(); if (mFuture != null) { mFuture.cancel(true); } WeakReference wakeLockRef = new WeakReference<>(mActivity.wakeLock); mFuture = ThreadUtils.postOnBackgroundThread(() -> { PowerManager.WakeLock wakeLock = wakeLockRef.get(); if (wakeLock != null && !wakeLock.isHeld()) { wakeLock.acquire(); } try { List applicationItems = new ArrayList<>(); List applicationLabels = new ArrayList<>(); Backup backup; BackupManager backupManager = new BackupManager(); for (ApplicationItem item : PackageUtils.getInstalledOrBackedUpApplicationsFromDb(requireContext(), false, true)) { if (ThreadUtils.isInterrupted()) return; backup = item.backup; if (backup == null || !item.isInstalled) continue; try { backupManager.verify(backup.relativeDir); } catch (Throwable e) { applicationItems.add(item); applicationLabels.add(new SpannableStringBuilder(backup.label) .append(LangUtils.getSeparatorString()) .append(backup.backupName) .append('\n') .append(UIUtils.getSmallerText(UIUtils.getSecondaryText(mActivity, new SpannableStringBuilder(backup.packageName) .append('\n') .append(e.getMessage()))))); } } if (ThreadUtils.isInterrupted()) return; ThreadUtils.postOnMainThread(() -> runMultiChoiceDialog(applicationItems, applicationLabels)); } finally { CpuUtils.releaseWakeLock(wakeLock); } }); }); view.findViewById(R.id.backup_apps_with_changes).setOnClickListener(v -> { mActivity.progressIndicator.show(); if (mFuture != null) { mFuture.cancel(true); } WeakReference wakeLockRef = new WeakReference<>(mActivity.wakeLock); mFuture = ThreadUtils.postOnBackgroundThread(() -> { PowerManager.WakeLock wakeLock = wakeLockRef.get(); if (wakeLock != null && !wakeLock.isHeld()) { wakeLock.acquire(); } try { List applicationItems = new ArrayList<>(); List applicationLabels = new ArrayList<>(); Set ignoredDirs = new HashSet<>(); ignoredDirs.add("cache"); ignoredDirs.add("code_cache"); ignoredDirs.add("no_backup"); boolean hasUsageAccess = FeatureController.isUsageAccessEnabled() && SelfPermissions.checkUsageStatsPermission(); BackupManager backupManager = new BackupManager(); Backup backup; for (ApplicationItem item : PackageUtils.getInstalledOrBackedUpApplicationsFromDb(requireContext(), false, true)) { if (ThreadUtils.isInterrupted()) return; backup = item.backup; if (backup == null) continue; // Checks // 0. App is installed (Skip backup) if (!item.isInstalled) continue; // 1. App version code and 2. last update date (Whether to back up source) boolean needSourceUpdate = item.versionCode > backup.versionCode || item.lastUpdateTime > backup.backupTime; if (needSourceUpdate // 3. Last activity date || (hasUsageAccess && AppUsageStatsManager.getLastActivityTime(item.packageName, new TimeInterval(backup.backupTime, System.currentTimeMillis())) > backup.backupTime) // 4. Check directory change || isDataDirectoryChanged(backup, ignoredDirs) // 5. Check integrity || !isVerified(backupManager, backup)) { applicationItems.add(item); applicationLabels.add(new SpannableStringBuilder().append(backup.label) .append(LangUtils.getSeparatorString()) .append(backup.backupName) .append('\n') .append(UIUtils.getSmallerText(UIUtils.getSecondaryText(mActivity, backup.packageName)))); } } if (ThreadUtils.isInterrupted()) return; ThreadUtils.postOnMainThread(() -> runMultiChoiceDialog(applicationItems, applicationLabels)); } finally { CpuUtils.releaseWakeLock(wakeLock); } }); }); return new MaterialAlertDialogBuilder(mActivity) .setView(view) .setTitle(R.string.back_up) .setNegativeButton(R.string.cancel, null) .create(); } @UiThread private void runMultiChoiceDialog(List applicationItems, List applicationLabels) { if (isDetached()) return; mActivity.progressIndicator.hide(); new SearchableMultiChoiceDialogBuilder<>(mActivity, applicationItems, applicationLabels) .addSelections(applicationItems) .setTitle(R.string.filtered_packages) .setPositiveButton(R.string.back_up, (dialog, which, selectedItems) -> { if (isDetached()) return; BackupRestoreDialogFragment fragment = BackupRestoreDialogFragment.getInstance( PackageUtils.getUserPackagePairs(selectedItems), BackupRestoreDialogFragment.MODE_BACKUP); fragment.setOnActionBeginListener(mode -> mActivity.progressIndicator.show()); fragment.setOnActionCompleteListener((mode, failedPackages) -> mActivity.progressIndicator.hide()); if (isDetached()) return; fragment.show(getParentFragmentManager(), BackupRestoreDialogFragment.TAG); }) .setNegativeButton(R.string.cancel, null) .show(); } @Override public void onDestroy() { if (mFuture != null) { mFuture.cancel(true); } super.onDestroy(); } private boolean isVerified(@NonNull BackupManager backupManager, @NonNull Backup backup) { try { backupManager.verify(backup.relativeDir); return true; } catch (Throwable ignore) { return false; } } private static boolean isDataDirectoryChanged(@NonNull Backup backup, Set ignoredDirs) { try { for (String dir : backup.getItem().getMetadata().metadata.dataDirs) { if (DirectoryUtils.isDirectoryChanged(Paths.get(dir), backup.backupTime, 3, ignoredDirs)) { return true; } } } catch (IOException ignore) { } return false; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/oneclickops/ClearCacheAppWidget.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.oneclickops; import android.app.PendingIntent; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProvider; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.widget.RemoteViews; import androidx.core.app.PendingIntentCompat; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.batchops.BatchOpsManager; public class ClearCacheAppWidget extends AppWidgetProvider { static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) { // Construct the RemoteViews object RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.app_widget_clear_cache); Intent intent = new Intent(context, OneClickOpsActivity.class); intent.putExtra(OneClickOpsActivity.EXTRA_OP, BatchOpsManager.OP_CLEAR_CACHE); views.setOnClickPendingIntent(android.R.id.background, PendingIntentCompat.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT, false)); // Instruct the widget manager to update the widget appWidgetManager.updateAppWidget(appWidgetId, views); } @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { // There may be multiple widgets active, so update all of them for (int appWidgetId : appWidgetIds) { updateAppWidget(context, appWidgetManager, appWidgetId); } } @Override public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions) { updateAppWidget(context, appWidgetManager, appWidgetId); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/oneclickops/ItemCount.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.oneclickops; public class ItemCount { public String packageName; public String packageLabel; public int count; } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/oneclickops/OneClickOpsActivity.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.oneclickops; import static io.github.muntashirakon.AppManager.utils.PackageUtils.getAppOpModeNames; import static io.github.muntashirakon.AppManager.utils.PackageUtils.getAppOpNames; import static io.github.muntashirakon.AppManager.utils.UIUtils.getSmallerText; import android.Manifest; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Build; import android.os.Bundle; import android.os.PowerManager; import android.text.SpannableStringBuilder; import android.view.MenuItem; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.collection.ArraySet; import androidx.core.content.ContextCompat; import androidx.lifecycle.ViewModelProvider; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.progressindicator.LinearProgressIndicator; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Set; import io.github.muntashirakon.AppManager.BaseActivity; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.apk.dexopt.DexOptDialog; import io.github.muntashirakon.AppManager.batchops.BatchOpsManager; import io.github.muntashirakon.AppManager.batchops.BatchOpsService; import io.github.muntashirakon.AppManager.batchops.BatchQueueItem; import io.github.muntashirakon.AppManager.batchops.struct.BatchAppOpsOptions; import io.github.muntashirakon.AppManager.batchops.struct.BatchComponentOptions; import io.github.muntashirakon.AppManager.compat.AppOpsManagerCompat; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.utils.ArrayUtils; import io.github.muntashirakon.AppManager.utils.CpuUtils; import io.github.muntashirakon.AppManager.utils.ListItemCreator; import io.github.muntashirakon.AppManager.utils.TextUtilsCompat; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.AppManager.utils.Utils; import io.github.muntashirakon.dialog.SearchableMultiChoiceDialogBuilder; import io.github.muntashirakon.dialog.TextInputDialogBuilder; import io.github.muntashirakon.dialog.TextInputDropdownDialogBuilder; public class OneClickOpsActivity extends BaseActivity { public static final String EXTRA_OP = "op"; LinearProgressIndicator progressIndicator; PowerManager.WakeLock wakeLock; private OneClickOpsViewModel mViewModel; private ListItemCreator mItemCreator; private final BroadcastReceiver mBatchOpsBroadCastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { progressIndicator.hide(); } }; @Override protected void onAuthenticated(Bundle savedInstanceState) { int op = getIntent().getIntExtra(EXTRA_OP, BatchOpsManager.OP_NONE); if (op == BatchOpsManager.OP_CLEAR_CACHE) { BatchQueueItem item = BatchQueueItem.getOneClickQueue(BatchOpsManager.OP_CLEAR_CACHE, null, null, null); launchService(item); finishAndRemoveTask(); return; } setContentView(R.layout.activity_one_click_ops); setSupportActionBar(findViewById(R.id.toolbar)); mViewModel = new ViewModelProvider(this).get(OneClickOpsViewModel.class); mItemCreator = new ListItemCreator(this, R.id.container); progressIndicator = findViewById(R.id.progress_linear); progressIndicator.setVisibilityAfterHide(View.GONE); wakeLock = CpuUtils.getPartialWakeLock("1-click_ops"); setItems(); // Watch LiveData mViewModel.watchTrackerCount().observe(this, this::blockTrackers); mViewModel.watchComponentCount().observe(this, listPair -> blockComponents(listPair.first, listPair.second)); mViewModel.watchAppOpsCount().observe(this, listPairPair -> setAppOps(listPairPair.first, listPairPair.second.first, listPairPair.second.second)); mViewModel.getClearDataCandidates().observe(this, this::clearData); mViewModel.watchTrimCachesResult().observe(this, isSuccessful -> { CpuUtils.releaseWakeLock(wakeLock); progressIndicator.hide(); UIUtils.displayShortToast(isSuccessful ? R.string.done : R.string.failed); }); mViewModel.getAppsInstalledByAmForDexOpt().observe(this, packages -> { CpuUtils.releaseWakeLock(wakeLock); progressIndicator.hide(); DexOptDialog dialog = DexOptDialog.getInstance(packages); dialog.show(getSupportFragmentManager(), DexOptDialog.TAG); }); } private void setItems() { mItemCreator.addItemWithTitleSubtitle(getString(R.string.block_unblock_trackers), getString(R.string.block_unblock_trackers_description)) .setOnClickListener(v -> new MaterialAlertDialogBuilder(this) .setTitle(R.string.block_unblock_trackers) .setMessage(R.string.apply_to_system_apps_question) .setPositiveButton(R.string.no, (dialog, which) -> { progressIndicator.show(); if (!wakeLock.isHeld()) { wakeLock.acquire(); } mViewModel.blockTrackers(false); }) .setNegativeButton(R.string.yes, (dialog, which) -> { progressIndicator.show(); if (!wakeLock.isHeld()) { wakeLock.acquire(); } mViewModel.blockTrackers(true); }) .show()); mItemCreator.addItemWithTitleSubtitle(getString(R.string.block_unblock_components_dots), getString(R.string.block_unblock_components_description)) .setOnClickListener(v -> new TextInputDialogBuilder(this, R.string.input_signatures) .setHelperText(R.string.input_signatures_description) .setCheckboxLabel(R.string.apply_to_system_apps) .setTitle(R.string.block_unblock_components_dots) .setPositiveButton(R.string.search, (dialog, which, signatureNames, systemApps) -> { if (signatureNames == null) return; progressIndicator.show(); if (!wakeLock.isHeld()) { wakeLock.acquire(); } String[] signatures = signatureNames.toString().split("\\s+"); mViewModel.blockComponents(systemApps, signatures); }) .setNegativeButton(R.string.cancel, null) .show()); mItemCreator.addItemWithTitleSubtitle(getString(R.string.set_mode_for_app_ops_dots), getString(R.string.deny_app_ops_description)) .setOnClickListener(v -> showAppOpsSelectionDialog()); mItemCreator.addItemWithTitleSubtitle(getText(R.string.back_up), getText(R.string.backup_msg)).setOnClickListener(v -> new BackupTasksDialogFragment().show(getSupportFragmentManager(), BackupTasksDialogFragment.TAG)); mItemCreator.addItemWithTitleSubtitle(getText(R.string.restore), getText(R.string.restore_msg)).setOnClickListener(v -> new RestoreTasksDialogFragment().show(getSupportFragmentManager(), RestoreTasksDialogFragment.TAG)); mItemCreator.addItemWithTitleSubtitle(getString(R.string.clear_data_from_uninstalled_apps), getString(R.string.clear_data_from_uninstalled_apps_description)) .setOnClickListener(v -> { if (!wakeLock.isHeld()) { wakeLock.acquire(); } mViewModel.clearData(); }); // mItemCreator.addItemWithTitleSubtitle(getString(R.string.clear_app_cache), // getString(R.string.clear_app_cache_description)) // .setOnClickListener(v -> clearAppCache()); mItemCreator.addItemWithTitleSubtitle(getString(R.string.trim_caches_in_all_apps), getString(R.string.trim_caches_in_all_apps_description)) .setOnClickListener(v -> { if (!SelfPermissions.checkSelfPermission(Manifest.permission.CLEAR_APP_CACHE) && !SelfPermissions.checkSelfOrRemotePermission(Manifest.permission.CLEAR_APP_CACHE)) { UIUtils.displayShortToast(R.string.only_works_in_root_or_adb_mode); return; } new MaterialAlertDialogBuilder(this) .setTitle(R.string.trim_caches_in_all_apps) .setMessage(R.string.are_you_sure) .setNegativeButton(R.string.no, null) .setPositiveButton(R.string.yes, (dialog, which) -> { progressIndicator.show(); if (!wakeLock.isHeld()) { wakeLock.acquire(); } mViewModel.trimCaches(); }) .show(); }); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { mItemCreator.addItemWithTitleSubtitle(getString(R.string.title_perform_runtime_optimization_to_apps), getString(R.string.summary_perform_runtime_optimization_to_apps)) .setOnClickListener(v -> { if (SelfPermissions.isSystemOrRootOrShell()) { DexOptDialog dialog = DexOptDialog.getInstance(null); dialog.show(getSupportFragmentManager(), DexOptDialog.TAG); return; } progressIndicator.show(); if (!wakeLock.isHeld()) { wakeLock.acquire(); } mViewModel.listAppsInstalledByAmForDexOpt(); }); } progressIndicator.hide(); } @Override protected void onResume() { super.onResume(); ContextCompat.registerReceiver(this, mBatchOpsBroadCastReceiver, new IntentFilter(BatchOpsService.ACTION_BATCH_OPS_COMPLETED), ContextCompat.RECEIVER_NOT_EXPORTED); } @Override protected void onPause() { super.onPause(); unregisterReceiver(mBatchOpsBroadCastReceiver); if (progressIndicator != null) { progressIndicator.hide(); } } private void blockTrackers(@Nullable List trackerCounts) { CpuUtils.releaseWakeLock(wakeLock); progressIndicator.hide(); if (trackerCounts == null) { UIUtils.displayShortToast(R.string.failed_to_fetch_package_info); return; } if (trackerCounts.isEmpty()) { UIUtils.displayShortToast(R.string.no_tracker_found); return; } final ArrayList trackerPackages = new ArrayList<>(); final List trackerPackagesWithTrackerCount = new ArrayList<>(trackerCounts.size()); for (ItemCount tracker : trackerCounts) { trackerPackages.add(tracker.packageName); trackerPackagesWithTrackerCount.add(new SpannableStringBuilder(tracker.packageLabel) .append("\n").append(getSmallerText(getResources().getQuantityString(R.plurals.no_of_trackers, tracker.count, tracker.count)))); } new SearchableMultiChoiceDialogBuilder<>(this, trackerPackages, trackerPackagesWithTrackerCount) .addSelections(trackerPackages) .setTitle(R.string.filtered_packages) .setPositiveButton(R.string.block, (dialog, which, selectedPackages) -> { progressIndicator.show(); BatchQueueItem item = BatchQueueItem.getOneClickQueue(BatchOpsManager.OP_BLOCK_TRACKERS, selectedPackages, null, null); launchService(item); }) .setNeutralButton(R.string.unblock, (dialog, which, selectedPackages) -> { progressIndicator.show(); BatchQueueItem item = BatchQueueItem.getOneClickQueue(BatchOpsManager.OP_UNBLOCK_TRACKERS, selectedPackages, null, null); launchService(item); }) .setNegativeButton(R.string.cancel, null) .show(); } private void blockComponents(@Nullable List componentCounts, @NonNull String[] signatures) { CpuUtils.releaseWakeLock(wakeLock); progressIndicator.hide(); if (componentCounts == null) { UIUtils.displayShortToast(R.string.failed_to_fetch_package_info); return; } if (componentCounts.isEmpty()) { UIUtils.displayShortToast(R.string.no_matching_package_found); return; } SpannableStringBuilder builder; final ArrayList selectedPackages = new ArrayList<>(); List packageNamesWithComponentCount = new ArrayList<>(); for (ItemCount component : componentCounts) { builder = new SpannableStringBuilder(component.packageLabel) .append("\n").append(getSmallerText(getResources().getQuantityString(R.plurals.no_of_components, component.count, component.count))); selectedPackages.add(component.packageName); packageNamesWithComponentCount.add(builder); } BatchComponentOptions options = new BatchComponentOptions(signatures); new SearchableMultiChoiceDialogBuilder<>(this, selectedPackages, packageNamesWithComponentCount) .addSelections(selectedPackages) .setTitle(R.string.filtered_packages) .setPositiveButton(R.string.block, (dialog1, which1, selectedItems) -> { progressIndicator.show(); BatchQueueItem item = BatchQueueItem.getOneClickQueue(BatchOpsManager.OP_BLOCK_COMPONENTS, selectedItems, null, options); launchService(item); }) .setNeutralButton(R.string.unblock, (dialog1, which1, selectedItems) -> { progressIndicator.show(); BatchQueueItem item = BatchQueueItem.getOneClickQueue(BatchOpsManager.OP_UNBLOCK_COMPONENTS, selectedItems, null, options); launchService(item); }) .setNegativeButton(R.string.cancel, null) .show(); } private void showAppOpsSelectionDialog() { if (!SelfPermissions.canModifyAppOpMode()) { UIUtils.displayShortToast(R.string.only_works_in_root_or_adb_mode); return; } List modes = AppOpsManagerCompat.getModeConstants(); List appOps = AppOpsManagerCompat.getAllOps(); List modeNames = Arrays.asList(getAppOpModeNames(modes)); List appOpNames = Arrays.asList(getAppOpNames(appOps)); TextInputDropdownDialogBuilder builder = new TextInputDropdownDialogBuilder(this, R.string.input_app_ops); builder.setTitle(R.string.set_mode_for_app_ops_dots) .setAuxiliaryInput(R.string.mode, null, null, modeNames, true) .setCheckboxLabel(R.string.apply_to_system_apps) .setHelperText(R.string.input_app_ops_description) .setPositiveButton(R.string.search, (dialog, which, appOpNameList, systemApps) -> { if (appOpNameList == null) return; // Get mode int mode; int[] appOpList; try { String[] appOpsStr = appOpNameList.toString().split("\\s+"); if (appOpsStr.length == 0) return; mode = Utils.getIntegerFromString(builder.getAuxiliaryInput(), modeNames, modes); // User can unknowingly insert duplicate values for app ops Set appOpSet = new ArraySet<>(appOpsStr.length); for (String appOp : appOpsStr) { appOpSet.add(Utils.getIntegerFromString(appOp, appOpNames, appOps)); } appOpList = ArrayUtils.convertToIntArray(appOpSet); } catch (IllegalArgumentException e) { UIUtils.displayShortToast(R.string.failed_to_parse_some_numbers); return; } progressIndicator.show(); if (!wakeLock.isHeld()) { wakeLock.acquire(); } mViewModel.setAppOps(appOpList, mode, systemApps); }) .setNegativeButton(R.string.cancel, null) .show(); } private void setAppOps(@Nullable List appOpCounts, @NonNull int[] appOpList, int mode) { CpuUtils.releaseWakeLock(wakeLock); progressIndicator.hide(); if (appOpCounts == null) { UIUtils.displayShortToast(R.string.failed_to_fetch_package_info); return; } if (appOpCounts.isEmpty()) { UIUtils.displayShortToast(R.string.no_matching_package_found); return; } SpannableStringBuilder builder1; final ArrayList selectedPackages = new ArrayList<>(); List packagesWithAppOpCount = new ArrayList<>(); for (AppOpCount appOp : appOpCounts) { builder1 = new SpannableStringBuilder(appOp.packageLabel) .append("\n").append(getSmallerText("(" + appOp.count + ") " + TextUtilsCompat.joinSpannable(", ", appOpToNames(appOp.appOps)))); selectedPackages.add(appOp.packageName); packagesWithAppOpCount.add(builder1); } BatchAppOpsOptions options = new BatchAppOpsOptions(appOpList, mode); new SearchableMultiChoiceDialogBuilder<>(this, selectedPackages, packagesWithAppOpCount) .addSelections(selectedPackages) .setTitle(R.string.filtered_packages) .setPositiveButton(R.string.apply, (dialog1, which1, selectedItems) -> { progressIndicator.show(); BatchQueueItem item = BatchQueueItem.getOneClickQueue(BatchOpsManager.OP_SET_APP_OPS, selectedItems, null, options); launchService(item); }) .setNegativeButton(R.string.cancel, (dialog1, which1, selectedItems) -> progressIndicator.hide()) .show(); } private void clearData(@NonNull List candidatePackages) { CpuUtils.releaseWakeLock(wakeLock); if (candidatePackages.isEmpty()) { UIUtils.displayLongToast(R.string.no_matching_package_found); return; } String[] packages = candidatePackages.toArray(new String[0]); new SearchableMultiChoiceDialogBuilder<>(this, packages, packages) .setTitle(R.string.filtered_packages) .setPositiveButton(R.string.apply, (dialog1, which1, selectedItems) -> { progressIndicator.show(); BatchQueueItem item = BatchQueueItem.getOneClickQueue(BatchOpsManager.OP_UNINSTALL, selectedItems, null, null); launchService(item); }) .setNegativeButton(R.string.cancel, (dialog1, which1, selectedItems) -> progressIndicator.hide()) .show(); } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == android.R.id.home) { finish(); return true; } return super.onOptionsItemSelected(item); } @Override protected void onDestroy() { CpuUtils.releaseWakeLock(wakeLock); super.onDestroy(); } private void launchService(@NonNull BatchQueueItem queueItem) { Intent intent = BatchOpsService.getServiceIntent(this, queueItem); ContextCompat.startForegroundService(this, intent); } @NonNull private List appOpToNames(@NonNull Collection appOps) { List appOpNames = new ArrayList<>(appOps.size()); for (int appOp : appOps) { appOpNames.add(AppOpsManagerCompat.opToName(appOp)); } return appOpNames; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/oneclickops/OneClickOpsViewModel.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.oneclickops; import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.MATCH_DISABLED_COMPONENTS; import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.MATCH_UNINSTALLED_PACKAGES; import android.Manifest; import android.app.Application; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Build; import android.os.RemoteException; import android.os.UserHandleHidden; import androidx.annotation.AnyThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.util.Pair; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.concurrent.Future; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.compat.ApplicationInfoCompat; import io.github.muntashirakon.AppManager.compat.ManifestCompat; import io.github.muntashirakon.AppManager.compat.PackageManagerCompat; import io.github.muntashirakon.AppManager.compat.StorageManagerCompat; import io.github.muntashirakon.AppManager.rules.compontents.ComponentUtils; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.settings.Ops; import io.github.muntashirakon.AppManager.users.Users; import io.github.muntashirakon.AppManager.utils.PackageUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; public class OneClickOpsViewModel extends AndroidViewModel { public static final String TAG = OneClickOpsViewModel.class.getSimpleName(); private final PackageManager mPm; private final MutableLiveData> mTrackerCount = new MutableLiveData<>(); private final MutableLiveData, String[]>> mComponentCount = new MutableLiveData<>(); private final MutableLiveData, Pair>> mAppOpsCount = new MutableLiveData<>(); private final MutableLiveData> mClearDataCandidates = new MutableLiveData<>(); private final MutableLiveData mTrimCachesResult = new MutableLiveData<>(); private final MutableLiveData mAppsInstalledByAmForDexOpt = new MutableLiveData<>(); @Nullable private Future mFutureResult; public OneClickOpsViewModel(@NonNull Application application) { super(application); mPm = application.getPackageManager(); } @Override protected void onCleared() { if (mFutureResult != null) { mFutureResult.cancel(true); } super.onCleared(); } public LiveData> watchTrackerCount() { return mTrackerCount; } public LiveData, String[]>> watchComponentCount() { return mComponentCount; } public LiveData, Pair>> watchAppOpsCount() { return mAppOpsCount; } public LiveData> getClearDataCandidates() { return mClearDataCandidates; } public LiveData watchTrimCachesResult() { return mTrimCachesResult; } public MutableLiveData getAppsInstalledByAmForDexOpt() { return mAppsInstalledByAmForDexOpt; } @AnyThread public void blockTrackers(boolean systemApps) { if (mFutureResult != null) { mFutureResult.cancel(true); } mFutureResult = ThreadUtils.postOnBackgroundThread(() -> { int flags = PackageManager.GET_ACTIVITIES | PackageManager.GET_RECEIVERS | PackageManager.GET_PROVIDERS | PackageManager.GET_SERVICES | MATCH_DISABLED_COMPONENTS | MATCH_UNINSTALLED_PACKAGES | PackageManagerCompat.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES; boolean canChangeComponentState = SelfPermissions.checkSelfOrRemotePermission(Manifest.permission.CHANGE_COMPONENT_ENABLED_STATE); if (!canChangeComponentState) { // Since there's no permission, it can only change its own component states try { PackageInfo packageInfo = getApplication().getPackageManager().getPackageInfo(BuildConfig.APPLICATION_ID, flags); if (systemApps || !ApplicationInfoCompat.isSystemApp(packageInfo.applicationInfo)) { ItemCount trackerCount = getTrackerCountForApp(packageInfo); if (trackerCount.count > 0) { mTrackerCount.postValue(Collections.singletonList(trackerCount)); return; } } } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); } mTrackerCount.postValue(Collections.emptyList()); return; } boolean crossUserPermission = SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.INTERACT_ACROSS_USERS) || SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.INTERACT_ACROSS_USERS_FULL); boolean isShell = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && Users.getSelfOrRemoteUid() == Ops.SHELL_UID; List trackerCounts = new ArrayList<>(); HashSet packageNames = new HashSet<>(); for (PackageInfo packageInfo : PackageUtils.getAllPackages(flags, !crossUserPermission)) { if (packageNames.contains(packageInfo.packageName)) { continue; } packageNames.add(packageInfo.packageName); if (ThreadUtils.isInterrupted()) return; ApplicationInfo applicationInfo = packageInfo.applicationInfo; if (isShell && !ApplicationInfoCompat.isTestOnly(applicationInfo)) { continue; } if (!systemApps && ApplicationInfoCompat.isSystemApp(applicationInfo)) { continue; } ItemCount trackerCount = getTrackerCountForApp(packageInfo); if (trackerCount.count > 0) { trackerCounts.add(trackerCount); } } mTrackerCount.postValue(trackerCounts); }); } @AnyThread public void blockComponents(boolean systemApps, @NonNull String[] signatures) { if (signatures.length == 0) return; if (mFutureResult != null) { mFutureResult.cancel(true); } mFutureResult = ThreadUtils.postOnBackgroundThread(() -> { boolean canChangeComponentState = SelfPermissions.checkSelfOrRemotePermission(Manifest.permission.CHANGE_COMPONENT_ENABLED_STATE); if (!canChangeComponentState) { // Since there's no permission, it can only change its own component states ApplicationInfo applicationInfo = getApplication().getApplicationInfo(); if (systemApps || !ApplicationInfoCompat.isSystemApp(applicationInfo)) { ItemCount componentCount = new ItemCount(); componentCount.packageName = applicationInfo.packageName; componentCount.packageLabel = applicationInfo.loadLabel(mPm).toString(); componentCount.count = PackageUtils.getFilteredComponents(applicationInfo.packageName, UserHandleHidden.myUserId(), signatures).size(); if (componentCount.count > 0) { mComponentCount.postValue(new Pair<>(Collections.singletonList(componentCount), signatures)); return; } } mComponentCount.postValue(new Pair<>(Collections.emptyList(), signatures)); return; } boolean crossUserPermission = SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.INTERACT_ACROSS_USERS) || SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.INTERACT_ACROSS_USERS_FULL); boolean isShell = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && Users.getSelfOrRemoteUid() == Ops.SHELL_UID; List componentCounts = new ArrayList<>(); HashSet packageNames = new HashSet<>(); for (ApplicationInfo applicationInfo : PackageUtils.getAllApplications(MATCH_UNINSTALLED_PACKAGES | PackageManagerCompat.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES, !crossUserPermission)) { if (packageNames.contains(applicationInfo.packageName)) { continue; } packageNames.add(applicationInfo.packageName); if (ThreadUtils.isInterrupted()) return; if (isShell && !ApplicationInfoCompat.isTestOnly(applicationInfo)) continue; if (!systemApps && (applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) continue; ItemCount componentCount = new ItemCount(); componentCount.packageName = applicationInfo.packageName; componentCount.packageLabel = applicationInfo.loadLabel(mPm).toString(); componentCount.count = PackageUtils.getFilteredComponents(applicationInfo.packageName, UserHandleHidden.myUserId(), signatures).size(); if (componentCount.count > 0) componentCounts.add(componentCount); } mComponentCount.postValue(new Pair<>(componentCounts, signatures)); }); } @AnyThread public void setAppOps(int[] appOpList, int mode, boolean systemApps) { if (mFutureResult != null) { mFutureResult.cancel(true); } mFutureResult = ThreadUtils.postOnBackgroundThread(() -> { Pair appOpsModePair = new Pair<>(appOpList, mode); List appOpCounts = new ArrayList<>(); HashSet packageNames = new HashSet<>(); for (ApplicationInfo applicationInfo : PackageUtils.getAllApplications(MATCH_UNINSTALLED_PACKAGES | PackageManagerCompat.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES)) { if (packageNames.contains(applicationInfo.packageName)) { continue; } packageNames.add(applicationInfo.packageName); if (ThreadUtils.isInterrupted()) return; if (!systemApps && (applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) continue; AppOpCount appOpCount = new AppOpCount(); appOpCount.packageName = applicationInfo.packageName; appOpCount.packageLabel = applicationInfo.loadLabel(mPm).toString(); appOpCount.appOps = PackageUtils.getFilteredAppOps(applicationInfo.packageName, UserHandleHidden.myUserId(), appOpList, mode); appOpCount.count = appOpCount.appOps.size(); if (appOpCount.count > 0) appOpCounts.add(appOpCount); } mAppOpsCount.postValue(new Pair<>(appOpCounts, appOpsModePair)); }); } public void clearData() { if (mFutureResult != null) { mFutureResult.cancel(true); } mFutureResult = ThreadUtils.postOnBackgroundThread(() -> { HashSet packageNames = new HashSet<>(); for (ApplicationInfo applicationInfo : PackageUtils.getAllApplications(MATCH_UNINSTALLED_PACKAGES | PackageManagerCompat.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES)) { if (packageNames.contains(applicationInfo.packageName)) { continue; } if (ApplicationInfoCompat.isOnlyDataInstalled(applicationInfo)) { packageNames.add(applicationInfo.packageName); } if (ThreadUtils.isInterrupted()) { return; } } mClearDataCandidates.postValue(new ArrayList<>(packageNames)); }); } @AnyThread public void trimCaches() { ThreadUtils.postOnBackgroundThread(() -> { long size = 1024L * 1024L * 1024L * 1024L; // 1 TB try { // TODO: 30/8/21 Iterate all volumes? PackageManagerCompat.freeStorageAndNotify(null /* internal */, size, StorageManagerCompat.FLAG_ALLOCATE_DEFY_ALL_RESERVED); mTrimCachesResult.postValue(true); } catch (RemoteException e) { mTrimCachesResult.postValue(false); } }); } public void listAppsInstalledByAmForDexOpt() { ThreadUtils.postOnBackgroundThread(() -> { HashSet packageNames = new HashSet<>(); for (ApplicationInfo applicationInfo : PackageUtils.getAllApplications(0)) { if (packageNames.contains(applicationInfo.packageName)) { continue; } packageNames.add(applicationInfo.packageName); if (ThreadUtils.isInterrupted()) { return; } } mAppsInstalledByAmForDexOpt.postValue(packageNames.toArray(new String[0])); }); } @NonNull private ItemCount getTrackerCountForApp(@NonNull PackageInfo packageInfo) { ItemCount trackerCount = new ItemCount(); trackerCount.packageName = packageInfo.packageName; trackerCount.packageLabel = packageInfo.applicationInfo.loadLabel(mPm).toString(); trackerCount.count = ComponentUtils.getTrackerComponentsCountForPackage(packageInfo); return trackerCount; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/oneclickops/RestoreTasksDialogFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.oneclickops; import android.app.Dialog; import android.os.Bundle; import android.os.PowerManager; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.fragment.app.DialogFragment; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Future; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.backup.dialog.BackupRestoreDialogFragment; import io.github.muntashirakon.AppManager.main.ApplicationItem; import io.github.muntashirakon.AppManager.utils.CpuUtils; import io.github.muntashirakon.AppManager.utils.PackageUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.dialog.SearchableMultiChoiceDialogBuilder; public class RestoreTasksDialogFragment extends DialogFragment { public static final String TAG = "RestoreTasksDialogFragment"; private OneClickOpsActivity mActivity; private Future mFuture; @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { mActivity = (OneClickOpsActivity) requireActivity(); View view = View.inflate(mActivity, R.layout.dialog_restore_tasks, null); // Restore all apps view.findViewById(R.id.restore_all).setOnClickListener(v -> { mActivity.progressIndicator.show(); if (mFuture != null) { mFuture.cancel(true); } WeakReference wakeLockRef = new WeakReference<>(mActivity.wakeLock); mFuture = ThreadUtils.postOnBackgroundThread(() -> { PowerManager.WakeLock wakeLock = wakeLockRef.get(); if (wakeLock != null && !wakeLock.isHeld()) { wakeLock.acquire(); } try { List applicationItems = new ArrayList<>(); List applicationLabels = new ArrayList<>(); for (ApplicationItem item : PackageUtils.getInstalledOrBackedUpApplicationsFromDb(requireContext(), false, true)) { if (ThreadUtils.isInterrupted()) return; if (item.backup != null) { applicationItems.add(item); applicationLabels.add(item.label); } } if (ThreadUtils.isInterrupted()) return; ThreadUtils.postOnMainThread(() -> runMultiChoiceDialog(applicationItems, applicationLabels)); } finally { CpuUtils.releaseWakeLock(wakeLock); } }); }); // Restore not installed view.findViewById(R.id.restore_not_installed).setOnClickListener(v -> { mActivity.progressIndicator.show(); if (mFuture != null) { mFuture.cancel(true); } WeakReference wakeLockRef = new WeakReference<>(mActivity.wakeLock); mFuture = ThreadUtils.postOnBackgroundThread(() -> { PowerManager.WakeLock wakeLock = wakeLockRef.get(); if (wakeLock != null && !wakeLock.isHeld()) { wakeLock.acquire(); } try { List applicationItems = new ArrayList<>(); List applicationLabels = new ArrayList<>(); for (ApplicationItem item : PackageUtils.getInstalledOrBackedUpApplicationsFromDb(requireContext(), false, true)) { if (ThreadUtils.isInterrupted()) return; if (!item.isInstalled && item.backup != null) { applicationItems.add(item); applicationLabels.add(item.label); } } if (ThreadUtils.isInterrupted()) return; ThreadUtils.postOnMainThread(() -> runMultiChoiceDialog(applicationItems, applicationLabels)); } finally { CpuUtils.releaseWakeLock(wakeLock); } }); }); // Restore latest versions only view.findViewById(R.id.restore_latest).setOnClickListener(v -> { mActivity.progressIndicator.show(); if (mFuture != null) { mFuture.cancel(true); } WeakReference wakeLockRef = new WeakReference<>(mActivity.wakeLock); mFuture = ThreadUtils.postOnBackgroundThread(() -> { PowerManager.WakeLock wakeLock = wakeLockRef.get(); if (wakeLock != null && !wakeLock.isHeld()) { wakeLock.acquire(); } try { List applicationItems = new ArrayList<>(); List applicationLabels = new ArrayList<>(); for (ApplicationItem item : PackageUtils.getInstalledOrBackedUpApplicationsFromDb(requireContext(), false, true)) { if (ThreadUtils.isInterrupted()) return; if (item.isInstalled && item.backup != null && item.versionCode < item.backup.versionCode) { applicationItems.add(item); applicationLabels.add(item.label); } } if (ThreadUtils.isInterrupted()) return; ThreadUtils.postOnMainThread(() -> runMultiChoiceDialog(applicationItems, applicationLabels)); } finally { CpuUtils.releaseWakeLock(wakeLock); } }); }); return new MaterialAlertDialogBuilder(requireActivity()) .setView(view) .setTitle(R.string.restore) .setNegativeButton(R.string.cancel, null) .create(); } @Override public void onDestroy() { if (mFuture != null) { mFuture.cancel(true); } super.onDestroy(); } @UiThread private void runMultiChoiceDialog(List applicationItems, List applicationLabels) { if (isDetached()) return; mActivity.progressIndicator.hide(); new SearchableMultiChoiceDialogBuilder<>(mActivity, applicationItems, applicationLabels) .addSelections(applicationItems) .setTitle(R.string.filtered_packages) .setPositiveButton(R.string.restore, (dialog, which, selectedItems) -> { if (isDetached()) return; BackupRestoreDialogFragment fragment = BackupRestoreDialogFragment.getInstance( PackageUtils.getUserPackagePairs(selectedItems), BackupRestoreDialogFragment.MODE_RESTORE); fragment.setOnActionBeginListener(mode -> mActivity.progressIndicator.show()); fragment.setOnActionCompleteListener((mode, failedPackages) -> mActivity.progressIndicator.hide()); if (isDetached()) return; fragment.show(getParentFragmentManager(), BackupRestoreDialogFragment.TAG); }) .setNegativeButton(R.string.cancel, null) .show(); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/permission/DevelopmentPermission.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.permission; public class DevelopmentPermission extends Permission { public DevelopmentPermission(String name, boolean granted, int appOp, boolean appOpAllowed, int flags) { super(name, granted, appOp, appOpAllowed, flags); runtime = false; readOnly = false; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/permission/PermUtils.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.permission; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static io.github.muntashirakon.AppManager.compat.PermissionCompat.FLAG_PERMISSION_AUTO_REVOKED; import static io.github.muntashirakon.AppManager.compat.PermissionCompat.FLAG_PERMISSION_ONE_TIME; import static io.github.muntashirakon.AppManager.compat.PermissionCompat.FLAG_PERMISSION_REVIEW_REQUIRED; import static io.github.muntashirakon.AppManager.compat.PermissionCompat.FLAG_PERMISSION_REVOKED_COMPAT; import static io.github.muntashirakon.AppManager.compat.PermissionCompat.FLAG_PERMISSION_USER_FIXED; import static io.github.muntashirakon.AppManager.compat.PermissionCompat.FLAG_PERMISSION_USER_SET; import android.Manifest; import android.annotation.UserIdInt; import android.app.AppOpsManager; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.os.Build; import android.os.RemoteException; import android.os.UserHandleHidden; import android.provider.Settings; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresPermission; import androidx.annotation.WorkerThread; import java.util.HashMap; import java.util.Map; import io.github.muntashirakon.AppManager.compat.ActivityManagerCompat; import io.github.muntashirakon.AppManager.compat.AppOpsManagerCompat; import io.github.muntashirakon.AppManager.compat.ManifestCompat; import io.github.muntashirakon.AppManager.compat.PermissionCompat; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.utils.BroadcastUtils; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.IntentUtils; public class PermUtils { private static final String KILL_REASON_APP_OP_CHANGE = "Permission related app op changed"; public static class SettingItem { public final String action; public final boolean supportPkg; public SettingItem(String action, boolean supportPkg) { this.action = action; this.supportPkg = supportPkg; } public SettingItem(String action) { this(action, true); } public Intent toIntent(String packageName) { return IntentUtils.getSettings(action, supportPkg ? packageName : null); } } public static final Map permissionNameToSettingItem = new HashMap() {{ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { put(Manifest.permission.ACCESS_NOTIFICATION_POLICY, new SettingItem(Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS, false)); put(Manifest.permission.PACKAGE_USAGE_STATS, new SettingItem(Settings.ACTION_USAGE_ACCESS_SETTINGS)); put(Manifest.permission.SYSTEM_ALERT_WINDOW, new SettingItem(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)); put(Manifest.permission.WRITE_SETTINGS, new SettingItem(Settings.ACTION_MANAGE_WRITE_SETTINGS)); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { put(Manifest.permission.REQUEST_INSTALL_PACKAGES, new SettingItem(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES)); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { put(Manifest.permission.MANAGE_EXTERNAL_STORAGE, new SettingItem(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)); // put(Manifest.permission.SYSTEM_ALERT_WINDOW, new SettingItem("android.settings.MANAGE_APP_OVERLAY_PERMISSION")); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { put(Manifest.permission.MANAGE_MEDIA, new SettingItem(Settings.ACTION_REQUEST_MANAGE_MEDIA)); put(Manifest.permission.SCHEDULE_EXACT_ALARM, new SettingItem(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM)); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { put(Manifest.permission.POST_NOTIFICATIONS, new SettingItem(Settings.ACTION_APP_NOTIFICATION_SETTINGS)); // android.provider.extra.APP_PACKAGE } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { put(Manifest.permission.RUN_USER_INITIATED_JOBS, new SettingItem("android.settings.MANAGE_APP_LONG_RUNNING_JOBS")); put(Manifest.permission.USE_FULL_SCREEN_INTENT, new SettingItem(Settings.ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT)); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { put(Manifest.permission.MEDIA_ROUTING_CONTROL, new SettingItem(Settings.ACTION_REQUEST_MEDIA_ROUTING_CONTROL)); } // Bound permissions put(Manifest.permission.BIND_ACCESSIBILITY_SERVICE, new SettingItem(Settings.ACTION_ACCESSIBILITY_SETTINGS, false)); put(Manifest.permission.BIND_INPUT_METHOD, new SettingItem(Settings.ACTION_INPUT_METHOD_SETTINGS, false)); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { put(Manifest.permission.BIND_AUTOFILL_SERVICE, new SettingItem(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE)); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { put(Manifest.permission.BIND_CREDENTIAL_PROVIDER_SERVICE, new SettingItem(Settings.ACTION_CREDENTIAL_PROVIDER)); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { put(Manifest.permission.BIND_NOTIFICATION_LISTENER_SERVICE, new SettingItem(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS, false)); } }}; /** * Grant the permission. * *

This also automatically grants app op if it has app op. * * @param setByTheUser If the user has made the decision. This does not unset the flag * @param fixedByTheUser If the user requested that she/he does not want to be asked again */ @RequiresPermission(allOf = { "android.permission.MANAGE_APP_OPS_MODES", ManifestCompat.permission.GRANT_RUNTIME_PERMISSIONS, ManifestCompat.permission.REVOKE_RUNTIME_PERMISSIONS, }) @WorkerThread public static void grantPermission(@NonNull PackageInfo packageInfo, @NonNull Permission permission, @NonNull AppOpsManagerCompat appOpsManager, boolean setByTheUser, boolean fixedByTheUser) throws PermissionException { boolean killApp = false; boolean wasGranted = permission.isGrantedIncludingAppOp(); if (!isModifiable(permission)) { // Unmodifiable permission, do nothing throw new PermissionException("Unmodifiable permission " + permission.getName()); } if (!permission.isReadOnly() && (!permission.isRuntime() || supportsRuntimePermissions(packageInfo.applicationInfo))) { // Runtime/development permissions. In case of runtime, it is not a pre23 app. // Ensure the permission app op is enabled before the permission grant. if (permission.affectsAppOp() && !permission.isAppOpAllowed()) { permission.setAppOpAllowed(true); } // Grant the permission if needed. if (!permission.isGranted()) { permission.setGranted(true); } // Update the permission flags. if (!fixedByTheUser) { // Now the apps can ask for the permission as the user // no longer has it fixed in a denied state. if (permission.isUserFixed()) { permission.setUserFixed(false); } if (setByTheUser) { if (!permission.isUserSet()) { permission.setUserSet(true); } } } else { if (!permission.isUserFixed()) { permission.setUserFixed(true); } if (permission.isUserSet()) { permission.setUserSet(false); } } } else { // Read-only or legacy permissions // Legacy apps cannot have a not granted runtime permission but just in case. if (permission.isRuntime() && !permission.isGranted()) { throw new PermissionException("Legacy app cannot have not-granted runtime permission " + permission.getName()); } // If the permissions has no corresponding app op, then it is a // third-party one, and we do not offer toggling of such permissions. if (permission.affectsAppOp()) { if (!permission.isAppOpAllowed()) { permission.setAppOpAllowed(true); // Legacy apps do not know that they have to retry access to a // resource due to changes in runtime permissions (app ops in this // case). Therefore, we restart them on app op change, so they // can pick up the change. killApp = true; } // Mark that the permission is not kept granted only for compatibility. if (permission.isRevokedCompat()) { permission.setRevokedCompat(false); } } // Granting a permission explicitly means the user already // reviewed it so clear the review flag on every grant. if (permission.isReviewRequired()) { permission.unsetReviewRequired(); } } try { persistChanges(packageInfo.applicationInfo, permission, appOpsManager, false, null); if (killApp && SelfPermissions.canKillUid()) { ActivityManagerCompat.killUid(packageInfo.applicationInfo.uid, KILL_REASON_APP_OP_CHANGE); } } catch (Exception e) { throw new PermissionException(e); } } /** * Revoke the permission. * *

This also disallows the app op for the permission if it has app op. * * @param fixedByTheUser If the user requested that she/he does not want to be asked again */ @RequiresPermission(allOf = { "android.permission.MANAGE_APP_OPS_MODES", ManifestCompat.permission.GRANT_RUNTIME_PERMISSIONS, ManifestCompat.permission.REVOKE_RUNTIME_PERMISSIONS, }) @WorkerThread public static void revokePermission(@NonNull PackageInfo packageInfo, @NonNull Permission permission, @NonNull AppOpsManagerCompat appOpsManager, boolean fixedByTheUser) throws PermissionException { boolean killApp = false; if (!isModifiable(permission)) { // Unmodifiable permission, do nothing throw new PermissionException("Unmodifiable permission " + permission.getName()); } if (!permission.isReadOnly() && (!permission.isRuntime() || supportsRuntimePermissions(packageInfo.applicationInfo))) { // Runtime/development permissions. In case of runtime, it is not a pre23 app. // Revoke the permission if needed. if (permission.isGranted()) { permission.setGranted(false); } // Update the permission flags. if (fixedByTheUser) { // Take a note that the user fixed the permission. if (permission.isUserSet() || !permission.isUserFixed()) { permission.setUserSet(false); permission.setUserFixed(true); } } else { if (!permission.isUserSet() || permission.isUserFixed()) { permission.setUserSet(true); permission.setUserFixed(false); } } if (permission.affectsAppOp()) { permission.setAppOpAllowed(false); } } else { // Read-only or legacy permissions // Legacy apps cannot have a non-granted permission but just in case. if (permission.isRuntime() && !permission.isGranted()) { throw new PermissionException("Legacy app cannot have not-granted runtime permission " + permission.getName()); } // If the permission has no corresponding app op, then it is a // third-party one and we do not offer toggling of such permissions. if (permission.affectsAppOp()) { if (permission.isAppOpAllowed()) { permission.setAppOpAllowed(false); // Disabling an app op may put the app in a situation in which it // has a handle to state it shouldn't have, so we have to kill the // app. This matches the revoke runtime permission behavior. killApp = true; } // Mark that the permission is kept granted only for compatibility. if (!permission.isRevokedCompat()) { permission.setRevokedCompat(true); } } } try { persistChanges(packageInfo.applicationInfo, permission, appOpsManager, false, null); if (killApp && SelfPermissions.canKillUid()) { ActivityManagerCompat.killUid(packageInfo.applicationInfo.uid, KILL_REASON_APP_OP_CHANGE); } } catch (Exception e) { throw new PermissionException(e); } } @RequiresPermission(allOf = { "android.permission.MANAGE_APP_OPS_MODES", ManifestCompat.permission.GRANT_RUNTIME_PERMISSIONS, ManifestCompat.permission.REVOKE_RUNTIME_PERMISSIONS, }) @WorkerThread private static void persistChanges(@NonNull ApplicationInfo applicationInfo, @NonNull Permission permission, @NonNull AppOpsManagerCompat appOpsManager, boolean mayKillBecauseOfAppOpsChange, @Nullable String revokeReason) throws PermissionException, RemoteException { int uid = applicationInfo.uid; int userId = UserHandleHidden.getUserId(uid); boolean shouldKillApp = false; if (!permission.isReadOnly()) { if (permission.isGranted()) { PermissionCompat.grantPermission(applicationInfo.packageName, permission.getName(), userId); Log.d("PERM", "Granted %s", permission.getName()); } else { boolean isCurrentlyGranted = PermissionCompat.checkPermission(permission.getName(), applicationInfo.packageName, userId) == PERMISSION_GRANTED; if (isCurrentlyGranted) { if (revokeReason == null) { PermissionCompat.revokePermission(applicationInfo.packageName, permission.getName(), userId); } else { PermissionCompat.revokePermission(applicationInfo.packageName, permission.getName(), userId, revokeReason); } Log.d("PERM", "Revoked %s", permission.getName()); } } } if (!permission.readOnly) { // Flags of the system fixed permissions may also be updated updateFlags(applicationInfo, permission, userId); } if (permission.affectsAppOp()) { if (!permission.isSystemFixed()) { // FIXME: 7/2/22 Disable system fixed check? // Enabling/Disabling an app op may put the app in a situation in which it has // a handle to state it shouldn't have, so we have to kill the app. This matches // the revoke runtime permission behavior. if (permission.isAppOpAllowed()) { boolean wasChanged = allowAppOp(appOpsManager, permission.getAppOp(), applicationInfo.packageName, uid); shouldKillApp = wasChanged && !supportsRuntimePermissions(applicationInfo); } else { shouldKillApp = disallowAppOp(appOpsManager, permission.getAppOp(), applicationInfo.packageName, uid); } } } if (mayKillBecauseOfAppOpsChange && shouldKillApp && SelfPermissions.canKillUid()) { ActivityManagerCompat.killUid(uid, KILL_REASON_APP_OP_CHANGE); } if (userId != UserHandleHidden.myUserId()) { BroadcastUtils.sendPackageAltered(ContextUtils.getContext(), new String[]{applicationInfo.packageName}); } } @RequiresPermission(anyOf = { ManifestCompat.permission.GET_RUNTIME_PERMISSIONS, ManifestCompat.permission.GRANT_RUNTIME_PERMISSIONS, ManifestCompat.permission.REVOKE_RUNTIME_PERMISSIONS, }) private static void updateFlags(@NonNull ApplicationInfo applicationInfo, @NonNull Permission permission, @UserIdInt int userId) throws RemoteException { int flags = (permission.isUserSet() ? FLAG_PERMISSION_USER_SET : 0) | (permission.isUserFixed() ? FLAG_PERMISSION_USER_FIXED : 0) | (permission.isRevokedCompat() ? FLAG_PERMISSION_REVOKED_COMPAT : 0) // | (permission.isPolicyFixed() ? FLAG_PERMISSION_POLICY_FIXED : 0) // TODO: Disabled in AOSP | (permission.isReviewRequired() ? FLAG_PERMISSION_REVIEW_REQUIRED : 0); boolean checkAdjustPolicy = PermissionCompat.getCheckAdjustPolicyFlagPermission(applicationInfo); PermissionCompat.updatePermissionFlags(permission.getName(), applicationInfo.packageName, FLAG_PERMISSION_USER_SET | FLAG_PERMISSION_USER_FIXED | FLAG_PERMISSION_REVOKED_COMPAT // | FLAG_PERMISSION_POLICY_FIXED // TODO: Disabled in AOSP | (permission.isReviewRequired() ? 0 : FLAG_PERMISSION_REVIEW_REQUIRED) | FLAG_PERMISSION_ONE_TIME | FLAG_PERMISSION_AUTO_REVOKED, // clear auto revoke flags, checkAdjustPolicy, userId); } /** * @return {@code true} iff app-op was changed */ @RequiresPermission("android.permission.MANAGE_APP_OPS_MODES") public static boolean allowAppOp(AppOpsManagerCompat appOpsManager, int appOp, String packageName, int uid) throws PermissionException { return setAppOpMode(appOpsManager, appOp, packageName, uid, AppOpsManager.MODE_ALLOWED); } /** * @return {@code true} iff app-op was changed */ @RequiresPermission("android.permission.MANAGE_APP_OPS_MODES") public static boolean disallowAppOp(AppOpsManagerCompat appOpsManager, int appOp, String packageName, int uid) throws PermissionException { return setAppOpMode(appOpsManager, appOp, packageName, uid, AppOpsManager.MODE_IGNORED); } /** * Set mode of an app-op if needed. * * @return {@code true} iff app-op was changed */ @RequiresPermission("android.permission.MANAGE_APP_OPS_MODES") public static boolean setAppOpMode(@NonNull AppOpsManagerCompat appOpsManager, int appOp, String packageName, int uid, @AppOpsManagerCompat.Mode int mode) throws PermissionException { try { int currentMode = appOpsManager.checkOperation(appOp, uid, packageName); if (currentMode == mode) { return false; } appOpsManager.setMode(appOp, uid, packageName, mode); return true; } catch (Exception e) { throw new PermissionException(e); } } private static boolean supportsRuntimePermissions(@NonNull ApplicationInfo applicationInfo) { return applicationInfo.targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1; } public static boolean systemSupportsRuntimePermissions() { return Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1; } public static boolean isModifiable(@NonNull Permission permission) { // Non-readonly permissions or permissions with app ops are modifiable return SelfPermissions.canModifyPermissions() && (!permission.isReadOnly() || permission.affectsAppOp()); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/permission/Permission.java ================================================ // SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.permission; import static io.github.muntashirakon.AppManager.compat.PermissionCompat.FLAG_PERMISSION_GRANTED_BY_DEFAULT; import static io.github.muntashirakon.AppManager.compat.PermissionCompat.FLAG_PERMISSION_POLICY_FIXED; import static io.github.muntashirakon.AppManager.compat.PermissionCompat.FLAG_PERMISSION_REVIEW_REQUIRED; import static io.github.muntashirakon.AppManager.compat.PermissionCompat.FLAG_PERMISSION_REVOKED_COMPAT; import static io.github.muntashirakon.AppManager.compat.PermissionCompat.FLAG_PERMISSION_REVOKE_ON_UPGRADE; import static io.github.muntashirakon.AppManager.compat.PermissionCompat.FLAG_PERMISSION_SYSTEM_FIXED; import static io.github.muntashirakon.AppManager.compat.PermissionCompat.FLAG_PERMISSION_USER_FIXED; import static io.github.muntashirakon.AppManager.compat.PermissionCompat.FLAG_PERMISSION_USER_SET; import android.os.Build; import io.github.muntashirakon.AppManager.compat.AppOpsManagerCompat; import io.github.muntashirakon.AppManager.compat.PermissionCompat; // Copyright (C) 2015 The Android Open Source Project public class Permission { boolean runtime; boolean readOnly; private final String mName; private final int mAppOp; private boolean mGranted; private boolean mAppOpAllowed; @PermissionCompat.PermissionFlags private int mFlags; public Permission(String name, boolean granted, int appOp, boolean appOpAllowed, @PermissionCompat.PermissionFlags int flags) { mName = name; mGranted = granted; mAppOp = appOp; mAppOpAllowed = appOpAllowed; mFlags = flags; runtime = true; readOnly = false; } public boolean isRuntime() { return runtime; } public boolean isReadOnly() { return readOnly || isSystemFixed(); } public String getName() { return mName; } public int getAppOp() { return mAppOp; } @PermissionCompat.PermissionFlags public int getFlags() { return mFlags; } public boolean hasAppOp() { return mAppOp != AppOpsManagerCompat.OP_NONE; } /** * Does this permission affect app ops. * *

I.e. does this permission have a matching app op or is this a background permission. All * background permissions affect the app op if it's assigned foreground permission. * * @return {@code true} if this permission affects app ops */ public boolean affectsAppOp() { return mAppOp != AppOpsManagerCompat.OP_NONE; } public boolean isGranted() { return mGranted; } public boolean isGrantedIncludingAppOp() { return mGranted && !isReviewRequired() && (!affectsAppOp() || isAppOpAllowed()); } public boolean isReviewRequired() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { return (mFlags & FLAG_PERMISSION_REVIEW_REQUIRED) != 0; } else return false; } public void resetReviewRequired() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { mFlags &= ~FLAG_PERMISSION_REVIEW_REQUIRED; } } public void unsetReviewRequired() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { mFlags &= ~FLAG_PERMISSION_REVIEW_REQUIRED; } } public void setGranted(boolean mGranted) { this.mGranted = mGranted; } public boolean isAppOpAllowed() { return mAppOpAllowed; } public boolean isUserFixed() { return (mFlags & FLAG_PERMISSION_USER_FIXED) != 0; } public void setUserFixed(boolean userFixed) { if (userFixed) { mFlags |= FLAG_PERMISSION_USER_FIXED; } else { mFlags &= ~FLAG_PERMISSION_USER_FIXED; } } public boolean isSystemFixed() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { return (mFlags & FLAG_PERMISSION_SYSTEM_FIXED) != 0; } return false; } public boolean isPolicyFixed() { return (mFlags & FLAG_PERMISSION_POLICY_FIXED) != 0; } public boolean isUserSet() { return (mFlags & FLAG_PERMISSION_USER_SET) != 0; } public boolean isGrantedByDefault() { return (mFlags & FLAG_PERMISSION_GRANTED_BY_DEFAULT) != 0; } public void setUserSet(boolean userSet) { if (userSet) { mFlags |= FLAG_PERMISSION_USER_SET; } else { mFlags &= ~FLAG_PERMISSION_USER_SET; } } public void setPolicyFixed(boolean policyFixed) { if (policyFixed) { mFlags |= FLAG_PERMISSION_POLICY_FIXED; } else { mFlags &= ~FLAG_PERMISSION_POLICY_FIXED; } } public boolean shouldRevokeOnUpgrade() { return (mFlags & FLAG_PERMISSION_REVOKE_ON_UPGRADE) != 0; } public void setRevokeOnUpgrade(boolean revokeOnUpgrade) { if (revokeOnUpgrade) { mFlags |= FLAG_PERMISSION_REVOKE_ON_UPGRADE; } else { mFlags &= ~FLAG_PERMISSION_REVOKE_ON_UPGRADE; } } public boolean isRevokedCompat() { return (mFlags & FLAG_PERMISSION_REVOKED_COMPAT) != 0; } public void setRevokedCompat(boolean revokedCompat) { if (revokedCompat) { mFlags |= FLAG_PERMISSION_REVOKED_COMPAT; } else { mFlags &= ~FLAG_PERMISSION_REVOKED_COMPAT; } } public void setAppOpAllowed(boolean mAppOpAllowed) { this.mAppOpAllowed = mAppOpAllowed; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/permission/PermissionException.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.permission; import android.util.AndroidException; public class PermissionException extends AndroidException { public PermissionException(String name) { super(name); } public PermissionException(String name, Throwable cause) { super(name, cause); } public PermissionException(Exception cause) { super(cause); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/permission/ReadOnlyPermission.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.permission; public class ReadOnlyPermission extends Permission { public ReadOnlyPermission(String name, boolean granted, int appOp, boolean appOpAllowed, int flags) { super(name, granted, appOp, appOpAllowed, flags); runtime = false; readOnly = true; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/permission/RuntimePermission.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.permission; public class RuntimePermission extends Permission { public RuntimePermission(String name, boolean granted, int appOp, boolean appOpAllowed, int flags) { super(name, granted, appOp, appOpAllowed, flags); runtime = true; readOnly = false; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/profiles/AddToProfileDialogFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.profiles; import static io.github.muntashirakon.AppManager.utils.UIUtils.getSecondaryText; import static io.github.muntashirakon.AppManager.utils.UIUtils.getSmallerText; import android.app.Dialog; import android.os.Bundle; import android.text.SpannableStringBuilder; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.DialogFragment; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicReference; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.profiles.struct.AppsProfile; import io.github.muntashirakon.AppManager.utils.ExUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.dialog.DialogTitleBuilder; import io.github.muntashirakon.dialog.SearchableMultiChoiceDialogBuilder; import io.github.muntashirakon.dialog.TextInputDialogBuilder; import io.github.muntashirakon.io.Path; public class AddToProfileDialogFragment extends DialogFragment { public static final String TAG = AddToProfileDialogFragment.class.getSimpleName(); private static final String ARG_PKGS = "pkgs"; public static AddToProfileDialogFragment getInstance(@NonNull String[] packages) { AddToProfileDialogFragment fragment = new AddToProfileDialogFragment(); Bundle args = new Bundle(); args.putStringArray(ARG_PKGS, packages); fragment.setArguments(args); return fragment; } @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { String[] packages = requireArguments().getStringArray(ARG_PKGS); // TODO: 16/9/23 Migrate to bottom sheet dialog and use loader before retrieving the profiles List profiles = ExUtils.requireNonNullElse(() -> ProfileManager.getProfiles(AppsProfile.PROFILE_TYPE_APPS), Collections.emptyList()); List profileNames = new ArrayList<>(profiles.size()); for (AppsProfile profile : profiles) { profileNames.add(new SpannableStringBuilder(profile.name).append("\n") .append(getSecondaryText(requireContext(), getSmallerText( profile.toLocalizedString(requireContext()))))); } AtomicReference dialogRef = new AtomicReference<>(); DialogTitleBuilder titleBuilder = new DialogTitleBuilder(requireContext()) .setTitle(R.string.add_to_profile) .setEndIconContentDescription(R.string.new_profile) .setEndIcon(R.drawable.ic_add, v -> new TextInputDialogBuilder(requireContext(), R.string.input_profile_name) .setTitle(R.string.new_profile) .setHelperText(R.string.input_profile_name_description) .setNegativeButton(R.string.cancel, null) .setPositiveButton(R.string.go, (dialog, which, profName, isChecked) -> { if (!TextUtils.isEmpty(profName)) { startActivity(AppsProfileActivity.getNewProfileIntent(requireContext(), profName.toString(), packages)); if (dialogRef.get() != null) { dialogRef.get().dismiss(); } } }) .show()); AlertDialog alertDialog = new SearchableMultiChoiceDialogBuilder<>(requireContext(), profiles, profileNames) .setTitle(titleBuilder.build()) .setNegativeButton(R.string.cancel, null) .setPositiveButton(R.string.add, (dialog, which, selectedItems) -> ThreadUtils.postOnBackgroundThread(() -> { boolean isSuccess = true; for (AppsProfile profile : selectedItems) { Path profilePath = ProfileManager.findProfilePathById(profile.profileId); if (profilePath == null) { isSuccess = false; continue; } try (OutputStream os = profilePath.openOutputStream()) { profile.appendPackages(packages); profile.write(os); } catch (Throwable e) { isSuccess = false; e.printStackTrace(); } } if (isSuccess) { ThreadUtils.postOnMainThread(() -> UIUtils.displayShortToast(R.string.done)); } else { ThreadUtils.postOnMainThread(() -> UIUtils.displayShortToast(R.string.failed)); } })) .create(); dialogRef.set(alertDialog); return alertDialog; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/profiles/AppsBaseProfileActivity.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.profiles; import static io.github.muntashirakon.AppManager.profiles.ProfileApplierActivity.ST_ADVANCED; import static io.github.muntashirakon.AppManager.profiles.ProfileApplierActivity.ST_SIMPLE; import android.content.Intent; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.text.TextUtils; import android.view.Menu; import android.view.MenuItem; import android.view.View; import androidx.activity.OnBackPressedCallback; import androidx.annotation.CallSuper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.lifecycle.ViewModelProvider; import androidx.viewpager2.adapter.FragmentStateAdapter; import androidx.viewpager2.widget.ViewPager2; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.navigation.NavigationBarView; import com.google.android.material.progressindicator.LinearProgressIndicator; import java.util.Objects; import io.github.muntashirakon.AppManager.BaseActivity; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.shortcut.CreateShortcutDialogFragment; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.dialog.SearchableSingleChoiceDialogBuilder; import io.github.muntashirakon.dialog.TextInputDialogBuilder; import io.github.muntashirakon.util.UiUtils; public abstract class AppsBaseProfileActivity extends BaseActivity implements NavigationBarView.OnItemSelectedListener { protected static final String EXTRA_NEW_PROFILE_NAME = "new_prof"; protected static final String EXTRA_PROFILE_ID = "prof"; protected static final String EXTRA_STATE = "state"; private ViewPager2 mViewPager; NavigationBarView bottomNavigationView; private MenuItem mPrevMenuItem; private final Fragment[] mFragments = new Fragment[3]; private final ViewPager2.OnPageChangeCallback mPageChangeCallback = new ViewPager2.OnPageChangeCallback() { @Override public void onPageSelected(int position) { if (mPrevMenuItem != null) { mPrevMenuItem.setChecked(false); } else { bottomNavigationView.getMenu().getItem(0).setChecked(false); } bottomNavigationView.getMenu().getItem(position).setChecked(true); mPrevMenuItem = bottomNavigationView.getMenu().getItem(position); } }; private final OnBackPressedCallback mOnBackPressedCallback = new OnBackPressedCallback(false) { @Override public void handleOnBackPressed() { if (model != null && model.isModified()) { new MaterialAlertDialogBuilder(AppsBaseProfileActivity.this) .setTitle(R.string.exit_confirmation) .setMessage(R.string.profile_modified_are_you_sure) .setPositiveButton(R.string.no, null) .setNegativeButton(R.string.yes, (dialog, which) -> { setEnabled(false); getOnBackPressedDispatcher().onBackPressed(); }) .setNeutralButton(R.string.save_and_exit, (dialog, which) -> { model.save(true); setEnabled(false); }) .show(); return; } setEnabled(false); getOnBackPressedDispatcher().onBackPressed(); } }; @Nullable protected String profileId; AppsProfileViewModel model; FloatingActionButton fab; LinearProgressIndicator progressIndicator; public abstract Fragment getAppsBaseFragment(); public abstract void loadNewProfile(@NonNull String newProfileName, @NonNull Intent intent); @CallSuper @Override protected void onAuthenticated(@Nullable Bundle savedInstanceState) { model = new ViewModelProvider(this).get(AppsProfileViewModel.class); setContentView(R.layout.activity_apps_profile); setSupportActionBar(findViewById(R.id.toolbar)); getOnBackPressedDispatcher().addCallback(this, mOnBackPressedCallback); progressIndicator = findViewById(R.id.progress_linear); progressIndicator.setVisibilityAfterHide(View.GONE); fab = findViewById(R.id.floatingActionButton); UiUtils.applyWindowInsetsAsMargin(fab); if (getIntent() == null) { finish(); return; } @Nullable String newProfileName = getIntent().getStringExtra(EXTRA_NEW_PROFILE_NAME); profileId = getIntent().getStringExtra(EXTRA_PROFILE_ID); if (profileId == null && newProfileName == null) { // Neither profile name/id is set finish(); return; } // Load/clone profile if (newProfileName != null) { // New profile requested if (profileId != null) { // Clone profile model.loadAndCloneProfile(profileId, newProfileName); } else { // New profile loadNewProfile(newProfileName, getIntent()); } } else { model.loadProfile(profileId); } mViewPager = findViewById(R.id.pager); mViewPager.setOffscreenPageLimit(2); mViewPager.registerOnPageChangeCallback(mPageChangeCallback); mViewPager.setAdapter(new ProfileFragmentPagerAdapter(this)); bottomNavigationView = findViewById(R.id.bottom_navigation); bottomNavigationView.setOnItemSelectedListener(this); // Observers model.getProfileModifiedLiveData().observe(this, modified -> { mOnBackPressedCallback.setEnabled(modified); if (getSupportActionBar() != null) { String name = (modified ? "* " : "") + model.getProfileName(); getSupportActionBar().setTitle(name); } }); model.observeToast().observe(this, stringResAndIsFinish -> { UIUtils.displayShortToast(stringResAndIsFinish.first); if (stringResAndIsFinish.second) { getOnBackPressedDispatcher().onBackPressed(); } }); model.observeProfileLoaded().observe(this, profileName -> { setTitle(profileName); progressIndicator.hide(); }); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.fragment_profile_apps_actions, menu); return super.onCreateOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int id = item.getItemId(); if (id == android.R.id.home) { getOnBackPressedDispatcher().onBackPressed(); } else if (id == R.id.action_apply) { Intent intent = ProfileApplierActivity.getApplierIntent(this, model.getProfileName()); startActivity(intent); } else if (id == R.id.action_save) { model.save(false); } else if (id == R.id.action_discard) { model.discard(); } else if (id == R.id.action_delete) { new MaterialAlertDialogBuilder(this) .setTitle(getString(R.string.delete_filename, model.getProfileName())) .setMessage(R.string.are_you_sure) .setPositiveButton(R.string.cancel, null) .setNegativeButton(R.string.ok, (dialog, which) -> model.delete()) .show(); } else if (id == R.id.action_duplicate) { new TextInputDialogBuilder(this, R.string.input_profile_name) .setTitle(R.string.new_profile) .setHelperText(R.string.input_profile_name_description) .setNegativeButton(R.string.cancel, null) .setPositiveButton(R.string.go, (dialog, which, profName, isChecked) -> { if (TextUtils.isEmpty(profName)) { UIUtils.displayShortToast(R.string.failed_to_duplicate_profile); return; } progressIndicator.show(); model.cloneProfile(profName.toString()); }) .show(); } else if (id == R.id.action_shortcut) { final String[] shortcutTypesL = new String[]{ getString(R.string.simple), getString(R.string.advanced) }; final String[] shortcutTypes = new String[]{ST_SIMPLE, ST_ADVANCED}; new SearchableSingleChoiceDialogBuilder<>(this, shortcutTypes, shortcutTypesL) .setTitle(R.string.create_shortcut) .setOnSingleChoiceClickListener((dialog, which, item1, isChecked) -> { if (!isChecked) { return; } Drawable icon = Objects.requireNonNull(ContextCompat.getDrawable(this, R.drawable.ic_launcher_foreground)); ProfileShortcutInfo shortcutInfo = new ProfileShortcutInfo(model.getProfileId(), model.getProfileName(), shortcutTypes[which], shortcutTypesL[which]); shortcutInfo.setIcon(UIUtils.getBitmapFromDrawable(icon)); CreateShortcutDialogFragment dialog1 = CreateShortcutDialogFragment.getInstance(shortcutInfo); dialog1.show(getSupportFragmentManager(), CreateShortcutDialogFragment.TAG); dialog.dismiss(); }) .show(); } else return super.onOptionsItemSelected(item); return true; } @Override protected void onDestroy() { if (mViewPager != null) { mViewPager.unregisterOnPageChangeCallback(mPageChangeCallback); } super.onDestroy(); } @Override public boolean onNavigationItemSelected(@NonNull MenuItem item) { int itemId = item.getItemId(); if (itemId == R.id.action_apps) { mViewPager.setCurrentItem(0, true); } else if (itemId == R.id.action_preview) { mViewPager.setCurrentItem(0, true); } else if (itemId == R.id.action_conf) { mViewPager.setCurrentItem(1, true); } else if (itemId == R.id.action_logs) { mViewPager.setCurrentItem(2, true); } else return false; return true; } // For tab layout private class ProfileFragmentPagerAdapter extends FragmentStateAdapter { ProfileFragmentPagerAdapter(@NonNull FragmentActivity fragmentActivity) { super(fragmentActivity); } @NonNull @Override public Fragment createFragment(int position) { Fragment fragment = mFragments[position]; if (fragment == null) { switch (position) { case 0: return mFragments[position] = getAppsBaseFragment(); case 1: return mFragments[position] = new ConfFragment(); case 2: return mFragments[position] = new LogViewerFragment(); } } return Objects.requireNonNull(fragment); } @Override public int getItemCount() { return mFragments.length; } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/profiles/AppsFilterProfileActivity.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.profiles; import android.content.Context; import android.content.Intent; import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.filters.EditFiltersDialogFragment; import io.github.muntashirakon.AppManager.filters.FilterItem; public class AppsFilterProfileActivity extends AppsBaseProfileActivity implements EditFiltersDialogFragment.OnSaveDialogButtonInterface { @NonNull public static Intent getProfileIntent(@NonNull Context context, @NonNull String profileId) { Intent intent = new Intent(context, AppsFilterProfileActivity.class); intent.putExtra(EXTRA_PROFILE_ID, profileId); return intent; } @NonNull public static Intent getNewProfileIntent(@NonNull Context context, @NonNull String profileName) { Intent intent = new Intent(context, AppsFilterProfileActivity.class); intent.putExtra(EXTRA_NEW_PROFILE_NAME, profileName); return intent; } @NonNull public static Intent getCloneProfileIntent(@NonNull Context context, @NonNull String oldProfileId, @NonNull String newProfileName) { Intent intent = new Intent(context, AppsFilterProfileActivity.class); intent.putExtra(EXTRA_PROFILE_ID, oldProfileId); intent.putExtra(EXTRA_NEW_PROFILE_NAME, newProfileName); return intent; } @Override public Fragment getAppsBaseFragment() { return new AppsFragment(); } @Override public void loadNewProfile(@NonNull String newProfileName, @NonNull Intent intent) { model.loadNewAppsFilterProfile(newProfileName); } @Override protected void onAuthenticated(@Nullable Bundle savedInstanceState) { super.onAuthenticated(savedInstanceState); bottomNavigationView.getMenu().removeItem(R.id.action_apps); fab.setImageResource(R.drawable.ic_filter_menu); fab.setContentDescription(getString(R.string.filters)); fab.setOnClickListener(v -> { EditFiltersDialogFragment dialog = new EditFiltersDialogFragment(); dialog.setOnSaveDialogButtonInterface(this); dialog.show(getSupportFragmentManager(), EditFiltersDialogFragment.TAG); }); } @NonNull @Override public FilterItem getFilterItem() { return model.getFilterItem(); } @Override public void onItemAltered(@NonNull FilterItem item) { model.setModified(true); model.loadPackages(); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/profiles/AppsFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.profiles; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.PopupMenu; import androidx.fragment.app.Fragment; import com.google.android.material.progressindicator.LinearProgressIndicator; import java.util.ArrayList; import java.util.List; import java.util.Objects; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.filters.IFilterableAppInfo; import io.github.muntashirakon.AppManager.self.imagecache.ImageLoader; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.util.AdapterUtils; import io.github.muntashirakon.widget.RecyclerView; import io.github.muntashirakon.widget.SwipeRefreshLayout; public class AppsFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener { public static class AppsFragmentItem { @NonNull public final String packageName; @Nullable public CharSequence label; public ApplicationInfo applicationInfo; public IFilterableAppInfo filterableAppInfo; public AppsFragmentItem(@NonNull String packageName) { this.packageName = packageName; } @Override public boolean equals(Object o) { if (this == o) return true; if (o instanceof String) { return Objects.equals(packageName, o); } if (!(o instanceof AppsFragmentItem)) return false; AppsFragmentItem that = (AppsFragmentItem) o; return Objects.equals(packageName, that.packageName); } @Override public int hashCode() { return Objects.hash(packageName); } } private AppsBaseProfileActivity mActivity; private SwipeRefreshLayout mSwipeRefresh; private LinearProgressIndicator mProgressIndicator; private AppsProfileViewModel mModel; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); mActivity = (AppsBaseProfileActivity) requireActivity(); } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.pager_app_details, container, false); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); // Swipe refresh mSwipeRefresh = view.findViewById(R.id.swipe_refresh); mSwipeRefresh.setOnRefreshListener(this); RecyclerView recyclerView = view.findViewById(R.id.scrollView); recyclerView.setLayoutManager(UIUtils.getGridLayoutAt450Dp(mActivity)); final TextView emptyView = view.findViewById(android.R.id.empty); emptyView.setText(R.string.no_apps); recyclerView.setEmptyView(emptyView); mProgressIndicator = view.findViewById(R.id.progress_linear); mProgressIndicator.setVisibilityAfterHide(View.GONE); mProgressIndicator.show(); view.findViewById(R.id.alert_text).setVisibility(View.GONE); mSwipeRefresh.setOnChildScrollUpCallback((parent, child) -> recyclerView.canScrollVertically(-1)); mModel = mActivity.model; AppsRecyclerAdapter adapter = new AppsRecyclerAdapter(); recyclerView.setAdapter(adapter); mModel.getPackages().observe(getViewLifecycleOwner(), packages -> { mProgressIndicator.hide(); adapter.setList(packages); }); } @Override public void onResume() { super.onResume(); if (mActivity.getSupportActionBar() != null) { mActivity.getSupportActionBar().setSubtitle(mModel.getPreviewTitleString()); } mActivity.fab.show(); } @Override public void onRefresh() { mSwipeRefresh.setRefreshing(false); mModel.loadPackages(); } public class AppsRecyclerAdapter extends RecyclerView.Adapter { final PackageManager pm; final ArrayList packages = new ArrayList<>(); final ImageLoader.DefaultImageDrawable defaultImage; private AppsRecyclerAdapter() { pm = mActivity.getPackageManager(); defaultImage = new ImageLoader.DefaultImageDrawable("android_default_icon", pm.getDefaultActivityIcon()); } void setList(List list) { AdapterUtils.notifyDataSetChanged(this, packages, list); } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(io.github.muntashirakon.ui.R.layout.m3_preference, parent, false); return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { AppsFragmentItem fragmentItem = packages.get(position); holder.icon.setTag(fragmentItem); if (fragmentItem.applicationInfo != null) { ImageLoader.getInstance().displayImage(fragmentItem.packageName, fragmentItem.applicationInfo, holder.icon); } else { ImageLoader.getInstance().displayImage(fragmentItem.packageName, holder.icon, tag -> new ImageLoader.ImageFetcherResult(tag, UIUtils.getBitmapFromDrawable(fragmentItem.filterableAppInfo.getAppIcon()), true, true, defaultImage)); } CharSequence label = fragmentItem.label; holder.title.setText(label != null ? label : fragmentItem.packageName); if (label != null) { holder.subtitle.setVisibility(View.VISIBLE); holder.subtitle.setText(fragmentItem.packageName); } else { holder.subtitle.setVisibility(View.GONE); } if (fragmentItem.applicationInfo != null) { holder.itemView.setOnClickListener(v -> { }); holder.itemView.setOnLongClickListener(v -> { PopupMenu popupMenu = new PopupMenu(mActivity, holder.itemView); popupMenu.setForceShowIcon(true); popupMenu.getMenu().add(R.string.delete).setIcon(R.drawable.ic_trash_can) .setOnMenuItemClickListener(item -> { mModel.deletePackage(fragmentItem.packageName); return true; }); popupMenu.show(); return true; }); } } @Override public int getItemCount() { return packages.size(); } public class ViewHolder extends RecyclerView.ViewHolder { ImageView icon; TextView title; TextView subtitle; public ViewHolder(@NonNull View itemView) { super(itemView); icon = itemView.findViewById(android.R.id.icon); icon.setContentDescription(itemView.getContext().getString(R.string.icon)); title = itemView.findViewById(android.R.id.title); subtitle = itemView.findViewById(android.R.id.summary); } } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/profiles/AppsProfileActivity.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.profiles; import static io.github.muntashirakon.AppManager.utils.UIUtils.getSmallerText; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.os.Bundle; import android.text.SpannableStringBuilder; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.util.Pair; import androidx.fragment.app.Fragment; import java.util.ArrayList; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.dialog.SearchableMultiChoiceDialogBuilder; public class AppsProfileActivity extends AppsBaseProfileActivity { @NonNull public static Intent getProfileIntent(@NonNull Context context, @NonNull String profileId) { Intent intent = new Intent(context, AppsProfileActivity.class); intent.putExtra(EXTRA_PROFILE_ID, profileId); return intent; } @NonNull public static Intent getNewProfileIntent(@NonNull Context context, @NonNull String profileName) { return getNewProfileIntent(context, profileName, null); } @NonNull public static Intent getNewProfileIntent(@NonNull Context context, @NonNull String profileName, @Nullable String[] initialPackages) { Intent intent = new Intent(context, AppsProfileActivity.class); intent.putExtra(EXTRA_NEW_PROFILE_NAME, profileName); if (initialPackages != null) { intent.putExtra(EXTRA_NEW_PROFILE_PACKAGES, initialPackages); } return intent; } @NonNull public static Intent getCloneProfileIntent(@NonNull Context context, @NonNull String oldProfileId, @NonNull String newProfileName) { Intent intent = new Intent(context, AppsProfileActivity.class); intent.putExtra(EXTRA_PROFILE_ID, oldProfileId); intent.putExtra(EXTRA_NEW_PROFILE_NAME, newProfileName); return intent; } private static final String EXTRA_NEW_PROFILE_PACKAGES = "new_prof_pkgs"; private static final String EXTRA_SHORTCUT_TYPE = "shortcut"; @Override public Fragment getAppsBaseFragment() { return new AppsFragment(); } @Override public void loadNewProfile(@NonNull String newProfileName, @NonNull Intent intent) { @Nullable String[] initialPackages = intent.getStringArrayExtra(EXTRA_NEW_PROFILE_PACKAGES); model.loadNewAppsProfile(newProfileName, initialPackages); } @Override protected void onAuthenticated(@Nullable Bundle savedInstanceState) { super.onAuthenticated(savedInstanceState); bottomNavigationView.getMenu().removeItem(R.id.action_preview); if (getIntent().hasExtra(EXTRA_SHORTCUT_TYPE)) { // Compatibility mode for shortcut @ProfileApplierActivity.ShortcutType String shortcutType = getIntent().getStringExtra(EXTRA_SHORTCUT_TYPE); @Nullable String profileState = getIntent().getStringExtra(EXTRA_STATE); if (shortcutType != null && profileId != null) { ProfileApplierActivity.getShortcutIntent(this, profileId, shortcutType, profileState); } // Finish regardless of whether the profile applier launched or not finish(); return; } fab.setImageResource(R.drawable.ic_add); fab.setContentDescription(getString(R.string.add_item)); fab.setOnClickListener(v -> { progressIndicator.show(); model.loadInstalledApps(); }); model.observeInstalledApps().observe(this, itemPairs -> { ArrayList items = new ArrayList<>(itemPairs.size()); ArrayList itemNames = new ArrayList<>(itemPairs.size()); for (Pair itemPair : itemPairs) { items.add(itemPair.second.packageName); boolean isSystem = (itemPair.second.flags & ApplicationInfo.FLAG_SYSTEM) != 0; itemNames.add(new SpannableStringBuilder(itemPair.first) .append("\n") .append(getSmallerText(getString(isSystem ? R.string.system : R.string.user)))); } progressIndicator.hide(); new SearchableMultiChoiceDialogBuilder<>(this, items, itemNames) .addSelections(model.getCurrentPackages()) .setTitle(R.string.apps) .setPositiveButton(R.string.ok, (d, i, selectedItems) -> model.setPackages(selectedItems)) .setNegativeButton(R.string.cancel, null) .show(); }); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/profiles/AppsProfileViewModel.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.profiles; import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.MATCH_DISABLED_COMPONENTS; import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES; import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.MATCH_UNINSTALLED_PACKAGES; import static io.github.muntashirakon.AppManager.utils.PackageUtils.getAppOpNames; import android.annotation.SuppressLint; import android.app.Application; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.os.RemoteException; import android.os.UserHandleHidden; import androidx.annotation.AnyThread; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.annotation.WorkerThread; import androidx.core.util.Pair; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import org.json.JSONException; import java.io.IOException; import java.io.OutputStream; import java.text.Collator; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.Future; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.compat.AppOpsManagerCompat; import io.github.muntashirakon.AppManager.compat.PackageManagerCompat; import io.github.muntashirakon.AppManager.filters.FilterItem; import io.github.muntashirakon.AppManager.filters.FilterableAppInfo; import io.github.muntashirakon.AppManager.filters.FilteringUtils; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.profiles.struct.AppsBaseProfile; import io.github.muntashirakon.AppManager.profiles.struct.AppsFilterProfile; import io.github.muntashirakon.AppManager.profiles.struct.AppsProfile; import io.github.muntashirakon.AppManager.profiles.struct.BaseProfile; import io.github.muntashirakon.AppManager.users.Users; import io.github.muntashirakon.AppManager.utils.ArrayUtils; import io.github.muntashirakon.AppManager.utils.PackageUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.Utils; import io.github.muntashirakon.io.Path; public class AppsProfileViewModel extends AndroidViewModel { private final Object mProfileLock = new Object(); private final MutableLiveData> mToast = new MutableLiveData<>(); private final MutableLiveData>> mInstalledApps = new MutableLiveData<>(); private final MutableLiveData mProfileLoaded = new MutableLiveData<>(); private final MutableLiveData mProfileModifiedLiveData = new MutableLiveData<>(); private final MutableLiveData mLogs = new MutableLiveData<>(); private MutableLiveData> packagesLiveData; @GuardedBy("profileLock") @Nullable private AppsBaseProfile mProfile; private boolean mIsModified; @Nullable private Future mLoadProfileResult; private Future mLoadAppsResult; public AppsProfileViewModel(@NonNull Application application) { super(application); } @Override protected void onCleared() { if (mLoadProfileResult != null) { mLoadProfileResult.cancel(true); } if (mLoadAppsResult != null) { mLoadAppsResult.cancel(true); } super.onCleared(); } public boolean isModified() { return mIsModified; } @SuppressLint("WrongThread") // main thread check present @AnyThread public void setModified(boolean modified) { if (mIsModified != modified) { mIsModified = modified; if (ThreadUtils.isMainThread()) { mProfileModifiedLiveData.setValue(modified); } else { mProfileModifiedLiveData.postValue(modified); } } } public LiveData> observeToast() { return mToast; } public LiveData>> observeInstalledApps() { return mInstalledApps; } public LiveData observeProfileLoaded() { return mProfileLoaded; } public LiveData getProfileModifiedLiveData() { return mProfileModifiedLiveData; } public LiveData getLogs() { return mLogs; } @AnyThread public void loadInstalledApps() { if (mLoadAppsResult != null) { mLoadAppsResult.cancel(true); } mLoadAppsResult = ThreadUtils.postOnBackgroundThread(() -> { PackageManager pm = getApplication().getPackageManager(); try { ArrayList> itemPairs; List applicationInfoList; applicationInfoList = PackageUtils.getAllApplications(MATCH_UNINSTALLED_PACKAGES | MATCH_STATIC_SHARED_AND_SDK_LIBRARIES); HashSet applicationInfoHashMap = new HashSet<>(); itemPairs = new ArrayList<>(); for (ApplicationInfo info : applicationInfoList) { if (applicationInfoHashMap.contains(info.packageName)) { continue; } applicationInfoHashMap.add(info.packageName); itemPairs.add(new Pair<>(pm.getApplicationLabel(info), info)); if (ThreadUtils.isInterrupted()) { return; } } List selectedPackages = mProfile instanceof AppsProfile ? Arrays.asList(((AppsProfile) mProfile).packages) : Collections.emptyList(); Collator collator = Collator.getInstance(); Collections.sort(itemPairs, (o1, o2) -> collator.compare(o1.first.toString(), o2.first.toString())); Collections.sort(itemPairs, (o1, o2) -> { boolean o1Selected = selectedPackages.contains(o1.second.packageName); boolean o2Selected = selectedPackages.contains(o2.second.packageName); if (o1Selected && o2Selected) { return 0; } if (o1Selected) { return -1; } if (o2Selected) { return +1; } return 0; }); mInstalledApps.postValue(itemPairs); } catch (Exception e) { e.printStackTrace(); } }); } public String getProfileName() { return mProfile != null ? mProfile.name : null; } public String getProfileId() { return mProfile != null ? mProfile.profileId : null; } @AnyThread public void loadProfile(@NonNull String profileId) { if (mLoadProfileResult != null) { mLoadProfileResult.cancel(true); } mLoadProfileResult = ThreadUtils.postOnBackgroundThread(() -> { loadProfileInternal(profileId); mProfileLoaded.postValue(mProfile != null ? mProfile.name : null); }); } @AnyThread public void loadLogs() { if (mProfile == null) { return; } ThreadUtils.postOnBackgroundThread(() -> mLogs.postValue(ProfileLogger.getAllLogs(mProfile.profileId))); } @WorkerThread @GuardedBy("profileLock") private void loadProfileInternal(@NonNull String profileId) { synchronized (mProfileLock) { Path profilePath = ProfileManager.findProfilePathById(profileId); try { mProfile = (AppsBaseProfile) BaseProfile.fromPath(profilePath); } catch (IOException | JSONException e) { e.printStackTrace(); } } } @WorkerThread @GuardedBy("profileLock") private void cloneProfileInternal(@NonNull String newProfileName) { synchronized (mProfileLock) { mProfile = (AppsBaseProfile) BaseProfile.newProfile(newProfileName, mProfile.type, mProfile); } } @AnyThread public void cloneProfile(@NonNull String newProfileName) { if (mLoadProfileResult != null) { mLoadProfileResult.cancel(true); } mLoadProfileResult = ThreadUtils.postOnBackgroundThread(() -> { if (mProfile == null) { mToast.postValue(new Pair<>(R.string.failed, false)); return; } cloneProfileInternal(newProfileName); mToast.postValue(new Pair<>(R.string.done, false)); setModified(true); mProfileLoaded.postValue(mProfile != null ? mProfile.name : null); }); } @AnyThread public void loadNewAppsProfile(@NonNull String newProfileName, @Nullable String[] initialPackages) { if (mLoadProfileResult != null) { mLoadProfileResult.cancel(true); } mLoadProfileResult = ThreadUtils.postOnBackgroundThread(() -> { AppsProfile appsProfile; synchronized (mProfileLock) { appsProfile = (AppsProfile) BaseProfile.newProfile(newProfileName, BaseProfile.PROFILE_TYPE_APPS, null); } if (initialPackages != null) { appsProfile.packages = initialPackages; } mProfile = appsProfile; setModified(true); mProfileLoaded.postValue(mProfile != null ? mProfile.name : null); }); } @AnyThread public void loadNewAppsFilterProfile(@NonNull String newProfileName) { if (mLoadProfileResult != null) { mLoadProfileResult.cancel(true); } mLoadProfileResult = ThreadUtils.postOnBackgroundThread(() -> { synchronized (mProfileLock) { mProfile = (AppsFilterProfile) BaseProfile.newProfile(newProfileName, BaseProfile.PROFILE_TYPE_APPS_FILTER, null); } setModified(true); mProfileLoaded.postValue(mProfile != null ? mProfile.name : null); }); } @AnyThread public void loadAndCloneProfile(@NonNull String profileId, @NonNull String newProfileName) { if (mLoadProfileResult != null) { mLoadProfileResult.cancel(true); } mLoadProfileResult = ThreadUtils.postOnBackgroundThread(() -> { if (mProfile == null) { loadProfileInternal(profileId); if (ThreadUtils.isInterrupted()) { return; } } cloneProfileInternal(newProfileName); setModified(true); mProfileLoaded.postValue(mProfile != null ? mProfile.name : null); }); } @AnyThread @GuardedBy("profileLock") public void setPackages(@NonNull List packages) { if (mProfile == null) return; assert mProfile instanceof AppsProfile; setModified(true); synchronized (mProfileLock) { ((AppsProfile) mProfile).packages = packages.toArray(new String[0]); Log.e("Packages", "%s", packages); loadPackages(); } } @GuardedBy("profileLock") public void deletePackage(@NonNull String packageName) { if (mProfile == null) return; assert mProfile instanceof AppsProfile; setModified(true); synchronized (mProfileLock) { AppsProfile profile = (AppsProfile) mProfile; profile.packages = Objects.requireNonNull(ArrayUtils.removeString(profile.packages, packageName)); loadPackages(); } } @AnyThread @GuardedBy("profileLock") public void save(boolean exitOnSave) { if (mProfile == null) return; // Should never happen ThreadUtils.postOnBackgroundThread(() -> { synchronized (mProfileLock) { try { Path profilePath = ProfileManager.requireProfilePathById(mProfile.profileId); try (OutputStream os = profilePath.openOutputStream()) { mProfile.write(os); mToast.postValue(new Pair<>(R.string.saved_successfully, exitOnSave)); setModified(false); } } catch (IOException e) { e.printStackTrace(); mToast.postValue(new Pair<>(R.string.saving_failed, false)); } } }); } @AnyThread public void discard() { if (mLoadProfileResult != null) { mLoadProfileResult.cancel(true); } mLoadProfileResult = ThreadUtils.postOnBackgroundThread(() -> { synchronized (mProfileLock) { if (mProfile != null) { loadProfileInternal(mProfile.profileId); } if (ThreadUtils.isInterrupted()) { return; } loadPackages(); setModified(false); } }); } @AnyThread public void delete() { ThreadUtils.postOnBackgroundThread(() -> { synchronized (mProfileLock) { if (mProfile == null) return; if (ProfileManager.deleteProfile(mProfile.profileId)) { mToast.postValue(new Pair<>(R.string.deleted_successfully, true)); } else mToast.postValue(new Pair<>(R.string.deletion_failed, false)); } }); } @NonNull public LiveData> getPackages() { if (packagesLiveData == null) { packagesLiveData = new MutableLiveData<>(); loadPackages(); } return packagesLiveData; } public List getCurrentPackages() { if (mProfile == null) { return Collections.emptyList(); } assert mProfile instanceof AppsProfile; return Arrays.asList(((AppsProfile) mProfile).packages); } public FilterItem getFilterItem() { assert mProfile instanceof AppsFilterProfile; return ((AppsFilterProfile) mProfile).getFilterItem(); } @StringRes public int getPreviewTitleString() { if (mProfile instanceof AppsProfile) { return R.string.apps; } else if (mProfile instanceof AppsFilterProfile) { return R.string.preview; } if (mProfile != null) { throw new UnsupportedOperationException("Invalid profile type: " + mProfile.type); } else throw new UnsupportedOperationException("Profile not initialized"); } @AnyThread public void loadPackages() { if (mLoadProfileResult != null) { mLoadProfileResult.cancel(true); } mLoadProfileResult = ThreadUtils.postOnBackgroundThread(() -> { synchronized (mProfileLock) { if (mProfile instanceof AppsProfile) { packagesLiveData.postValue(loadAppsPackages((AppsProfile) mProfile)); } else if (mProfile instanceof AppsFilterProfile) { packagesLiveData.postValue(loadAppsFilteredPackages((AppsFilterProfile) mProfile)); } } }); } @NonNull private ArrayList loadAppsPackages(@NonNull AppsProfile profile) { ArrayList oldItems = packagesLiveData.getValue(); ArrayList items = new ArrayList<>(profile.packages.length); int userId = UserHandleHidden.myUserId(); PackageManager pm = getApplication().getPackageManager(); for (String packageName : profile.packages) { AppsFragment.AppsFragmentItem item = new AppsFragment.AppsFragmentItem(packageName); // Check for old item for faster loading in case there are hundreds of items if (oldItems != null) { int i = oldItems.indexOf(item); if (i != -1) { AppsFragment.AppsFragmentItem oldItem = oldItems.get(i); if (oldItem.applicationInfo != null) { item.applicationInfo = oldItem.applicationInfo; item.label = oldItem.label; } } } if (item.applicationInfo == null) { try { item.applicationInfo = PackageManagerCompat.getApplicationInfo(packageName, MATCH_UNINSTALLED_PACKAGES | MATCH_DISABLED_COMPONENTS | MATCH_STATIC_SHARED_AND_SDK_LIBRARIES, userId); } catch (RemoteException | PackageManager.NameNotFoundException ignore) { } if (item.applicationInfo != null) { item.label = item.applicationInfo.loadLabel(pm); } if (Objects.equals(item.label, packageName)) { item.label = null; } } items.add(item); } return items; } List mFilterableAppInfoList; @NonNull private ArrayList loadAppsFilteredPackages(@NonNull AppsFilterProfile profile) { int[] users = profile.users != null ? profile.users : Users.getUsersIds(); if (mFilterableAppInfoList == null) { mFilterableAppInfoList = FilteringUtils.loadFilterableAppInfo(users); } List> filteredItems = profile.getFilterItem().getFilteredList(mFilterableAppInfoList); ArrayList items = new ArrayList<>(filteredItems.size()); for (FilterItem.FilteredItemInfo itemInfo : filteredItems) { AppsFragment.AppsFragmentItem item = new AppsFragment.AppsFragmentItem(itemInfo.info.getPackageName()); item.label = itemInfo.info.getAppLabel(); item.filterableAppInfo = itemInfo.info; items.add(item); } return items; } public void putBoolean(@NonNull String key, boolean value) { if (mProfile == null) return; setModified(true); switch (key) { case "freeze": mProfile.freeze = value; break; case "force_stop": mProfile.forceStop = value; break; case "clear_cache": mProfile.clearCache = value; break; case "clear_data": mProfile.clearData = value; break; case "block_trackers": mProfile.blockTrackers = value; break; case "save_apk": mProfile.saveApk = value; break; case "allow_routine": mProfile.allowRoutine = value; break; } } public boolean getBoolean(@NonNull String key, boolean defValue) { if (mProfile == null) return defValue; switch (key) { case "freeze": return mProfile.freeze; case "force_stop": return mProfile.forceStop; case "clear_cache": return mProfile.clearCache; case "clear_data": return mProfile.clearData; case "block_trackers": return mProfile.blockTrackers; case "save_apk": return mProfile.saveApk; case "allow_routine": return mProfile.allowRoutine; default: return defValue; } } @Nullable public String getComment() { if (mProfile == null) return null; return mProfile.comment; } public void setComment(@Nullable String comment) { if (mProfile == null) return; setModified(true); mProfile.comment = comment; } public void setState(@BaseProfile.ProfileState String state) { if (mProfile == null) return; setModified(true); mProfile.state = state; } @NonNull @BaseProfile.ProfileState public String getState() { return mProfile == null || mProfile.state == null ? BaseProfile.STATE_OFF : mProfile.state; } public void setUsers(@Nullable int[] users) { if (mProfile == null) return; setModified(true); mProfile.users = users; } @WorkerThread @NonNull public int[] getUsers() { return mProfile == null || mProfile.users == null ? Users.getUsersIds() : mProfile.users; } public void setExportRules(@Nullable Integer flags) { if (mProfile == null) return; setModified(true); mProfile.exportRules = flags; } @Nullable public Integer getExportRules() { if (mProfile == null) return null; return mProfile.exportRules; } public void setComponents(@Nullable String[] components) { if (mProfile == null) return; setModified(true); mProfile.components = components; } @Nullable public String[] getComponents() { if (mProfile == null) return null; return mProfile.components; } public void setPermissions(@Nullable String[] permissions) { if (mProfile == null) return; setModified(true); if (permissions != null) { for (String permission : permissions) { if (permission.equals("*")) { // Wildcard found, ignore all permissions in favour of global wildcard mProfile.permissions = new String[]{"*"}; return; } } } mProfile.permissions = permissions; } @Nullable public String[] getPermissions() { if (mProfile == null) return null; return mProfile.permissions; } public void setAppOps(@Nullable String[] appOpsStr) { if (mProfile == null) return; setModified(true); if (appOpsStr == null) { mProfile.appOps = null; return; } Set selectedAppOps = new HashSet<>(appOpsStr.length); List appOpList = AppOpsManagerCompat.getAllOps(); List appOpNameList = Arrays.asList(getAppOpNames(appOpList)); for (CharSequence appOpStr : appOpsStr) { if (appOpStr.equals("*")) { // Wildcard found, ignore all app ops in favour of global wildcard mProfile.appOps = new int[]{AppOpsManagerCompat.OP_NONE}; return; } try { selectedAppOps.add(Utils.getIntegerFromString(appOpStr, appOpNameList, appOpList)); } catch (IllegalArgumentException ignore) { } } mProfile.appOps = selectedAppOps.isEmpty() ? null : ArrayUtils.convertToIntArray(selectedAppOps); } @Nullable public String[] getAppOpsStr() { if (mProfile == null) return null; int[] appOps = mProfile.appOps; if (appOps == null) return null; String[] appOpsStr = new String[appOps.length]; for (int i = 0; i < appOps.length; ++i) { appOpsStr[i] = AppOpsManagerCompat.opToName(appOps[i]); } return appOpsStr; } public void setBackupInfo(@Nullable AppsBaseProfile.BackupInfo backupInfo) { if (mProfile == null) return; setModified(true); mProfile.backupData = backupInfo; } @Nullable public AppsBaseProfile.BackupInfo getBackupInfo() { if (mProfile == null) return null; return mProfile.backupData; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/profiles/ConfFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.profiles; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import io.github.muntashirakon.AppManager.R; public class ConfFragment extends Fragment { private AppsBaseProfileActivity mActivity; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); mActivity = (AppsBaseProfileActivity) requireActivity(); } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_container, container, false); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); AppsProfileViewModel model = new ViewModelProvider(requireActivity()).get(AppsProfileViewModel.class); model.observeProfileLoaded().observe(getViewLifecycleOwner(), profileName -> { if (profileName == null) return; getChildFragmentManager() .beginTransaction() .replace(R.id.fragment_container_view_tag, new ConfPreferences()) .commit(); }); } @Override public void onResume() { super.onResume(); if (mActivity.getSupportActionBar() != null) { mActivity.getSupportActionBar().setSubtitle(R.string.configurations); } mActivity.fab.hide(); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/profiles/ConfPreferences.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.profiles; import android.os.Bundle; import android.text.TextUtils; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.preference.Preference; import androidx.preference.PreferenceDataStore; import androidx.preference.PreferenceFragmentCompat; import androidx.preference.SwitchPreferenceCompat; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.textfield.TextInputEditText; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.backup.BackupFlags; import io.github.muntashirakon.AppManager.profiles.struct.AppsBaseProfile; import io.github.muntashirakon.AppManager.profiles.struct.BaseProfile; import io.github.muntashirakon.AppManager.rules.RulesTypeSelectionDialogFragment; import io.github.muntashirakon.AppManager.users.UserInfo; import io.github.muntashirakon.AppManager.users.Users; import io.github.muntashirakon.AppManager.utils.ArrayUtils; import io.github.muntashirakon.AppManager.utils.TextUtilsCompat; import io.github.muntashirakon.AppManager.utils.Utils; import io.github.muntashirakon.dialog.SearchableMultiChoiceDialogBuilder; import io.github.muntashirakon.dialog.SearchableSingleChoiceDialogBuilder; import io.github.muntashirakon.dialog.TextInputDialogBuilder; import io.github.muntashirakon.util.UiUtils; public class ConfPreferences extends PreferenceFragmentCompat { private AppsBaseProfileActivity mActivity; private AppsProfileViewModel mModel; @BaseProfile.ProfileState private final List mStates = Arrays.asList(BaseProfile.STATE_ON, BaseProfile.STATE_OFF); @Nullable private String[] mComponents; @Nullable private String[] mAppOps; @Nullable private String[] mPermissions; @Nullable private AppsBaseProfile.BackupInfo mBackupInfo; @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); // https://github.com/androidx/androidx/blob/androidx-main/preference/preference/res/layout/preference_recyclerview.xml RecyclerView recyclerView = view.findViewById(R.id.recycler_view); recyclerView.setFitsSystemWindows(true); recyclerView.setClipToPadding(false); UiUtils.applyWindowInsetsAsPadding(recyclerView, false, true); } @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.preferences_profile_config, rootKey); getPreferenceManager().setPreferenceDataStore(new ConfDataStore()); mActivity = (AppsBaseProfileActivity) requireActivity(); if (mActivity.model == null) { // ViewModel should never be null. // If it's null, it means that we're on the wrong Fragment return; } mModel = mActivity.model; // Set profile ID Preference profileIdPref = Objects.requireNonNull(findPreference("profile_id")); profileIdPref.setSummary(mModel.getProfileId()); profileIdPref.setOnPreferenceClickListener(preference -> { Utils.copyToClipboard(mActivity, mModel.getProfileName(), mModel.getProfileId()); return true; }); // Set comment Preference commentPref = Objects.requireNonNull(findPreference("comment")); commentPref.setSummary(mModel.getComment()); commentPref.setOnPreferenceClickListener(preference -> { new TextInputDialogBuilder(mActivity, R.string.comment) .setTitle(R.string.comment) .setInputText(mModel.getComment()) .setPositiveButton(R.string.ok, (dialog, which, inputText, isChecked) -> { mModel.setComment(TextUtils.isEmpty(inputText) ? null : inputText.toString()); commentPref.setSummary(mModel.getComment()); }) .setNegativeButton(R.string.cancel, null) .show(); return true; }); // Set state Preference statePref = Objects.requireNonNull(findPreference("state")); final String[] statesL = new String[]{ getString(R.string.on), getString(R.string.off) }; statePref.setTitle(getString(R.string.process_state, statesL[mStates.indexOf(mModel.getState())])); statePref.setOnPreferenceClickListener(preference -> { new SearchableSingleChoiceDialogBuilder<>(mActivity, mStates, statesL) .setTitle(R.string.profile_state) .setSelection(mModel.getState()) .setOnSingleChoiceClickListener((dialog, which, item, isChecked) -> { if (!isChecked) { return; } mModel.setState(mStates.get(which)); statePref.setTitle(getString(R.string.process_state, statesL[which])); dialog.dismiss(); }) .show(); return true; }); // Set users Preference usersPref = Objects.requireNonNull(findPreference("users")); handleUsersPref(usersPref); // Set components Preference componentsPref = Objects.requireNonNull(findPreference("components")); updateComponentsPref(componentsPref); componentsPref.setOnPreferenceClickListener(preference -> { new TextInputDialogBuilder(mActivity, R.string.input_signatures) .setTitle(R.string.components) .setInputText(mComponents == null ? "" : TextUtils.join(" ", mComponents)) .setHelperText(R.string.input_signatures_description) .setPositiveButton(R.string.ok, (dialog, which, inputText, isChecked) -> { if (!TextUtils.isEmpty(inputText)) { String[] newComponents = inputText.toString().split("\\s+"); mModel.setComponents(newComponents); } else mModel.setComponents(null); updateComponentsPref(componentsPref); }) .setNegativeButton(R.string.disable, (dialog, which, inputText, isChecked) -> { mModel.setComponents(null); updateComponentsPref(componentsPref); }) .show(); return true; }); // Set app ops Preference appOpsPref = Objects.requireNonNull(findPreference("app_ops")); updateAppOpsPref(appOpsPref); appOpsPref.setOnPreferenceClickListener(preference -> { new TextInputDialogBuilder(mActivity, R.string.input_app_ops) .setTitle(R.string.app_ops) .setInputText(mAppOps == null ? "" : TextUtils.join(" ", mAppOps)) .setHelperText(R.string.input_app_ops_description_profile) .setPositiveButton(R.string.ok, (dialog, which, inputText, isChecked) -> { if (!TextUtils.isEmpty(inputText)) { String[] newAppOps = inputText.toString().split("\\s+"); mModel.setAppOps(newAppOps); } else mModel.setAppOps(null); updateAppOpsPref(appOpsPref); }) .setNegativeButton(R.string.disable, (dialog, which, inputText, isChecked) -> { mModel.setAppOps(null); updateAppOpsPref(appOpsPref); }) .show(); return true; }); // Set permissions Preference permissionsPref = Objects.requireNonNull(findPreference("permissions")); updatePermissionsPref(permissionsPref); permissionsPref.setOnPreferenceClickListener(preference -> { new TextInputDialogBuilder(mActivity, R.string.input_permissions) .setTitle(R.string.declared_permission) .setInputText(mPermissions == null ? "" : TextUtils.join(" ", mPermissions)) .setHelperText(R.string.input_permissions_description) .setPositiveButton(R.string.ok, (dialog, which, inputText, isChecked) -> { if (!TextUtils.isEmpty(inputText)) { String[] newPermissions = inputText.toString().split("\\s+"); mModel.setPermissions(newPermissions); } else mModel.setPermissions(null); updatePermissionsPref(permissionsPref); }) .setNegativeButton(R.string.disable, (dialog, which, inputText, isChecked) -> { mModel.setPermissions(null); updatePermissionsPref(permissionsPref); }) .show(); return true; }); Preference backupDataPref = Objects.requireNonNull(findPreference("backup_data")); mBackupInfo = mModel.getBackupInfo(); backupDataPref.setSummary(mBackupInfo != null ? R.string.enabled : R.string.disabled_app); backupDataPref.setOnPreferenceClickListener(preference -> { View view = View.inflate(mActivity, R.layout.dialog_profile_backup_restore, null); final BackupFlags flags; if (mBackupInfo != null) { flags = new BackupFlags(mBackupInfo.flags); } else flags = BackupFlags.fromPref(); final AtomicInteger backupFlags = new AtomicInteger(flags.getFlags()); view.findViewById(R.id.dialog_button).setOnClickListener(v -> { List supportedBackupFlags = BackupFlags.getSupportedBackupFlagsAsArray(); new SearchableMultiChoiceDialogBuilder<>(requireActivity(), supportedBackupFlags, BackupFlags.getFormattedFlagNames(requireContext(), supportedBackupFlags)) .setTitle(R.string.backup_options) .addSelections(flags.flagsToCheckedIndexes(supportedBackupFlags)) .hideSearchBar(true) .showSelectAll(false) .setPositiveButton(R.string.save, (dialog, which, selectedItems) -> { int flagsInt = 0; for (int flag : selectedItems) { flagsInt |= flag; } flags.setFlags(flagsInt); backupFlags.set(flags.getFlags()); }) .setNegativeButton(R.string.cancel, null) .show(); }); final TextInputEditText editText = view.findViewById(android.R.id.input); if (mBackupInfo != null) { editText.setText(mBackupInfo.name); } new MaterialAlertDialogBuilder(mActivity) .setTitle(R.string.backup_restore) .setView(view) .setPositiveButton(R.string.ok, (dialog, which) -> { if (mBackupInfo == null) { mBackupInfo = new AppsBaseProfile.BackupInfo(); } CharSequence backupName = editText.getText(); BackupFlags backupFlags1 = new BackupFlags(backupFlags.get()); if (!TextUtils.isEmpty(backupName)) { backupFlags1.addFlag(BackupFlags.BACKUP_MULTIPLE); mBackupInfo.name = backupName.toString(); } else { backupFlags1.removeFlag(BackupFlags.BACKUP_MULTIPLE); mBackupInfo.name = null; } mBackupInfo.flags = backupFlags1.getFlags(); mModel.setBackupInfo(mBackupInfo); backupDataPref.setSummary(R.string.enabled); }) .setNegativeButton(R.string.disable, (dialog, which) -> { mModel.setBackupInfo(mBackupInfo = null); backupDataPref.setSummary(R.string.disabled_app); }) .show(); return true; }); // Set export rules Preference exportRulesPref = Objects.requireNonNull(findPreference("export_rules")); int rulesCount = RulesTypeSelectionDialogFragment.RULE_TYPES.length; List checkedItems = new ArrayList<>(rulesCount); List selectedRules = updateExportRulesPref(exportRulesPref); for (int i = 0; i < rulesCount; ++i) checkedItems.add(1 << i); exportRulesPref.setOnPreferenceClickListener(preference -> { new SearchableMultiChoiceDialogBuilder<>(mActivity, checkedItems, R.array.rule_types) .setTitle(R.string.options) .hideSearchBar(true) .addSelections(selectedRules) .setPositiveButton(R.string.ok, (dialog, which, selectedItems) -> { int value = 0; for (int item : selectedItems) value |= item; if (value != 0) { mModel.setExportRules(value); } else mModel.setExportRules(null); selectedRules.clear(); selectedRules.addAll(updateExportRulesPref(exportRulesPref)); }) .setNegativeButton(R.string.disable, (dialog, which, selectedItems) -> { mModel.setExportRules(null); selectedRules.clear(); selectedRules.addAll(updateExportRulesPref(exportRulesPref)); }) .show(); return true; }); // Set others ((SwitchPreferenceCompat) Objects.requireNonNull(findPreference("freeze"))) .setChecked(mModel.getBoolean("freeze", false)); ((SwitchPreferenceCompat) Objects.requireNonNull(findPreference("force_stop"))) .setChecked(mModel.getBoolean("force_stop", false)); ((SwitchPreferenceCompat) Objects.requireNonNull(findPreference("clear_cache"))) .setChecked(mModel.getBoolean("clear_cache", false)); ((SwitchPreferenceCompat) Objects.requireNonNull(findPreference("clear_data"))) .setChecked(mModel.getBoolean("clear_data", false)); ((SwitchPreferenceCompat) Objects.requireNonNull(findPreference("block_trackers"))) .setChecked(mModel.getBoolean("block_trackers", false)); ((SwitchPreferenceCompat) Objects.requireNonNull(findPreference("save_apk"))) .setChecked(mModel.getBoolean("save_apk", false)); ((SwitchPreferenceCompat) Objects.requireNonNull(findPreference("allow_routine"))) .setChecked(mModel.getBoolean("allow_routine", false)); } @NonNull private List updateExportRulesPref(Preference pref) { Integer rules = mModel.getExportRules(); List selectedRules = new ArrayList<>(); if (rules == null || rules == 0) pref.setSummary(R.string.disabled_app); else { List selectedRulesStr = new ArrayList<>(); int i = 0; while (rules != 0) { int flag = (rules & (~(1 << i))); if (flag != rules) { selectedRulesStr.add(RulesTypeSelectionDialogFragment.RULE_TYPES[i].toString()); rules = flag; selectedRules.add(1 << i); } ++i; } pref.setSummary(TextUtils.join(", ", selectedRulesStr)); } return selectedRules; } private void updateComponentsPref(Preference pref) { mComponents = mModel.getComponents(); if (mComponents == null || mComponents.length == 0) pref.setSummary(R.string.disabled_app); else { pref.setSummary(TextUtils.join(", ", mComponents)); } } private void updateAppOpsPref(Preference pref) { mAppOps = mModel.getAppOpsStr(); if (mAppOps == null || mAppOps.length == 0) pref.setSummary(R.string.disabled_app); else { pref.setSummary(TextUtils.join(", ", mAppOps)); } } private void updatePermissionsPref(Preference pref) { mPermissions = mModel.getPermissions(); if (mPermissions == null || mPermissions.length == 0) pref.setSummary(R.string.disabled_app); else { pref.setSummary(TextUtils.join(", ", mPermissions)); } } private List mSelectedUsers; private void handleUsersPref(Preference pref) { List users = Users.getUsers(); if (users.size() > 1) { pref.setVisible(true); CharSequence[] userNames = new String[users.size()]; List userHandles = new ArrayList<>(users.size()); int i = 0; for (UserInfo info : users) { userNames[i] = info.toLocalizedString(requireContext()); userHandles.add(info.id); ++i; } mSelectedUsers = new ArrayList<>(); for (Integer user : mModel.getUsers()) { mSelectedUsers.add(user); } mActivity.runOnUiThread(() -> { pref.setSummary(TextUtilsCompat.joinSpannable(", ", getUserInfo(users, mSelectedUsers))); pref.setOnPreferenceClickListener(v -> { new SearchableMultiChoiceDialogBuilder<>(mActivity, userHandles, userNames) .setTitle(R.string.select_user) .addSelections(mSelectedUsers) .showSelectAll(false) .setPositiveButton(R.string.ok, (dialog, which, selectedUserHandles) -> { if (selectedUserHandles.isEmpty()) { mSelectedUsers = userHandles; } else mSelectedUsers = selectedUserHandles; pref.setSummary(TextUtilsCompat.joinSpannable(", ", getUserInfo(users, mSelectedUsers))); mModel.setUsers(ArrayUtils.convertToIntArray(mSelectedUsers)); }) .setNegativeButton(R.string.cancel, null) .show(); return true; }); }); } else { mActivity.runOnUiThread(() -> pref.setVisible(false)); } } @NonNull private List getUserInfo(@NonNull List userInfoList, @NonNull List userHandles) { List userInfoOut = new ArrayList<>(); for (UserInfo info : userInfoList) { if (userHandles.contains(info.id)) { userInfoOut.add(info.toLocalizedString(requireContext())); } } return userInfoOut; } public class ConfDataStore extends PreferenceDataStore { @Override public void putBoolean(@NonNull String key, boolean value) { mModel.putBoolean(key, value); } @Override public boolean getBoolean(@NonNull String key, boolean defValue) { return mModel.getBoolean(key, defValue); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/profiles/LogViewerFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.profiles; import android.graphics.Typeface; import android.os.Bundle; import android.text.SpannableString; import android.text.Spanned; import android.text.style.StyleSpan; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatEditText; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.util.UiUtils; public class LogViewerFragment extends Fragment { @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_log_viewer, container, false); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { AppsProfileViewModel model = new ViewModelProvider(requireActivity()).get(AppsProfileViewModel.class); AppCompatEditText tv = view.findViewById(R.id.log_content); tv.setKeyListener(null); ExtendedFloatingActionButton efab = view.findViewById(R.id.floatingActionButton); UiUtils.applyWindowInsetsAsMargin(efab, false, true); efab.setOnClickListener(v -> { ProfileLogger.clearLogs(model.getProfileId()); tv.setText(""); }); model.getLogs().observe(getViewLifecycleOwner(), logs -> tv.setText(getFormattedLogs(logs))); model.observeProfileLoaded().observe(getViewLifecycleOwner(), profileName -> model.loadLogs()); } @Override public void onResume() { AppsBaseProfileActivity activity = (AppsBaseProfileActivity) requireActivity(); if (activity.getSupportActionBar() != null) { activity.getSupportActionBar().setSubtitle(R.string.log_viewer); } activity.fab.hide(); super.onResume(); } public CharSequence getFormattedLogs(String logs) { SpannableString str = new SpannableString(logs); int fIndex = 0; while(true) { fIndex = logs.indexOf("====> ", fIndex); if (fIndex == -1) { return str; } int lIndex = logs.indexOf('\n', fIndex); str.setSpan(new StyleSpan(Typeface.BOLD), fIndex, lIndex, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); fIndex = lIndex; } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/profiles/NewProfileDialogFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.profiles; import android.app.Dialog; import android.os.Bundle; import android.text.Editable; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.DialogFragment; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.textfield.TextInputEditText; import com.google.android.material.textfield.TextInputLayout; import java.lang.ref.WeakReference; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.profiles.struct.BaseProfile; import io.github.muntashirakon.adapters.SelectedArrayAdapter; import io.github.muntashirakon.lifecycle.SoftInputLifeCycleObserver; import io.github.muntashirakon.view.TextInputLayoutCompat; import io.github.muntashirakon.widget.MaterialSpinner; public class NewProfileDialogFragment extends DialogFragment { public static final String TAG = NewProfileDialogFragment.class.getSimpleName(); public interface OnCreateNewProfileInterface { void onCreateNewProfile(@NonNull String newProfileName, @BaseProfile.ProfileType int type); } @NonNull public static NewProfileDialogFragment getInstance(@Nullable OnCreateNewProfileInterface createNewProfileInterface) { NewProfileDialogFragment fragment = new NewProfileDialogFragment(); fragment.setOnCreateNewProfileInterface(createNewProfileInterface); return fragment; } @Nullable private OnCreateNewProfileInterface mOnCreateNewProfileInterface; private View mDialogView; private TextInputEditText mEditText; private int mType; @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { mDialogView = View.inflate(requireActivity(), R.layout.dialog_new_file, null); mEditText = mDialogView.findViewById(R.id.name); String name = "Untitled profile"; mEditText.setText(name); mEditText.selectAll(); TextInputLayout editTextLayout = TextInputLayoutCompat.fromTextInputEditText(mEditText); editTextLayout.setHelperText(requireContext().getText(R.string.input_profile_name_description)); MaterialSpinner spinner = mDialogView.findViewById(R.id.type_selector_spinner); ArrayAdapter spinnerAdapter = SelectedArrayAdapter.createFromResource(requireContext(), R.array.profile_types, io.github.muntashirakon.ui.R.layout.auto_complete_dropdown_item); spinner.setAdapter(spinnerAdapter); spinner.setSelection(BaseProfile.PROFILE_TYPE_APPS); spinner.setOnItemClickListener((parent, view, position, id) -> { if (mType != position) { mType = position; } }); return new MaterialAlertDialogBuilder(requireActivity()) .setTitle(R.string.new_profile) .setView(mDialogView) .setPositiveButton(R.string.go, (dialog, which) -> { Editable editable = mEditText.getText(); if (!TextUtils.isEmpty(editable) && mOnCreateNewProfileInterface != null) { mOnCreateNewProfileInterface.onCreateNewProfile(editable.toString(), mType); } }) .setNegativeButton(R.string.cancel, null) .create(); } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return mDialogView; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { getLifecycle().addObserver(new SoftInputLifeCycleObserver(new WeakReference<>(mEditText))); } public void setOnCreateNewProfileInterface(@Nullable OnCreateNewProfileInterface createNewProfileInterface) { mOnCreateNewProfileInterface = createNewProfileInterface; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/profiles/ProfileApplierActivity.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.profiles; import android.app.Application; import android.content.Context; import android.content.Intent; import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringDef; import androidx.core.content.ContextCompat; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModelProvider; import org.json.JSONException; import java.io.IOException; import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.Queue; import io.github.muntashirakon.AppManager.BaseActivity; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.profiles.struct.BaseProfile; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.dialog.DialogTitleBuilder; import io.github.muntashirakon.dialog.SearchableSingleChoiceDialogBuilder; import io.github.muntashirakon.io.Path; public class ProfileApplierActivity extends BaseActivity { private static final String EXTRA_SHORTCUT_TYPE = "shortcut"; public static final String EXTRA_PROFILE_ID = "prof"; public static final String EXTRA_STATE = "state"; private static final String EXTRA_NOTIFY = "notify"; @StringDef({ST_SIMPLE, ST_ADVANCED}) public @interface ShortcutType { } public static final String ST_SIMPLE = "simple"; public static final String ST_ADVANCED = "advanced"; @NonNull public static Intent getShortcutIntent(@NonNull Context context, @NonNull String profileId, @ShortcutType @Nullable String shortcutType, @Nullable String state) { // Compatibility: Old shortcuts still store profile name instead of profile ID. String realProfileId = ProfileManager.getProfileIdCompat(profileId); Intent intent = new Intent(context, ProfileApplierActivity.class); intent.putExtra(EXTRA_PROFILE_ID, realProfileId); if (shortcutType == null) { if (state != null) { // State => It's a simple shortcut intent.putExtra(EXTRA_SHORTCUT_TYPE, ST_SIMPLE); intent.putExtra(EXTRA_STATE, state); } else { // Otherwise it's an advance shortcut intent.putExtra(EXTRA_SHORTCUT_TYPE, ST_ADVANCED); } } else { intent.putExtra(EXTRA_SHORTCUT_TYPE, shortcutType); if (state != null) { intent.putExtra(EXTRA_STATE, state); } } return intent; } @NonNull public static Intent getAutomationIntent(@NonNull Context context, @NonNull String profileId, @Nullable String state) { // Compatibility: Old shortcuts still store profile name instead of profile ID. String realProfileId = ProfileManager.getProfileIdCompat(profileId); Intent intent = new Intent(context, ProfileApplierActivity.class); intent.putExtra(EXTRA_PROFILE_ID, realProfileId); if (state != null) { // State => Automatic trigger intent.putExtra(EXTRA_SHORTCUT_TYPE, ST_SIMPLE); intent.putExtra(EXTRA_STATE, state); // Avoid issuing completion notification intent.putExtra(EXTRA_NOTIFY, false); } else { // Manual trigger intent.putExtra(EXTRA_SHORTCUT_TYPE, ST_ADVANCED); } return intent; } @NonNull public static Intent getApplierIntent(@NonNull Context context, @NonNull String profileId) { // Compatibility: Old shortcuts still store profile name instead of profile ID. String realProfileId = ProfileManager.getProfileIdCompat(profileId); Intent intent = new Intent(context, ProfileApplierActivity.class); intent.putExtra(EXTRA_PROFILE_ID, realProfileId); intent.putExtra(EXTRA_SHORTCUT_TYPE, ST_ADVANCED); return intent; } public static class ProfileApplierInfo { public BaseProfile profile; public String profileId; @ShortcutType public String shortcutType; @Nullable public String state; public boolean notify; } private final Queue mQueue = new LinkedList<>(); private ProfileApplierViewModel mViewModel; @Override public boolean getTransparentBackground() { return true; } @Override protected void onAuthenticated(@Nullable Bundle savedInstanceState) { mViewModel = new ViewModelProvider(this).get(ProfileApplierViewModel.class); synchronized (mQueue) { mQueue.add(getIntent()); } mViewModel.mProfileLiveData.observe(this, this::handleShortcut); next(); } @Override protected void onNewIntent(@NonNull Intent intent) { synchronized (mQueue) { mQueue.add(intent); } super.onNewIntent(intent); } private void next() { Intent intent; synchronized (mQueue) { intent = mQueue.poll(); } if (intent == null) { finish(); return; } @ShortcutType String shortcutType = intent.getStringExtra(EXTRA_SHORTCUT_TYPE); String profileId = intent.getStringExtra(EXTRA_PROFILE_ID); String profileState = intent.getStringExtra(EXTRA_STATE); boolean notify = intent.getBooleanExtra(EXTRA_NOTIFY, true); if (shortcutType == null || profileId == null) { // Invalid shortcut return; } ProfileApplierInfo info = new ProfileApplierInfo(); info.profileId = profileId; info.shortcutType = shortcutType; info.state = profileState; info.notify = notify; mViewModel.loadProfile(info); } private void handleShortcut(@Nullable ProfileApplierInfo info) { if (info == null) { next(); return; } info.state = info.state != null ? info.state : info.profile.state; switch (info.shortcutType) { case ST_SIMPLE: Intent intent = ProfileApplierService.getIntent(this, ProfileQueueItem.fromProfiledApplierInfo(info), info.notify); ContextCompat.startForegroundService(this, intent); next(); break; case ST_ADVANCED: final String[] statesL = new String[]{ getString(R.string.on), getString(R.string.off) }; @BaseProfile.ProfileState final List states = Arrays.asList(BaseProfile.STATE_ON, BaseProfile.STATE_OFF); DialogTitleBuilder titleBuilder = new DialogTitleBuilder(this) .setTitle(getString(R.string.apply_profile, info.profile.name)) .setSubtitle(R.string.choose_a_profile_state); new SearchableSingleChoiceDialogBuilder<>(this, states, statesL) .setTitle(titleBuilder.build()) .setSelection(info.state) .setPositiveButton(R.string.ok, (dialog, which, selectedState) -> { info.state = selectedState; Intent aIntent = ProfileApplierService.getIntent(this, ProfileQueueItem.fromProfiledApplierInfo(info), info.notify); ContextCompat.startForegroundService(this, aIntent); }) .setNegativeButton(R.string.cancel, null) .setOnDismissListener(dialog -> next()) .show(); break; default: next(); } } public static class ProfileApplierViewModel extends AndroidViewModel { final MutableLiveData mProfileLiveData = new MutableLiveData<>(); public ProfileApplierViewModel(@NonNull Application application) { super(application); } public void loadProfile(ProfileApplierInfo info) { ThreadUtils.postOnBackgroundThread(() -> { Path profilePath = ProfileManager.findProfilePathById(info.profileId); try { info.profile = BaseProfile.fromPath(profilePath); mProfileLiveData.postValue(info); } catch (IOException | JSONException e) { e.printStackTrace(); } }); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/profiles/ProfileApplierService.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.profiles; import static io.github.muntashirakon.AppManager.history.ops.OpHistoryManager.HISTORY_TYPE_PROFILE; import android.app.Activity; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.os.PowerManager; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.NotificationManagerCompat; import androidx.core.app.PendingIntentCompat; import androidx.core.app.ServiceCompat; import java.io.IOException; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.batchops.BatchOpsResultsActivity; import io.github.muntashirakon.AppManager.batchops.BatchOpsService; import io.github.muntashirakon.AppManager.history.ops.OpHistoryManager; import io.github.muntashirakon.AppManager.intercept.IntentCompat; import io.github.muntashirakon.AppManager.progress.NotificationProgressHandler; import io.github.muntashirakon.AppManager.progress.NotificationProgressHandler.NotificationManagerInfo; import io.github.muntashirakon.AppManager.progress.ProgressHandler; import io.github.muntashirakon.AppManager.progress.QueuedProgressHandler; import io.github.muntashirakon.AppManager.types.ForegroundService; import io.github.muntashirakon.AppManager.utils.CpuUtils; import io.github.muntashirakon.AppManager.utils.NotificationUtils; import io.github.muntashirakon.io.Path; public class ProfileApplierService extends ForegroundService { private static final String EXTRA_QUEUE_ITEM = "queue_item"; private static final String EXTRA_NOTIFY = "notify"; /** * Notification channel ID */ private static final String CHANNEL_ID = BuildConfig.APPLICATION_ID + ".channel.PROFILE_APPLIER"; @NonNull public static Intent getIntent(@NonNull Context context, @NonNull ProfileQueueItem queueItem, boolean notify) { Intent intent = new Intent(context, ProfileApplierService.class); IntentCompat.putWrappedParcelableExtra(intent, EXTRA_QUEUE_ITEM, queueItem); intent.putExtra(EXTRA_NOTIFY, notify); return intent; } private QueuedProgressHandler mProgressHandler; private NotificationProgressHandler.NotificationInfo mNotificationInfo; private PowerManager.WakeLock mWakeLock; public ProfileApplierService() { super("ProfileApplierService"); } @Override public void onCreate() { super.onCreate(); mWakeLock = CpuUtils.getPartialWakeLock("profile_applier"); mWakeLock.acquire(); } @Override public int onStartCommand(@Nullable Intent intent, int flags, int startId) { if (isWorking()) return super.onStartCommand(intent, flags, startId); NotificationManagerInfo notificationManagerInfo = new NotificationManagerInfo(CHANNEL_ID, "Profile Applier", NotificationManagerCompat.IMPORTANCE_LOW); mProgressHandler = new NotificationProgressHandler(this, notificationManagerInfo, NotificationUtils.HIGH_PRIORITY_NOTIFICATION_INFO, NotificationUtils.HIGH_PRIORITY_NOTIFICATION_INFO); mProgressHandler.setProgressTextInterface(ProgressHandler.PROGRESS_REGULAR); mNotificationInfo = new NotificationProgressHandler.NotificationInfo() .setBody(getString(R.string.operation_running)) .setOperationName(getText(R.string.profiles)); mProgressHandler.onAttach(this, mNotificationInfo); return super.onStartCommand(intent, flags, startId); } @Override protected void onHandleIntent(@Nullable Intent intent) { ProfileQueueItem item = getQueueItem(intent); if (item == null) { return; } boolean notify = intent.getBooleanExtra(EXTRA_NOTIFY, true); Path tempProfilePath = item.getTempProfilePath(); try { ProfileManager profileManager = new ProfileManager(item.getProfileId(), tempProfilePath); profileManager.applyProfile(item.getState(), mProgressHandler); profileManager.conclude(); OpHistoryManager.addHistoryItem(HISTORY_TYPE_PROFILE, item, true); sendNotification(item.getProfileName(), Activity.RESULT_OK, notify, profileManager.requiresRestart()); } catch (IOException e) { sendNotification(item.getProfileName(), Activity.RESULT_CANCELED, notify, false); } finally { if (tempProfilePath != null) { tempProfilePath.delete(); } } } @Override protected void onQueued(@Nullable Intent intent) { ProfileQueueItem item = getQueueItem(intent); if (item == null) { return; } Object notificationInfo = new NotificationProgressHandler.NotificationInfo() .setAutoCancel(true) .setTime(System.currentTimeMillis()) .setOperationName(getText(R.string.profiles)) .setTitle(item.getProfileName()) .setBody(getString(R.string.added_to_queue)); mProgressHandler.onQueue(notificationInfo); } @Override protected void onStartIntent(@Nullable Intent intent) { ProfileQueueItem item = getQueueItem(intent); if (item != null) { Intent notificationIntent = ProfileManager.getProfileIntent(this, item.getProfileType(), item.getProfileId()); PendingIntent pendingIntent = PendingIntentCompat.getActivity(this, 0, notificationIntent, 0, false); mNotificationInfo.setDefaultAction(pendingIntent); } // Set profile name in the ongoing notification mNotificationInfo.setTitle(item != null ? item.getProfileName() : null); mProgressHandler.onProgressStart(-1, 0, mNotificationInfo); } @Override public void onDestroy() { ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE); if (mProgressHandler != null) { mProgressHandler.onDetach(this); } CpuUtils.releaseWakeLock(mWakeLock); super.onDestroy(); } @Nullable private ProfileQueueItem getQueueItem(@Nullable Intent intent) { if (intent == null) { return null; } return IntentCompat.getUnwrappedParcelableExtra(intent, EXTRA_QUEUE_ITEM, ProfileQueueItem.class); } private void sendNotification(@NonNull String profileName, int result, boolean notify, boolean requiresRestart) { NotificationProgressHandler.NotificationInfo notificationInfo = new NotificationProgressHandler .NotificationInfo() .setAutoCancel(true) .setTime(System.currentTimeMillis()) .setOperationName(getText(R.string.profiles)) .setTitle(profileName); switch (result) { case Activity.RESULT_CANCELED: // Failure notificationInfo.setBody(getString(R.string.error)); break; case Activity.RESULT_OK: // Successful notificationInfo.setBody(getString(R.string.the_operation_was_successful)); } if (requiresRestart) { Intent intent = new Intent(this, BatchOpsResultsActivity.class); intent.putExtra(BatchOpsService.EXTRA_REQUIRES_RESTART, true); PendingIntent pendingIntent = PendingIntentCompat.getActivity(this, 0, intent, PendingIntent.FLAG_ONE_SHOT, false); notificationInfo.addAction(0, getString(R.string.restart_device), pendingIntent); } mProgressHandler.onResult(notify ? notificationInfo : null); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/profiles/ProfileLogger.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.profiles; import androidx.annotation.NonNull; import java.io.File; import java.io.IOException; import io.github.muntashirakon.AppManager.logs.Logger; import io.github.muntashirakon.io.Paths; public class ProfileLogger extends Logger { @NonNull public static File getLogFile(@NonNull String profileId) { return new File(getLoggingDirectory(), "profile_" + profileId + ".log"); } public ProfileLogger(@NonNull String profileId) throws IOException { super(getLogFile(profileId), true); } @NonNull public static String getAllLogs(@NonNull String profileId) { return Paths.get(getLogFile(profileId)).getContentAsString(); } public static void clearLogs(@NonNull String profileId) { getLogFile(profileId).delete(); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/profiles/ProfileManager.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.profiles; import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.json.JSONException; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Objects; import java.util.UUID; import io.github.muntashirakon.AppManager.profiles.struct.BaseProfile; import io.github.muntashirakon.AppManager.profiles.struct.ProfileApplierResult; import io.github.muntashirakon.AppManager.progress.ProgressHandler; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; public class ProfileManager { public static final String TAG = "ProfileManager"; public static final String PROFILE_EXT = ".am.json"; @NonNull public static Intent getProfileIntent(@NonNull Context context, @BaseProfile.ProfileType int type, @NonNull String profileId) { if (type == BaseProfile.PROFILE_TYPE_APPS) { return AppsProfileActivity.getProfileIntent(context, profileId); } else if (type == BaseProfile.PROFILE_TYPE_APPS_FILTER) { return AppsFilterProfileActivity.getProfileIntent(context, profileId); } else throw new UnsupportedOperationException("Invalid type: " + type); } @NonNull public static Intent getNewProfileIntent(@NonNull Context context, @BaseProfile.ProfileType int type, @NonNull String profileName) { if (type == BaseProfile.PROFILE_TYPE_APPS) { return AppsProfileActivity.getNewProfileIntent(context, profileName); } else if (type == BaseProfile.PROFILE_TYPE_APPS_FILTER) { return AppsFilterProfileActivity.getNewProfileIntent(context, profileName); } else throw new UnsupportedOperationException("Invalid type: " + type); } @NonNull public static Intent getCloneProfileIntent(@NonNull Context context, @BaseProfile.ProfileType int type, @NonNull String oldProfileId, @NonNull String newProfileName) { if (type == BaseProfile.PROFILE_TYPE_APPS) { return AppsProfileActivity.getCloneProfileIntent(context, oldProfileId, newProfileName); } else if (type == BaseProfile.PROFILE_TYPE_APPS_FILTER) { return AppsFilterProfileActivity.getCloneProfileIntent(context, oldProfileId, newProfileName); } else throw new UnsupportedOperationException("Invalid type: " + type); } @NonNull public static Path getProfilesDir() { Context context = ContextUtils.getContext(); return Objects.requireNonNull(Paths.build(context.getFilesDir(), "profiles")); } @Nullable public static Path findProfilePathById(@NonNull String profileId) { return Paths.build(getProfilesDir(), profileId + PROFILE_EXT); } @NonNull public static Path requireProfilePathById(@NonNull String profileId) throws IOException { Path profilesDir = getProfilesDir(); if (!profilesDir.exists()) { profilesDir.mkdirs(); } return getProfilesDir().findOrCreateFile(profileId + PROFILE_EXT, null); } public static boolean deleteProfile(@NonNull String profileId) { Path profilePath = findProfilePathById(profileId); return profilePath == null || !profilePath.exists() || profilePath.delete(); } @NonNull public static String getProfileName(@NonNull String filename) { int index = filename.indexOf(PROFILE_EXT); if (index == -1) { // Maybe only ends with .json index = filename.indexOf(".json"); } return index != -1 ? filename.substring(0, index) : filename; } @NonNull public static ArrayList getProfileNames() { Path profilesPath = getProfilesDir(); String[] profilesFiles = profilesPath.listFileNames((dir, name) -> name.endsWith(PROFILE_EXT)); ArrayList profileNames = new ArrayList<>(profilesFiles.length); for (String profile : profilesFiles) { profileNames.add(getProfileName(profile)); } return profileNames; } @NonNull public static HashMap getProfileSummaries(@NonNull Context context) throws IOException, JSONException { Path profilesPath = getProfilesDir(); Path[] profilePaths = profilesPath.listFiles((dir, name) -> name.endsWith(PROFILE_EXT)); HashMap profiles = new HashMap<>(profilePaths.length); for (Path profilePath : profilePaths) { if (ThreadUtils.isInterrupted()) { // Thread interrupted, return as is return profiles; } BaseProfile profile = BaseProfile.fromPath(profilePath); profiles.put(profile, profile.toLocalizedString(context)); } return profiles; } @NonNull public static List getProfiles(int type) throws IOException, JSONException { Path profilesPath = getProfilesDir(); Path[] profilePaths = profilesPath.listFiles((dir, name) -> name.endsWith(PROFILE_EXT)); List profiles = new ArrayList<>(profilePaths.length); for (Path profilePath : profilePaths) { BaseProfile profile = BaseProfile.fromPath(profilePath); if (profile.type == type) { profiles.add((T) profile); } } return profiles; } @NonNull public static List getProfiles() throws IOException, JSONException { Path profilesPath = getProfilesDir(); Path[] profilePaths = profilesPath.listFiles((dir, name) -> name.endsWith(PROFILE_EXT)); List profiles = new ArrayList<>(profilePaths.length); for (Path profilePath : profilePaths) { profiles.add(BaseProfile.fromPath(profilePath)); } return profiles; } @NonNull public static String getProfileIdCompat(@NonNull String profileName) { String profileId = Paths.sanitizeFilename(profileName, "_", Paths.SANITIZE_FLAG_SPACE | Paths.SANITIZE_FLAG_UNIX_ILLEGAL_CHARS | Paths.SANITIZE_FLAG_UNIX_RESERVED | Paths.SANITIZE_FLAG_FAT_ILLEGAL_CHARS); return profileId != null ? profileId : UUID.randomUUID().toString(); } @NonNull private final BaseProfile mProfile; @Nullable private ProfileLogger mLogger; private boolean mRequiresRestart; public ProfileManager(@NonNull String profileId, @Nullable Path profilePath) throws IOException { try { mLogger = new ProfileLogger(profileId); } catch (IOException e) { e.printStackTrace(); } try { Path realProfilePath = profilePath != null ? profilePath : findProfilePathById(profileId); mProfile = BaseProfile.fromPath(realProfilePath); } catch (IOException e) { if (mLogger != null) { mLogger.println(null, e); } throw e; } catch (JSONException e) { if (mLogger != null) { mLogger.println(null, e); } throw new IOException(e); } } public boolean requiresRestart() { return mRequiresRestart; } @SuppressLint("SwitchIntDef") public void applyProfile(@Nullable String state, @Nullable ProgressHandler progressHandler) { // Set state if (state == null) state = mProfile.state; log("====> Started execution with state " + state); ProfileApplierResult result = mProfile.apply(state, mLogger, progressHandler); mRequiresRestart = result.requiresRestart(); log("====> Execution completed."); } public void conclude() { if (mLogger != null) { mLogger.close(); } } private void log(@Nullable String message) { if (mLogger != null) { mLogger.println(message); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/profiles/ProfileQueueItem.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.profiles; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.os.ParcelCompat; import org.json.JSONException; import org.json.JSONObject; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.Objects; import io.github.muntashirakon.AppManager.history.IJsonSerializer; import io.github.muntashirakon.AppManager.history.JsonDeserializer; import io.github.muntashirakon.AppManager.profiles.ProfileApplierActivity.ProfileApplierInfo; import io.github.muntashirakon.AppManager.profiles.struct.BaseProfile; import io.github.muntashirakon.AppManager.self.filecache.FileCache; import io.github.muntashirakon.AppManager.utils.JSONUtils; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; public class ProfileQueueItem implements Parcelable, IJsonSerializer { @NonNull public static ProfileQueueItem fromProfiledApplierInfo(@NonNull ProfileApplierInfo info) { return new ProfileQueueItem(info.profile, info.state); } @NonNull private final String mProfileId; @BaseProfile.ProfileType private final int mProfileType; @NonNull private final String mProfileName; @Nullable private final String mState; @Nullable private final Path mTempProfilePath; private ProfileQueueItem(@NonNull BaseProfile profile, @Nullable String state) { mProfileId = profile.profileId; mProfileType = profile.type; mProfileName = profile.name; mState = state; mTempProfilePath = null; } protected ProfileQueueItem(@NonNull Parcel in) { mProfileId = Objects.requireNonNull(in.readString()); mProfileType = in.readInt(); mProfileName = Objects.requireNonNull(in.readString()); mState = in.readString(); Uri uri = ParcelCompat.readParcelable(in, Uri.class.getClassLoader(), Uri.class); mTempProfilePath = uri != null ? Paths.get(uri) : null; } @NonNull public String getProfileId() { return mProfileId; } @BaseProfile.ProfileType public int getProfileType() { return mProfileType; } @NonNull public String getProfileName() { return mProfileName; } @Nullable public String getState() { return mState; } @Nullable public Path getTempProfilePath() { return mTempProfilePath; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeString(mProfileId); dest.writeInt(mProfileType); dest.writeString(mProfileName); dest.writeString(mState); dest.writeParcelable(mTempProfilePath != null ? mTempProfilePath.getUri() : null, flags); } protected ProfileQueueItem(@NonNull JSONObject jsonObject) throws JSONException { mProfileId = jsonObject.getString("profile_id"); mProfileType = jsonObject.optInt("profile_type", BaseProfile.PROFILE_TYPE_APPS); mProfileName = jsonObject.getString("profile_name"); mState = JSONUtils.getString(jsonObject, "state"); JSONObject profile = jsonObject.optJSONObject("profile"); File profilePath = null; if (profile != null) { try (InputStream is = new ByteArrayInputStream(profile.toString().getBytes(StandardCharsets.UTF_8))) { profilePath = FileCache.getGlobalFileCache().getCachedFile(is, ProfileManager.PROFILE_EXT); } catch (IOException e) { //noinspection UnnecessaryInitCause throw (JSONException) new JSONException(e.getMessage()).initCause(e); } } mTempProfilePath = profilePath != null ? Paths.get(profilePath) : null; } @NonNull @Override public JSONObject serializeToJson() throws JSONException { JSONObject jsonObject = new JSONObject(); jsonObject.put("profile_id", mProfileId); jsonObject.put("profile_type", mProfileType); jsonObject.put("profile_name", mProfileName); jsonObject.put("state", mState); // A profile can be altered any time. So, we need to store a snapshot of the profile try { BaseProfile profile = BaseProfile.fromPath(ProfileManager.findProfilePathById(mProfileId)); jsonObject.put("profile", profile.serializeToJson()); } catch (IOException e) { //noinspection UnnecessaryInitCause throw (JSONException) new JSONException(e.getMessage()).initCause(e); } return jsonObject; } public static final JsonDeserializer.Creator DESERIALIZER = ProfileQueueItem::new; public static final Creator CREATOR = new Creator() { @NonNull @Override public ProfileQueueItem createFromParcel(@NonNull Parcel in) { return new ProfileQueueItem(in); } @NonNull @Override public ProfileQueueItem[] newArray(int size) { return new ProfileQueueItem[size]; } }; } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/profiles/ProfileShortcutInfo.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.profiles; import android.content.Context; import android.content.Intent; import android.os.Parcel; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import io.github.muntashirakon.AppManager.shortcut.ShortcutInfo; public class ProfileShortcutInfo extends ShortcutInfo { public final String profileId; @ProfileApplierActivity.ShortcutType public final String shortcutType; public ProfileShortcutInfo(@NonNull String profileId, @NonNull String profileName, @ProfileApplierActivity.ShortcutType String shortcutType, @Nullable CharSequence readableShortcutType) { this.profileId = profileId; this.shortcutType = shortcutType; setName(profileName + " - " + (readableShortcutType != null ? readableShortcutType : shortcutType)); } protected ProfileShortcutInfo(Parcel in) { super(in); profileId = in.readString(); shortcutType = in.readString(); } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeString(profileId); dest.writeString(shortcutType); } @Override public Intent toShortcutIntent(@NonNull Context context) { Intent intent = ProfileApplierActivity.getShortcutIntent(context, profileId, shortcutType, null); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); return intent; } public static final Creator CREATOR = new Creator() { @Override public ProfileShortcutInfo createFromParcel(Parcel source) { return new ProfileShortcutInfo(source); } @Override public ProfileShortcutInfo[] newArray(int size) { return new ProfileShortcutInfo[size]; } }; } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/profiles/ProfilesActivity.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.profiles; import static io.github.muntashirakon.AppManager.profiles.ProfileApplierActivity.ST_ADVANCED; import static io.github.muntashirakon.AppManager.profiles.ProfileApplierActivity.ST_SIMPLE; import android.content.Intent; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.Filter; import android.widget.Filterable; import android.widget.TextView; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.PopupMenu; import androidx.core.content.ContextCompat; import androidx.lifecycle.ViewModelProvider; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.progressindicator.LinearProgressIndicator; import org.json.JSONException; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Objects; import io.github.muntashirakon.AppManager.BaseActivity; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.profiles.struct.BaseProfile; import io.github.muntashirakon.AppManager.shortcut.CreateShortcutDialogFragment; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.AppManager.utils.Utils; import io.github.muntashirakon.AppManager.utils.appearance.ColorCodes; import io.github.muntashirakon.dialog.SearchableSingleChoiceDialogBuilder; import io.github.muntashirakon.dialog.TextInputDialogBuilder; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; import io.github.muntashirakon.util.AdapterUtils; import io.github.muntashirakon.util.UiUtils; import io.github.muntashirakon.widget.RecyclerView; public class ProfilesActivity extends BaseActivity implements NewProfileDialogFragment.OnCreateNewProfileInterface { private static final String TAG = "ProfilesActivity"; private ProfilesAdapter mAdapter; private ProfilesViewModel mModel; private LinearProgressIndicator mProgressIndicator; @Nullable private String mProfileId; private final ActivityResultLauncher mExportProfile = registerForActivityResult( new ActivityResultContracts.CreateDocument("application/json"), uri -> { if (uri == null) { // Back button pressed. return; } if (mProfileId != null) { // Export profile try (OutputStream os = getContentResolver().openOutputStream(uri)) { if (os == null) { return; } Path profilePath = ProfileManager.findProfilePathById(mProfileId); BaseProfile profile = BaseProfile.fromPath(profilePath); profile.write(os); UIUtils.displayShortToast(R.string.the_export_was_successful); } catch (IOException | JSONException e) { Log.e(TAG, "Error: ", e); UIUtils.displayShortToast(R.string.export_failed); } } }); private final ActivityResultLauncher mImportProfile = registerForActivityResult( new ActivityResultContracts.GetContent(), uri -> { if (uri == null) { // Back button pressed. return; } try { // Verify Path profilePath = Paths.get(uri); BaseProfile profile = BaseProfile.fromPath(profilePath); BaseProfile newProfile = BaseProfile.newProfile(profile.name, profile.type, profile); Path innerProfilePath = ProfileManager.requireProfilePathById(newProfile.profileId); // Save try (OutputStream os = innerProfilePath.openOutputStream()) { newProfile.write(os); } UIUtils.displayShortToast(R.string.the_import_was_successful); // Load imported profile startActivity(ProfileManager.getProfileIntent(this, newProfile.type, newProfile.profileId)); } catch (IOException | JSONException e) { Log.e(TAG, "Error: ", e); UIUtils.displayShortToast(R.string.import_failed); } }); @Override protected void onAuthenticated(@Nullable Bundle savedInstanceState) { setContentView(R.layout.activity_profiles); setSupportActionBar(findViewById(R.id.toolbar)); mModel = new ViewModelProvider(this).get(ProfilesViewModel.class); mProgressIndicator = findViewById(R.id.progress_linear); mProgressIndicator.setVisibilityAfterHide(View.GONE); RecyclerView listView = findViewById(android.R.id.list); listView.setLayoutManager(UIUtils.getGridLayoutAt450Dp(this)); listView.setEmptyView(findViewById(android.R.id.empty)); UiUtils.applyWindowInsetsAsPaddingNoTop(listView); mAdapter = new ProfilesAdapter(this); listView.setAdapter(mAdapter); FloatingActionButton fab = findViewById(R.id.floatingActionButton); UiUtils.applyWindowInsetsAsMargin(fab); fab.setOnClickListener(v -> { NewProfileDialogFragment dialog = NewProfileDialogFragment.getInstance(this); dialog.show(getSupportFragmentManager(), NewProfileDialogFragment.TAG); }); mModel.getProfilesLiveData().observe(this, profiles -> { mProgressIndicator.hide(); mAdapter.setDefaultList(profiles); }); mProgressIndicator.show(); mModel.loadProfiles(); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.activity_profiles_actions, menu); return super.onCreateOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int id = item.getItemId(); if (id == android.R.id.home) { finish(); } else if (id == R.id.action_import) { mImportProfile.launch("application/json"); } else if (id == R.id.action_refresh) { mProgressIndicator.show(); mModel.loadProfiles(); } else return super.onOptionsItemSelected(item); return true; } @Override public void onCreateNewProfile(@NonNull String newProfileName, int type) { Intent intent = ProfileManager.getNewProfileIntent(this, type, newProfileName); startActivity(intent); } static class ProfilesAdapter extends RecyclerView.Adapter implements Filterable { private Filter mFilter; private String mConstraint; private BaseProfile[] mDefaultList; private BaseProfile[] mAdapterList; private HashMap mAdapterMap; private final ProfilesActivity mActivity; private final int mQueryStringHighlightColor; static class ViewHolder extends RecyclerView.ViewHolder { TextView title; TextView summary; public ViewHolder(@NonNull View itemView) { super(itemView); title = itemView.findViewById(android.R.id.title); summary = itemView.findViewById(android.R.id.summary); itemView.findViewById(R.id.icon_frame).setVisibility(View.GONE); } } ProfilesAdapter(@NonNull ProfilesActivity activity) { mActivity = activity; mQueryStringHighlightColor = ColorCodes.getQueryStringHighlightColor(activity); } void setDefaultList(@NonNull HashMap list) { mDefaultList = list.keySet().toArray(new BaseProfile[0]); int previousCount = getItemCount(); mAdapterList = mDefaultList; mAdapterMap = list; AdapterUtils.notifyDataSetChanged(this, previousCount, mAdapterList.length); } @Override public int getItemCount() { return mAdapterList == null ? 0 : mAdapterList.length; } @Override public long getItemId(int position) { return mAdapterList[position].hashCode(); } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(io.github.muntashirakon.ui.R.layout.m3_preference, parent, false); return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { BaseProfile profile = mAdapterList[position]; if (mConstraint != null && profile.name.toLowerCase(Locale.ROOT).contains(mConstraint)) { // Highlight searched query holder.title.setText(UIUtils.getHighlightedText(profile.name, mConstraint, mQueryStringHighlightColor)); } else { holder.title.setText(profile.name); } CharSequence value = mAdapterMap.get(profile); holder.summary.setText(value != null ? value : ""); holder.itemView.setOnClickListener(v -> { Intent intent = ProfileManager.getProfileIntent(mActivity, profile.type, profile.profileId); mActivity.startActivity(intent); }); holder.itemView.setOnLongClickListener(v -> { PopupMenu popupMenu = new PopupMenu(mActivity, v); popupMenu.setForceShowIcon(true); popupMenu.inflate(R.menu.activity_profiles_popup_actions); popupMenu.setOnMenuItemClickListener(item -> { int id = item.getItemId(); if (id == R.id.action_apply) { Intent intent = ProfileApplierActivity.getApplierIntent(mActivity, profile.profileId); mActivity.startActivity(intent); } else if (id == R.id.action_delete) { new MaterialAlertDialogBuilder(mActivity) .setTitle(mActivity.getString(R.string.delete_filename, profile.name)) .setMessage(R.string.are_you_sure) .setPositiveButton(R.string.cancel, null) .setNegativeButton(R.string.ok, (dialog, which) -> { if (ProfileManager.deleteProfile(profile.profileId)) { UIUtils.displayShortToast(R.string.deleted_successfully); } else { UIUtils.displayShortToast(R.string.deletion_failed); } }) .show(); } else if (id == R.id.action_routine_ops) { // TODO(7/11/20): Setup routine operations for this profile UIUtils.displayShortToast("Not yet implemented"); } else if (id == R.id.action_duplicate) { new TextInputDialogBuilder(mActivity, R.string.input_profile_name) .setTitle(R.string.new_profile) .setHelperText(R.string.input_profile_name_description) .setNegativeButton(R.string.cancel, null) .setPositiveButton(R.string.go, (dialog, which, newProfName, isChecked) -> { if (!TextUtils.isEmpty(newProfName)) { Intent intent = ProfileManager.getCloneProfileIntent( mActivity, profile.type, profile.profileId, newProfName.toString()); mActivity.startActivity(intent); } }) .show(); } else if (id == R.id.action_export) { mActivity.mProfileId = profile.profileId; mActivity.mExportProfile.launch(profile.name + ".am.json"); } else if (id == R.id.action_copy) { Utils.copyToClipboard(mActivity, profile.name, profile.profileId); } else if (id == R.id.action_shortcut) { final String[] shortcutTypesL = new String[]{ mActivity.getString(R.string.simple), mActivity.getString(R.string.advanced) }; final String[] shortcutTypes = new String[]{ST_SIMPLE, ST_ADVANCED}; new SearchableSingleChoiceDialogBuilder<>(mActivity, shortcutTypes, shortcutTypesL) .setTitle(R.string.create_shortcut) .setOnSingleChoiceClickListener((dialog, which, item1, isChecked) -> { if (!isChecked) { return; } Drawable icon = Objects.requireNonNull(ContextCompat.getDrawable(mActivity, R.drawable.ic_launcher_foreground)); ProfileShortcutInfo shortcutInfo = new ProfileShortcutInfo(profile.profileId, profile.name, shortcutTypes[which], shortcutTypesL[which]); shortcutInfo.setIcon(UIUtils.getBitmapFromDrawable(icon)); CreateShortcutDialogFragment dialog1 = CreateShortcutDialogFragment.getInstance(shortcutInfo); dialog1.show(mActivity.getSupportFragmentManager(), CreateShortcutDialogFragment.TAG); dialog.dismiss(); }) .show(); } else return false; return true; }); popupMenu.show(); return true; }); } @Override public Filter getFilter() { if (mFilter == null) mFilter = new Filter() { @Override protected FilterResults performFiltering(CharSequence charSequence) { String constraint = charSequence.toString().toLowerCase(Locale.ROOT); mConstraint = constraint; FilterResults filterResults = new FilterResults(); if (constraint.isEmpty()) { filterResults.count = 0; filterResults.values = null; return filterResults; } List list = new ArrayList<>(mDefaultList.length); for (BaseProfile item : mDefaultList) { if (item.name.toLowerCase(Locale.ROOT).contains(constraint)) list.add(item); } filterResults.count = list.size(); filterResults.values = list.toArray(new BaseProfile[0]); return filterResults; } @Override protected void publishResults(CharSequence charSequence, FilterResults filterResults) { int previousCount = mAdapterList != null ? mAdapterList.length : 0; if (filterResults.values == null) { mAdapterList = mDefaultList; } else { mAdapterList = (BaseProfile[]) filterResults.values; } AdapterUtils.notifyDataSetChanged(ProfilesAdapter.this, previousCount, mAdapterList.length); } }; return mFilter; } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/profiles/ProfilesViewModel.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.profiles; import android.app.Application; import android.os.FileObserver; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import org.json.JSONException; import java.io.File; import java.io.IOException; import java.util.HashMap; import java.util.concurrent.Future; import io.github.muntashirakon.AppManager.profiles.struct.BaseProfile; import io.github.muntashirakon.AppManager.utils.ThreadUtils; public class ProfilesViewModel extends AndroidViewModel { private final MutableLiveData> mProfilesLiveData = new MutableLiveData<>(); private Future mProfileResult; private FileObserver mFileObserver; public ProfilesViewModel(@NonNull Application application) { super(application); } @Override protected void onCleared() { if (mFileObserver != null) { mFileObserver.stopWatching(); } super.onCleared(); } public LiveData> getProfilesLiveData() { return mProfilesLiveData; } public void loadProfiles() { if (mProfileResult != null) { mProfileResult.cancel(true); } mProfileResult = ThreadUtils.postOnBackgroundThread(() -> { synchronized (mProfilesLiveData) { try { HashMap profiles = ProfileManager.getProfileSummaries(getApplication()); setUpObserverAndStart(); mProfilesLiveData.postValue(profiles); } catch (IOException | JSONException e) { e.printStackTrace(); } } }); } private void setUpObserverAndStart() { if (mFileObserver != null) { mFileObserver.startWatching(); return; } File profilePath = ProfileManager.getProfilesDir().getFile(); if (profilePath != null && !profilePath.exists()) { // Do not set up observer yet return; } int mask = FileObserver.CREATE | FileObserver.DELETE | FileObserver.DELETE_SELF | FileObserver.MOVED_TO | FileObserver.MODIFY | FileObserver.MOVED_FROM; mFileObserver = new FileObserver(profilePath, mask) { @Override public void onEvent(int event, @Nullable String path) { loadProfiles(); } }; mFileObserver.startWatching(); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/profiles/struct/AppsBaseProfile.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.profiles.struct; import android.app.AppOpsManager; import android.content.Context; import android.text.TextUtils; import androidx.annotation.CallSuper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.Collections; import java.util.List; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.backup.BackupFlags; import io.github.muntashirakon.AppManager.batchops.BatchOpsManager; import io.github.muntashirakon.AppManager.batchops.struct.BatchAppOpsOptions; import io.github.muntashirakon.AppManager.batchops.struct.BatchBackupOptions; import io.github.muntashirakon.AppManager.batchops.struct.BatchComponentOptions; import io.github.muntashirakon.AppManager.batchops.struct.BatchPermissionOptions; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.profiles.ProfileLogger; import io.github.muntashirakon.AppManager.progress.ProgressHandler; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.utils.JSONUtils; public abstract class AppsBaseProfile extends BaseProfile { public static final String TAG = AppsBaseProfile.class.getSimpleName(); public static class BackupInfo { @Nullable public String name; @BackupFlags.BackupFlag public int flags = Prefs.BackupRestore.getBackupFlags(); public BackupInfo() { } public BackupInfo(@NonNull BackupInfo backupInfo) { name = backupInfo.name; flags = backupInfo.flags; } } public int version = 1; // version public boolean allowRoutine = true; // allow_routine @Nullable public int[] users; // users @Nullable public String comment; // comment @Nullable public String[] components; // components @Nullable public int[] appOps; // app_ops @Nullable public String[] permissions; // permissions @Nullable public BackupInfo backupData; // backup_data @Nullable public Integer exportRules; // export_rules /** * Whether to freeze or unfreeze the selected packages. This only functions when the value is * set to {@code true} and {@link #state} {@code on} means freeze and * {@code off} means unfreeze. If it is set to {@code false}, it will be removed from * the profile. */ public boolean freeze = false; // misc.disable or misc.freeze (false = remove) public boolean forceStop = false; // misc.force_stop (false = remove) public boolean clearCache = false; // misc.clear_cache (false = remove) public boolean clearData = false; // misc.clear_data (false = remove) public boolean blockTrackers = false; // misc.block_trackers (false = remove) public boolean saveApk = false; // misc.save_apk (false = remove) protected AppsBaseProfile(@NonNull String profileId, @NonNull String profileName, int profileType) { super(profileId, profileName, profileType); } protected AppsBaseProfile(@NonNull String profileId, @NonNull String profileName, @NonNull AppsBaseProfile profile) { super(profileId, profileName, profile.type); version = profile.version; allowRoutine = profile.allowRoutine; state = profile.state; users = profile.users != null ? profile.users.clone() : null; comment = profile.comment; components = profile.components != null ? profile.components.clone() : null; appOps = profile.appOps != null ? profile.appOps.clone() : null; permissions = profile.permissions != null ? profile.permissions.clone() : null; backupData = profile.backupData != null ? new AppsBaseProfile.BackupInfo(profile.backupData) : null; exportRules = profile.exportRules != null ? profile.exportRules : null; freeze = profile.freeze; forceStop = profile.forceStop; clearCache = profile.clearCache; clearData = profile.clearData; blockTrackers = profile.blockTrackers; saveApk = profile.saveApk; } protected ProfileApplierResult apply(@NonNull List packageList, List assocUsers, @NonNull String state, @Nullable ProfileLogger logger, @Nullable ProgressHandler progressHandler) { // Send progress if (progressHandler != null) { progressHandler.postUpdate(calculateMaxProgress(packageList), 0); } ProfileApplierResult profileApplierResult = new ProfileApplierResult(); BatchOpsManager batchOpsManager = new BatchOpsManager(logger); BatchOpsManager.Result result; // Apply component blocking String[] components = this.components; if (components != null) { log(logger, "====> Started block/unblock components. State: " + state); BatchComponentOptions options = new BatchComponentOptions(components); int op; switch (state) { case BaseProfile.STATE_ON: op = BatchOpsManager.OP_BLOCK_COMPONENTS; break; case BaseProfile.STATE_OFF: op = BatchOpsManager.OP_UNBLOCK_COMPONENTS; break; default: op = BatchOpsManager.OP_NONE; } BatchOpsManager.BatchOpsInfo info = BatchOpsManager.BatchOpsInfo.getInstance(op, packageList, assocUsers, options); result = batchOpsManager.performOp(info, progressHandler); if (!result.isSuccessful()) { Log.d(TAG, "Failed packages: %s", result); } } else Log.d(TAG, "Skipped components."); // Apply app ops blocking int[] appOps = this.appOps; if (appOps != null) { log(logger, "====> Started ignore/default components. State: " + state); int mode; switch (state) { case BaseProfile.STATE_ON: mode = AppOpsManager.MODE_IGNORED; break; case BaseProfile.STATE_OFF: default: mode = AppOpsManager.MODE_DEFAULT; } BatchAppOpsOptions options = new BatchAppOpsOptions(appOps, mode); BatchOpsManager.BatchOpsInfo info = BatchOpsManager.BatchOpsInfo.getInstance(BatchOpsManager.OP_SET_APP_OPS, packageList, assocUsers, options); result = batchOpsManager.performOp(info, progressHandler); if (!result.isSuccessful()) { Log.d(TAG, "Failed packages: %s", result); } } else Log.d(TAG, "Skipped app ops."); // Apply permissions String[] permissions = this.permissions; if (permissions != null) { log(logger, "====> Started grant/revoke permissions."); int op; switch (state) { case BaseProfile.STATE_ON: op = BatchOpsManager.OP_REVOKE_PERMISSIONS; break; case BaseProfile.STATE_OFF: op = BatchOpsManager.OP_GRANT_PERMISSIONS; break; default: op = BatchOpsManager.OP_NONE; } BatchPermissionOptions options = new BatchPermissionOptions(permissions); BatchOpsManager.BatchOpsInfo info = BatchOpsManager.BatchOpsInfo.getInstance(op, packageList, assocUsers, options); result = batchOpsManager.performOp(info, progressHandler); if (!result.isSuccessful()) { Log.d(TAG, "Failed packages: %s", result); } } else Log.d(TAG, "Skipped permissions."); // Backup rules Integer rulesFlag = this.exportRules; if (rulesFlag != null) { log(logger, "====> Not implemented export rules."); // TODO(18/11/20): Export rules } else Log.d(TAG, "Skipped export rules."); // Disable/enable if (this.freeze) { log(logger, "====> Started freeze/unfreeze. State: " + state); int op; switch (state) { case BaseProfile.STATE_ON: op = BatchOpsManager.OP_FREEZE; break; case BaseProfile.STATE_OFF: op = BatchOpsManager.OP_UNFREEZE; break; default: op = BatchOpsManager.OP_NONE; } BatchOpsManager.BatchOpsInfo info = BatchOpsManager.BatchOpsInfo.getInstance(op, packageList, assocUsers, null); result = batchOpsManager.performOp(info, progressHandler); if (!result.isSuccessful()) { Log.d(TAG, "Failed packages: %s", result); } } else Log.d(TAG, "Skipped disable/enable."); // Force-stop if (this.forceStop) { log(logger, "====> Started force-stop."); BatchOpsManager.BatchOpsInfo info = BatchOpsManager.BatchOpsInfo.getInstance(BatchOpsManager.OP_FORCE_STOP, packageList, assocUsers, null); result = batchOpsManager.performOp(info, progressHandler); if (!result.isSuccessful()) { Log.d(TAG, "Failed packages: %s", result); } } else Log.d(TAG, "Skipped force stop."); // Clear cache if (this.clearCache) { log(logger, "====> Started clear cache."); BatchOpsManager.BatchOpsInfo info = BatchOpsManager.BatchOpsInfo.getInstance(BatchOpsManager.OP_CLEAR_CACHE, packageList, assocUsers, null); result = batchOpsManager.performOp(info, progressHandler); if (!result.isSuccessful()) { Log.d(TAG, "Failed packages: %s", result); } } else Log.d(TAG, "Skipped clear cache."); // Clear data if (this.clearData) { log(logger, "====> Started clear data."); BatchOpsManager.BatchOpsInfo info = BatchOpsManager.BatchOpsInfo.getInstance(BatchOpsManager.OP_CLEAR_DATA, packageList, assocUsers, null); result = batchOpsManager.performOp(info, progressHandler); if (!result.isSuccessful()) { Log.d(TAG, "Failed packages: %s", result); } } else Log.d(TAG, "Skipped clear data."); // Block trackers if (this.blockTrackers) { log(logger, "====> Started block trackers. State: " + state); int op; switch (state) { case BaseProfile.STATE_ON: op = BatchOpsManager.OP_BLOCK_TRACKERS; break; case BaseProfile.STATE_OFF: op = BatchOpsManager.OP_UNBLOCK_TRACKERS; break; default: op = BatchOpsManager.OP_NONE; } BatchOpsManager.BatchOpsInfo info = BatchOpsManager.BatchOpsInfo.getInstance(op, packageList, assocUsers, null); result = batchOpsManager.performOp(info, progressHandler); if (!result.isSuccessful()) { Log.d(TAG, "Failed packages: %s", result); } } else Log.d(TAG, "Skipped block trackers."); // Backup apk if (this.saveApk) { log(logger, "====> Started backup apk."); BatchOpsManager.BatchOpsInfo info = BatchOpsManager.BatchOpsInfo.getInstance(BatchOpsManager.OP_BACKUP_APK, packageList, assocUsers, null); result = batchOpsManager.performOp(info, progressHandler); if (!result.isSuccessful()) { Log.d(TAG, "Failed packages: %s", result); } } else Log.d(TAG, "Skipped backup apk."); // Backup/restore data AppsBaseProfile.BackupInfo backupInfo = this.backupData; if (backupInfo != null) { log(logger, "====> Started backup/restore."); BackupFlags backupFlags = new BackupFlags(backupInfo.flags); String[] backupNames; if (backupFlags.backupMultiple() && backupInfo.name != null) { backupNames = new String[]{backupInfo.name}; } else backupNames = null; // Always add backup custom users backupFlags.addFlag(BackupFlags.BACKUP_CUSTOM_USERS); BatchBackupOptions options = new BatchBackupOptions(backupFlags.getFlags(), backupNames, null); int op; switch (state) { case BaseProfile.STATE_ON: // Take backup op = BatchOpsManager.OP_BACKUP; break; case BaseProfile.STATE_OFF: // Restore backup op = BatchOpsManager.OP_RESTORE_BACKUP; break; default: op = BatchOpsManager.OP_NONE; } BatchOpsManager.BatchOpsInfo info = BatchOpsManager.BatchOpsInfo.getInstance(op, packageList, assocUsers, options); result = batchOpsManager.performOp(info, progressHandler); profileApplierResult.setRequiresRestart(profileApplierResult.requiresRestart() | result.requiresRestart()); if (!result.isSuccessful()) { Log.d(TAG, "Failed packages: %s", result); } } else Log.d(TAG, "Skipped backup/restore."); batchOpsManager.conclude(); return profileApplierResult; } private int calculateMaxProgress(@NonNull List userPackagePairs) { int packageCount = userPackagePairs.size(); int opCount = 0; if (components != null) ++opCount; if (appOps != null) ++opCount; if (permissions != null) ++opCount; // if (profile.exportRules != null) ++opCount; todo if (freeze) ++opCount; if (forceStop) ++opCount; if (clearCache) ++opCount; if (clearData) ++opCount; if (blockTrackers) ++opCount; if (saveApk) ++opCount; if (backupData != null) ++opCount; return opCount * packageCount; } private void log(@Nullable ProfileLogger logger, @Nullable String message) { if (logger != null) { logger.println(message); } } @NonNull private List getLocalisedSummaryOrComment(Context context) { if (comment != null) { return Collections.singletonList(comment); } List arrayList = new ArrayList<>(); if (components != null) arrayList.add(context.getString(R.string.components)); if (appOps != null) arrayList.add(context.getString(R.string.app_ops)); if (permissions != null) arrayList.add(context.getString(R.string.permissions)); if (backupData != null) arrayList.add(context.getString(R.string.backup_restore)); if (exportRules != null) arrayList.add(context.getString(R.string.blocking_rules)); if (freeze) arrayList.add(context.getString(R.string.freeze)); if (forceStop) arrayList.add(context.getString(R.string.force_stop)); if (clearCache) arrayList.add(context.getString(R.string.clear_cache)); if (clearData) arrayList.add(context.getString(R.string.clear_data)); if (blockTrackers) arrayList.add(context.getString(R.string.trackers)); if (saveApk) arrayList.add(context.getString(R.string.save_apk)); return arrayList; } @NonNull @Override public CharSequence toLocalizedString(@NonNull Context context) { List summaries = getLocalisedSummaryOrComment(context); if (summaries.isEmpty()) { return context.getString(R.string.no_configurations); } return TextUtils.join(", ", summaries); } @CallSuper @NonNull @Override public JSONObject serializeToJson() throws JSONException { JSONObject profileObj = super.serializeToJson(); profileObj.put("version", version); if (!allowRoutine) { // Only save allow_routine if it's set to false profileObj.put("allow_routine", false); } profileObj.put("comment", comment); profileObj.put("users", JSONUtils.getJSONArray(users)); profileObj.put("components", JSONUtils.getJSONArray(components)); profileObj.put("app_ops", JSONUtils.getJSONArray(appOps)); profileObj.put("permissions", JSONUtils.getJSONArray(permissions)); // Backup info if (backupData != null) { JSONObject backupInfo = new JSONObject(); backupInfo.put("name", backupData.name); backupInfo.put("flags", backupData.flags); profileObj.put("backup_data", backupInfo); } profileObj.put("export_rules", exportRules); // Misc JSONArray jsonArray = new JSONArray(); if (freeze) jsonArray.put("freeze"); if (forceStop) jsonArray.put("force_stop"); if (clearCache) jsonArray.put("clear_cache"); if (clearData) jsonArray.put("clear_data"); if (blockTrackers) jsonArray.put("block_trackers"); if (saveApk) jsonArray.put("save_apk"); if (jsonArray.length() > 0) profileObj.put("misc", jsonArray); return profileObj; } protected AppsBaseProfile(@NonNull JSONObject profileObj) throws JSONException { super(profileObj); comment = JSONUtils.getString(profileObj, "comment", null); version = profileObj.getInt("version"); allowRoutine = profileObj.optBoolean("allow_routine", true); try { users = JSONUtils.getIntArray(profileObj.getJSONArray("users")); } catch (JSONException ignore) { } try { components = JSONUtils.getArray(String.class, profileObj.getJSONArray("components")); } catch (JSONException ignore) { } try { appOps = JSONUtils.getIntArray(profileObj.getJSONArray("app_ops")); } catch (JSONException ignore) { } try { permissions = JSONUtils.getArray(String.class, profileObj.getJSONArray("permissions")); } catch (JSONException ignore) { } // Backup info try { JSONObject backupInfo = profileObj.getJSONObject("backup_data"); backupData = new AppsBaseProfile.BackupInfo(); backupData.name = JSONUtils.getString(backupInfo, "name", null); backupData.flags = backupInfo.getInt("flags"); } catch (JSONException ignore) { } exportRules = JSONUtils.getIntOrNull(profileObj, "export_rules"); // Misc try { List miscConfig = JSONUtils.getArray(profileObj.getJSONArray("misc")); freeze = miscConfig.contains("disable") || miscConfig.contains("freeze"); forceStop = miscConfig.contains("force_stop"); clearCache = miscConfig.contains("clear_cache"); clearData = miscConfig.contains("clear_data"); blockTrackers = miscConfig.contains("block_trackers"); saveApk = miscConfig.contains("save_apk"); } catch (Exception ignore) { } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/profiles/struct/AppsFilterProfile.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.profiles.struct; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.List; import io.github.muntashirakon.AppManager.filters.FilterItem; import io.github.muntashirakon.AppManager.filters.FilterableAppInfo; import io.github.muntashirakon.AppManager.filters.FilteringUtils; import io.github.muntashirakon.AppManager.history.JsonDeserializer; import io.github.muntashirakon.AppManager.profiles.ProfileLogger; import io.github.muntashirakon.AppManager.progress.ProgressHandler; import io.github.muntashirakon.AppManager.users.Users; public class AppsFilterProfile extends AppsBaseProfile { @NonNull private final FilterItem mFilterItem; protected AppsFilterProfile(@NonNull String profileId, @NonNull String profileName) { super(profileId, profileName, PROFILE_TYPE_APPS_FILTER); mFilterItem = new FilterItem(); } protected AppsFilterProfile(@NonNull String profileId, @NonNull String profileName, @NonNull AppsFilterProfile profile) { super(profileId, profileName, profile.type); try { // Shorthand for cloning filter items mFilterItem = FilterItem.DESERIALIZER.deserialize(profile.mFilterItem.serializeToJson()); } catch (JSONException e) { throw new IllegalArgumentException("Invalid profile", e); } } public FilterItem getFilterItem() { return mFilterItem; } @Override public ProfileApplierResult apply(@NonNull String state, @Nullable ProfileLogger logger, @Nullable ProgressHandler progressHandler) { // Filter results int[] users = this.users == null ? Users.getUsersIds() : this.users; List filterableAppInfoList = FilteringUtils.loadFilterableAppInfo(users); List> filteredList = mFilterItem.getFilteredList(filterableAppInfoList); if (filteredList.isEmpty()) { return ProfileApplierResult.EMPTY_RESULT; } List packages = new ArrayList<>(filteredList.size()); List assocUsers = new ArrayList<>(filteredList.size()); if (logger != null) { logger.println("====> Filtered packages: " + filteredList.size()); } StringBuilder sb = new StringBuilder(); for (FilterItem.FilteredItemInfo info : filteredList) { packages.add(info.info.getPackageName()); assocUsers.add(info.info.getUserId()); sb.append("(").append(info.info.getPackageName()).append(", ") .append(info.info.getUserId()).append("), "); } if (logger != null) { logger.println(sb); } return apply(packages, assocUsers, state, logger, progressHandler); } @NonNull @Override public JSONObject serializeToJson() throws JSONException { return super.serializeToJson().put("filters", mFilterItem.serializeToJson()); } protected AppsFilterProfile(@NonNull JSONObject profileObj) throws JSONException { super(profileObj); mFilterItem = FilterItem.DESERIALIZER.deserialize(profileObj.getJSONObject("filters")); } public static final JsonDeserializer.Creator DESERIALIZER = AppsFilterProfile::new; } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/profiles/struct/AppsProfile.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.profiles.struct; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.List; import aosp.libcore.util.EmptyArray; import io.github.muntashirakon.AppManager.history.JsonDeserializer; import io.github.muntashirakon.AppManager.profiles.ProfileLogger; import io.github.muntashirakon.AppManager.progress.ProgressHandler; import io.github.muntashirakon.AppManager.users.Users; import io.github.muntashirakon.AppManager.utils.ArrayUtils; import io.github.muntashirakon.AppManager.utils.JSONUtils; public class AppsProfile extends AppsBaseProfile { @NonNull public String[] packages; // packages (a list of packages) protected AppsProfile(@NonNull String profileId, @NonNull String profileName) { super(profileId, profileName, PROFILE_TYPE_APPS); packages = EmptyArray.STRING; } protected AppsProfile(@NonNull String profileId, @NonNull String profileName, @NonNull AppsProfile profile) { super(profileId, profileName, profile); packages = profile.packages.clone(); } @Override public ProfileApplierResult apply(@NonNull String state, @Nullable ProfileLogger logger, @Nullable ProgressHandler progressHandler) { if (packages.length == 0) return ProfileApplierResult.EMPTY_RESULT; int[] users = this.users == null ? Users.getUsersIds() : this.users; int size = packages.length * users.length; List packageList = new ArrayList<>(size); List assocUsers = new ArrayList<>(size); for (String packageName : packages) { for (int user : users) { packageList.add(packageName); assocUsers.add(user); } } return apply(packageList, assocUsers, state, logger, progressHandler); } public void appendPackages(@NonNull String[] packageList) { List uniquePackages = new ArrayList<>(); for (String newPackage : packageList) { if (!ArrayUtils.contains(packages, newPackage)) { uniquePackages.add(newPackage); } } packages = ArrayUtils.concatElements(String.class, packages, uniquePackages.toArray(new String[0])); } @NonNull @Override public JSONObject serializeToJson() throws JSONException { return super.serializeToJson() .put("packages", JSONUtils.getJSONArray(packages)); } protected AppsProfile(@NonNull JSONObject profileObj) throws JSONException { super(profileObj); packages = JSONUtils.getArray(String.class, profileObj.getJSONArray("packages")); } public static final JsonDeserializer.Creator DESERIALIZER = AppsProfile::new; } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/profiles/struct/BaseProfile.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.profiles.struct; import static io.github.muntashirakon.AppManager.profiles.ProfileManager.PROFILE_EXT; import androidx.annotation.CallSuper; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringDef; import org.jetbrains.annotations.Contract; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.io.OutputStream; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Objects; import io.github.muntashirakon.AppManager.history.IJsonSerializer; import io.github.muntashirakon.AppManager.history.JsonDeserializer; import io.github.muntashirakon.AppManager.profiles.ProfileLogger; import io.github.muntashirakon.AppManager.profiles.ProfileManager; import io.github.muntashirakon.AppManager.progress.ProgressHandler; import io.github.muntashirakon.AppManager.utils.JSONUtils; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; import io.github.muntashirakon.util.LocalizedString; public abstract class BaseProfile implements LocalizedString, IJsonSerializer { @Contract("null -> fail") @NonNull public static BaseProfile fromPath(@Nullable Path profilePath) throws IOException, JSONException { if (profilePath == null) { throw new IOException("Empty profile path"); } String profileStr = profilePath.getContentAsString(); JSONObject profileObj = new JSONObject(profileStr); return BaseProfile.DESERIALIZER.deserialize(profileObj); } @NonNull public static BaseProfile newProfile(@NonNull String newProfileName, int type, @Nullable BaseProfile source) { String profileId = ProfileManager.getProfileIdCompat(newProfileName); // TODO: 17/9/23 TODO: Remove these once we migrated to UUID based profile ID // BEGIN legacy: For legacy profile, the generated ID can be the same as an existing profile Path profilesDir = ProfileManager.getProfilesDir(); Path profilePath = Paths.build(profilesDir, profileId + PROFILE_EXT); String profileName = newProfileName; int i = 1; while (profilePath != null && profilePath.exists()) { // Try another name profileName = newProfileName + " (" + i + ")"; profileId = ProfileManager.getProfileIdCompat(profileName); profilePath = Paths.build(profilesDir, profileId + PROFILE_EXT); ++i; } // END legacy: For legacy profile, the generated ID can be the same as an existing profile switch (type) { case PROFILE_TYPE_APPS: if (source != null) { assert source instanceof AppsProfile; return new AppsProfile(profileId, profileName, (AppsProfile) source); } else return new AppsProfile(profileId, profileName); case PROFILE_TYPE_APPS_FILTER: if (source != null) { assert source instanceof AppsFilterProfile; return new AppsFilterProfile(profileId, profileName, (AppsFilterProfile) source); } else return new AppsFilterProfile(profileId, profileName); default: throw new IllegalArgumentException("Invalid type: " + type); } } public static final int PROFILE_TYPE_APPS = 0; public static final int PROFILE_TYPE_APPS_FILTER = 1; @IntDef({PROFILE_TYPE_APPS, PROFILE_TYPE_APPS_FILTER}) @Retention(RetentionPolicy.SOURCE) public @interface ProfileType { } public static final String STATE_ON = "on"; public static final String STATE_OFF = "off"; @StringDef({STATE_ON, STATE_OFF}) @Retention(RetentionPolicy.SOURCE) public @interface ProfileState { } @NonNull public final String profileId; // id @NonNull public final String name; // name (name of the profile) @ProfileType public final int type; // type @ProfileState public String state; // state protected BaseProfile(@NonNull String profileId, @NonNull String profileName, int profileType) { this.profileId = profileId; this.name = profileName; this.type = profileType; } public abstract ProfileApplierResult apply(@NonNull String state, @Nullable ProfileLogger logger, @Nullable ProgressHandler progressHandler); public void write(@NonNull OutputStream out) throws IOException { try { out.write(serializeToJson().toString().getBytes()); } catch (JSONException e) { throw new IOException(e); } } @NonNull @CallSuper @Override public JSONObject serializeToJson() throws JSONException { JSONObject profileObj = new JSONObject(); profileObj.put("id", profileId); profileObj.put("name", name); profileObj.put("type", type); profileObj.put("state", state); return profileObj; } protected BaseProfile(@NonNull JSONObject profileObj) throws JSONException { name = profileObj.getString("name"); profileId = JSONUtils.getString(profileObj, "id", ProfileManager.getProfileIdCompat(name)); type = profileObj.getInt("type"); state = JSONUtils.getString(profileObj, "state", STATE_ON); } public static final JsonDeserializer.Creator DESERIALIZER = jsonObject -> { int type = jsonObject.getInt("type"); if (type == PROFILE_TYPE_APPS) { return AppsProfile.DESERIALIZER.deserialize(jsonObject); } else if (type == PROFILE_TYPE_APPS_FILTER) { return AppsFilterProfile.DESERIALIZER.deserialize(jsonObject); } else throw new JSONException("Invalid type: " + type); }; @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof BaseProfile)) return false; BaseProfile that = (BaseProfile) o; return Objects.equals(profileId, that.profileId); } @Override public int hashCode() { return Objects.hash(profileId); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/profiles/struct/ProfileApplierResult.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.profiles.struct; public class ProfileApplierResult { public static final ProfileApplierResult EMPTY_RESULT = new ProfileApplierResult(); private boolean mRequiresRestart; public void setRequiresRestart(boolean requiresRestart) { mRequiresRestart = requiresRestart; } public boolean requiresRestart() { return mRequiresRestart; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/progress/NotificationProgressHandler.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.progress; import android.Manifest; import android.app.Notification; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.ActivityCompat; import androidx.core.app.NotificationChannelCompat; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import androidx.core.app.PendingIntentCompat; import org.jetbrains.annotations.Contract; import java.util.ArrayList; import java.util.Objects; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.types.ForegroundService; import io.github.muntashirakon.AppManager.utils.NotificationUtils; public class NotificationProgressHandler extends QueuedProgressHandler { private static final String TAG_PROGRESS = null; private static final String TAG_QUEUE = "queue"; private static final String TAG_ALERT = "alert"; @NonNull private final Context mContext; @NonNull private final NotificationManagerInfo mProgressNotificationManagerInfo; @NonNull private final NotificationManagerInfo mCompletionNotificationManagerInfo; @Nullable private final NotificationManagerInfo mQueueNotificationManagerInfo; @NonNull private final NotificationManagerCompat mProgressNotificationManager; @NonNull private final NotificationManagerCompat mCompletionNotificationManager; @Nullable private final NotificationManagerCompat mQueueNotificationManager; private final int mProgressNotificationId; @Nullable private NotificationInfo mLastProgressNotification = null; private volatile int mLastMax = MAX_INDETERMINATE; private volatile float mLastProgress = 0; private boolean mAttachedToService; public NotificationProgressHandler(@NonNull Context context, @NonNull NotificationManagerInfo progressNotificationManagerInfo, @NonNull NotificationManagerInfo completionNotificationManagerInfo, @Nullable NotificationManagerInfo queueNotificationManagerInfo) { mContext = context; mProgressNotificationManagerInfo = progressNotificationManagerInfo; mCompletionNotificationManagerInfo = completionNotificationManagerInfo; mQueueNotificationManagerInfo = queueNotificationManagerInfo; mProgressNotificationManager = getNotificationManager(context, mProgressNotificationManagerInfo); mCompletionNotificationManager = getNotificationManager(context, mCompletionNotificationManagerInfo); mQueueNotificationManager = getNotificationManager(context, mQueueNotificationManagerInfo); mProgressNotificationId = NotificationUtils.nextNotificationId(TAG_PROGRESS); } @Override public void onQueue(@Nullable Object message) { if (mQueueNotificationManager == null || mQueueNotificationManagerInfo == null || message == null) { return; } NotificationInfo info = (NotificationInfo) message; Notification notification = info .getBuilder(mContext, mQueueNotificationManagerInfo) .setLocalOnly(true) .build(); notify(mContext, mQueueNotificationManager, TAG_QUEUE, NotificationUtils.nextNotificationId(TAG_QUEUE), notification); } @Override public void onAttach(@Nullable Service service, @NonNull Object message) { mLastProgressNotification = (NotificationInfo) message; if (service != null) { mAttachedToService = true; Notification notification = mLastProgressNotification .getBuilder(mContext, mProgressNotificationManagerInfo) .setLocalOnly(true) .setOngoing(true) .setOnlyAlertOnce(true) .setProgress(0, 0, false) .build(); ForegroundService.start(service, mProgressNotificationId, notification, ForegroundService.FOREGROUND_SERVICE_TYPE_DATA_SYNC | ForegroundService.FOREGROUND_SERVICE_TYPE_SPECIAL_USE); } } @Override public void onProgressStart(int max, float current, @Nullable Object message) { onProgressUpdate(max, current, message); } @Override public void onProgressUpdate(int max, float current, @Nullable Object message) { if (message != null) { mLastProgressNotification = (NotificationInfo) message; } else { Objects.requireNonNull(mLastProgressNotification); } mLastMax = max; mLastProgress = current; CharSequence progressText = progressTextInterface.getProgressText(this); boolean indeterminate = max == -1; int newMax = Math.max(max, 0); int newCurrent = max < 0 ? 0 : (int) current; NotificationCompat.Builder builder = mLastProgressNotification .getBuilder(mContext, mProgressNotificationManagerInfo) .setLocalOnly(true) .setOngoing(true) .setOnlyAlertOnce(true) .setProgress(newMax, newCurrent, indeterminate); if (progressText != null) { if (max > 0) { builder.setContentText(progressText); } else if (max == MAX_FINISHED) { builder.setContentText(mContext.getString(R.string.done)); } else { builder.setContentText(mContext.getString(R.string.operation_running)); } } notify(mContext, mProgressNotificationManager, TAG_PROGRESS, mProgressNotificationId, builder.build()); } @Override public void onResult(@Nullable Object message) { if (!mAttachedToService) { mProgressNotificationManager.cancel(TAG_PROGRESS, mProgressNotificationId); } else { onProgressUpdate(MAX_FINISHED, 0, null); // Trick to remove progressbar } if (message == null) { return; } NotificationInfo info = (NotificationInfo) message; Notification notification = info .getBuilder(mContext, mCompletionNotificationManagerInfo) .build(); notify(mContext, mCompletionNotificationManager, TAG_ALERT, NotificationUtils.nextNotificationId(TAG_ALERT), notification); } @Override public void onDetach(@Nullable Service service) { if (service != null) { mAttachedToService = false; mProgressNotificationManager.cancel(TAG_PROGRESS, mProgressNotificationId); } } @Override @NonNull public ProgressHandler newSubProgressHandler() { return new NotificationProgressHandler(mContext, mProgressNotificationManagerInfo, mCompletionNotificationManagerInfo, null); } @Override public int getLastMax() { return mLastMax; } @Override public float getLastProgress() { return mLastProgress; } @Nullable @Override public Object getLastMessage() { return mLastProgressNotification; } @Override public void postUpdate(int max, float current, @Nullable Object message) { // Update values immediately to avoid issues mLastMax = max; mLastProgress = current; super.postUpdate(max, current, message); } private static void notify(@NonNull Context context, @NonNull NotificationManagerCompat notificationManager, @Nullable String notificationTag, int notificationId, @NonNull Notification notification) { if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) { notificationManager.notify(notificationTag, notificationId, notification); } } @Nullable @Contract("_, !null -> !null") private static NotificationManagerCompat getNotificationManager(@NonNull Context context, @Nullable NotificationManagerInfo info) { if (info == null) { return null; } NotificationChannelCompat channel = new NotificationChannelCompat .Builder(info.channelId, info.importance) .setName(info.channelName) .build(); NotificationManagerCompat managerCompat = NotificationManagerCompat.from(context); managerCompat.createNotificationChannel(channel); return managerCompat; } public static class NotificationManagerInfo { @NonNull public final String channelId; @NonNull public final CharSequence channelName; @NotificationUtils.NotificationImportance public final int importance; public NotificationManagerInfo(@NonNull String channelId, @NonNull CharSequence channelName, @NotificationUtils.NotificationImportance int importance) { this.channelId = channelId; this.channelName = channelName; this.importance = importance; } } public static class NotificationInfo { @DrawableRes private int icon = R.drawable.ic_default_notification; private int level; private long time = 0L; @Nullable private CharSequence operationName; @Nullable private CharSequence title; @Nullable private CharSequence body; @Nullable private CharSequence statusBarText; @Nullable private NotificationCompat.Style style; private boolean autoCancel; @Nullable private PendingIntent defaultAction; @Nullable private String groupId; private final ArrayList actions = new ArrayList<>(); public NotificationInfo() { } public NotificationInfo(@NonNull NotificationInfo notificationInfo) { icon = notificationInfo.icon; level = notificationInfo.level; time = notificationInfo.time; operationName = notificationInfo.operationName; title = notificationInfo.title; body = notificationInfo.body; statusBarText = notificationInfo.statusBarText; style = notificationInfo.style; autoCancel = notificationInfo.autoCancel; defaultAction = notificationInfo.defaultAction; groupId = notificationInfo.groupId; actions.addAll(notificationInfo.actions); } public NotificationInfo setIcon(int icon) { this.icon = icon; return this; } public NotificationInfo setLevel(int level) { this.level = level; return this; } public NotificationInfo setTitle(@Nullable CharSequence title) { this.title = title; return this; } public NotificationInfo setBody(@Nullable CharSequence body) { this.body = body; return this; } public NotificationInfo setStatusBarText(@Nullable CharSequence statusBarText) { this.statusBarText = statusBarText; return this; } public NotificationInfo setDefaultAction(@Nullable PendingIntent defaultAction) { this.defaultAction = defaultAction; return this; } public NotificationInfo setOperationName(@Nullable CharSequence operationName) { this.operationName = operationName; return this; } public NotificationInfo setStyle(@Nullable NotificationCompat.Style style) { this.style = style; return this; } public NotificationInfo setAutoCancel(boolean autoCancel) { this.autoCancel = autoCancel; return this; } public NotificationInfo setTime(long time) { this.time = time; return this; } public void setGroupId(@Nullable String groupId) { this.groupId = groupId; } public NotificationInfo addAction(int icon, @Nullable CharSequence title, @Nullable PendingIntent intent) { actions.add(new NotificationCompat.Action(icon, title, intent)); return this; } @NonNull NotificationCompat.Builder getBuilder(@NonNull Context context, @NonNull NotificationManagerInfo info) { PendingIntent contentIntent; if (autoCancel && defaultAction == null) { // Auto-cancel requires a content Intent contentIntent = PendingIntentCompat.getActivity(context, 0, new Intent(), 0, false); } else contentIntent = defaultAction; NotificationCompat.Builder builder = new NotificationCompat.Builder(context, info.channelId) .setLocalOnly(!Prefs.Misc.sendNotificationsToConnectedDevices()) .setPriority(NotificationUtils.importanceToPriority(info.importance)) .setDefaults(Notification.DEFAULT_ALL) .setSmallIcon(icon, level) .setSubText(operationName) .setTicker(statusBarText) .setContentTitle(title) .setContentText(body) .setContentIntent(contentIntent) .setAutoCancel(autoCancel) .setGroup(groupId) .setStyle(style); if (groupId == null) { builder.setGroupSummary(false); builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY); } for (NotificationCompat.Action action : actions) { builder.addAction(action); } if (time > 0L) { builder.setWhen(time); builder.setShowWhen(true); } return builder; } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/progress/ProgressHandler.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.progress; import android.annotation.SuppressLint; import android.app.Service; import androidx.annotation.AnyThread; import androidx.annotation.CallSuper; import androidx.annotation.MainThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.util.Locale; import io.github.muntashirakon.AppManager.utils.ThreadUtils; /** * A generic class to handle any kind of progress. Progress can be handled in various ways such as using notifications * or progress indicator or both. */ public abstract class ProgressHandler { public interface ProgressTextInterface { @Nullable CharSequence getProgressText(@NonNull ProgressHandler progressHandler); } public static final ProgressTextInterface PROGRESS_PERCENT = progressHandler -> { float current = progressHandler.getLastProgress() / progressHandler.getLastMax() * 100; return String.format(Locale.getDefault(), "%d%%", (int) current); }; public static final ProgressTextInterface PROGRESS_REGULAR = progressHandler -> String.format(Locale.getDefault(), "%d/%d", (int) progressHandler.getLastProgress(), progressHandler.getLastMax()); protected static final ProgressTextInterface PROGRESS_DEFAULT = progressHandler -> null; protected static final int MAX_INDETERMINATE = -1; protected static final int MAX_FINISHED = -2; @NonNull protected ProgressTextInterface progressTextInterface = PROGRESS_DEFAULT; /** * Call this function if the progress handler is backed by a foreground service and a progressbar is needed to be * initiated right away. After finished working with it, call {@link #onDetach(Service)} */ @MainThread public abstract void onAttach(@Nullable Service service, @NonNull Object message); /** * Initialise progress. Arguments here can be modified by calling {@link #onProgressUpdate(int, float, Object)}. * * @param max Maximum progress value. Use {@code -1} to switch to non-determinate mode. * @param current Current progress value. Should be {@code 0}. Irrelevant in non-determinate mode. * @param message Additional arguments to pass on. Depends on implementation. */ @MainThread public abstract void onProgressStart(int max, float current, @Nullable Object message); /** * Update progress * * @param max Maximum progress value. Use {@code -1} to switch to non-determinate mode. * @param current Current progress value. Irrelevant in non-determinate mode. * @param message Additional arguments to pass on. Depends on implementation. */ @MainThread public abstract void onProgressUpdate(int max, float current, @Nullable Object message); /** * Call when the progress is finished. If this is not attached to a foreground service, the progress also stops. */ @MainThread public abstract void onResult(@Nullable Object message); /** * Call this function to stop progress when this is attached to a foreground service. */ @MainThread public abstract void onDetach(@Nullable Service service); /** * Get a new progress handler from this handler. The handler will never have a queue handler. */ @NonNull public abstract ProgressHandler newSubProgressHandler(); @Nullable public abstract Object getLastMessage(); public abstract int getLastMax(); public abstract float getLastProgress(); public void setProgressTextInterface(@Nullable ProgressTextInterface progressTextInterface) { this.progressTextInterface = progressTextInterface != null ? progressTextInterface : PROGRESS_DEFAULT; } /** * Update progress from any thread. Arguments from the last time are used. * * @param current Current progress value. Irrelevant in non-determinate mode. */ @AnyThread public final void postUpdate(float current) { postUpdate(getLastMax(), current, getLastMessage()); } /** * Update progress from any thread. Arguments from the last time are used. * * @param max Max progress values. Use {@code -1} to switch to non-determinate mode. * @param current Current progress value. Irrelevant in non-determinate mode. */ @AnyThread public final void postUpdate(int max, float current) { postUpdate(max, current, getLastMessage()); } /** * Update progress from any thread. * * @param max Max progress values. Use {@code -1} to switch to non-determinate mode. * @param current Current progress value. Irrelevant in non-determinate mode. * @param message Additional arguments to pass on. Depends on implementation. */ @SuppressLint("WrongThread") @AnyThread @CallSuper public void postUpdate(int max, float current, @Nullable Object message) { if (ThreadUtils.isMainThread()) { onProgressUpdate(max, current, message); } else { ThreadUtils.postOnMainThread(() -> onProgressUpdate(max, current, message)); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/progress/QueuedProgressHandler.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.progress; import androidx.annotation.MainThread; import androidx.annotation.Nullable; public abstract class QueuedProgressHandler extends ProgressHandler { /** * Call when items are added to queue. This can be unrelated to progress, but useful in situations where queues * need to be handled. */ @MainThread public abstract void onQueue(@Nullable Object message); } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/rules/PseudoRules.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.rules; import android.os.RemoteException; import androidx.annotation.NonNull; import java.io.IOException; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; public class PseudoRules extends RulesStorageManager { public PseudoRules(@NonNull String packageName, int userHandle) { super(packageName, userHandle); setReadOnly(); } @Override public void setMutable() { // Do nothing } public void loadExternalEntries(Path file) throws IOException, RemoteException { super.loadEntries(file, true); } /** * No rules will be loaded * * @return /dev/null */ @NonNull @Override protected Path getDesiredFile(boolean create) { return Paths.get("/dev/null"); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/rules/RuleType.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.rules; import androidx.annotation.Keep; @Keep public enum RuleType { ACTIVITY, PROVIDER, RECEIVER, SERVICE, APP_OP, PERMISSION, MAGISK_HIDE, MAGISK_DENY_LIST, BATTERY_OPT, NET_POLICY, NOTIFICATION, URI_GRANT, SSAID, FREEZE, } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/rules/RulesExporter.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.rules; import android.content.Context; import android.net.Uri; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.io.IOException; import java.io.OutputStream; import java.util.List; import io.github.muntashirakon.AppManager.rules.compontents.ComponentUtils; import io.github.muntashirakon.AppManager.rules.compontents.ComponentsBlocker; import io.github.muntashirakon.AppManager.utils.ContextUtils; /** * Export rules to external directory either for a single package or multiple packages. * * @see RulesImporter */ public class RulesExporter { @NonNull private final Context mContext; @Nullable private List mPackagesToExport; @NonNull private final List mTypesToExport; @NonNull private final int[] mUserIds; public RulesExporter(@NonNull List typesToExport, @Nullable List packagesToExport, @NonNull int[] userIds) { mContext = ContextUtils.getContext(); mPackagesToExport = packagesToExport; mTypesToExport = typesToExport; mUserIds = userIds; } public void saveRules(Uri uri) throws IOException { if (mPackagesToExport == null) mPackagesToExport = ComponentUtils.getAllPackagesWithRules(mContext); try (OutputStream outputStream = mContext.getContentResolver().openOutputStream(uri)) { if (outputStream == null) throw new IOException("Content provider has crashed."); for (String packageName: mPackagesToExport) { for (int userHandle : mUserIds) { // Get a read-only instance try (ComponentsBlocker cb = ComponentsBlocker.getInstance(packageName, userHandle)) { ComponentUtils.storeRules(outputStream, cb.getAll(mTypesToExport), true); } } } } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/rules/RulesImporter.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.rules; import android.net.Uri; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import java.io.BufferedReader; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import io.github.muntashirakon.AppManager.rules.compontents.ComponentsBlocker; import io.github.muntashirakon.AppManager.rules.struct.RuleEntry; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.io.IoUtils; import io.github.muntashirakon.io.Path; /** * Rules importer is used to import internal rules to App Manager. Rules should only be imported * from settings and app data restore sections (although can be exported from various places). *
* Format: package_name component_name type mode|is_applied|is_granted * * @see RulesExporter * @see RuleType */ public class RulesImporter implements Closeable { @NonNull private final HashMap[] mComponentsBlockers; @NonNull private final List mTypesToImport; @Nullable private List mPackagesToImport; @NonNull private final int[] mUserIds; public RulesImporter(@NonNull List typesToImport, @NonNull int[] userIds) { if (userIds.length == 0) { throw new IllegalArgumentException("Input must contain one or more user handles"); } // Init CBs //noinspection unchecked mComponentsBlockers = new HashMap[userIds.length]; for (int i = 0; i < userIds.length; ++i) { mComponentsBlockers[i] = new HashMap<>(); } mTypesToImport = typesToImport; mUserIds = userIds; } public void addRulesFromUri(Uri uri) throws IOException { try (InputStream inputStream = ContextUtils.getContext().getContentResolver().openInputStream(uri)) { if (inputStream == null) throw new IOException("Content provider has crashed."); try (BufferedReader TSVFile = new BufferedReader(new InputStreamReader(inputStream))) { String dataRow; while ((dataRow = TSVFile.readLine()) != null) { RuleEntry entry = RuleEntry.unflattenFromString(null, dataRow, true); // Parse complete, now add the row to CB for (int i = 0; i < mUserIds.length; ++i) { if (mComponentsBlockers[i].get(entry.packageName) == null) { // Get a read-only instance, commit will be called manually mComponentsBlockers[i].put(entry.packageName, ComponentsBlocker.getInstance(entry.packageName, mUserIds[i])); } if (mTypesToImport.contains(entry.type)) { //noinspection ConstantConditions Returned ComponentsBlocker will never be null here mComponentsBlockers[i].get(entry.packageName).addEntry(entry); } } } } } } public void addRulesFromPath(Path path) throws IOException { try (InputStream inputStream = path.openInputStream()) { try (BufferedReader TSVFile = new BufferedReader(new InputStreamReader(inputStream))) { String dataRow; while ((dataRow = TSVFile.readLine()) != null) { RuleEntry entry = RuleEntry.unflattenFromString(null, dataRow, true); // Parse complete, now add the row to CB for (int i = 0; i < mUserIds.length; ++i) { if (mComponentsBlockers[i].get(entry.packageName) == null) { // Get a read-only instance, commit will be called manually mComponentsBlockers[i].put(entry.packageName, ComponentsBlocker.getInstance(entry.packageName, mUserIds[i])); } if (mTypesToImport.contains(entry.type)) { //noinspection ConstantConditions Returned ComponentsBlocker will never be null here mComponentsBlockers[i].get(entry.packageName).addEntry(entry); } } } } } } public List getPackages() { return new ArrayList<>(mComponentsBlockers[0].keySet()); } public void setPackagesToImport(List packageNames) { mPackagesToImport = packageNames; } @WorkerThread public void applyRules(boolean commitChanges) { if (mPackagesToImport == null) mPackagesToImport = getPackages(); // When #setPackagesToImport(List) is used, ComponentBlocker can be null @Nullable ComponentsBlocker cb; for (int i = 0; i < mUserIds.length; ++i) { for (String packageName : mPackagesToImport) { cb = mComponentsBlockers[i].get(packageName); if (cb == null) continue; // Set mutable, otherwise changes may not be applied properly cb.setMutable(); // Apply component blocking rules cb.applyRules(true); // Apply app op and permissions cb.applyAppOpsAndPerms(); // Store the changes or discard them if (commitChanges) { // Commit changes cb.commit(); } else { // Don't commit changes, discard the rules cb.setReadOnly(); } } } } @Override public void close() { // When #setPackagesToImport(List) is used, ComponentBlocker can be null for (int i = 0; i < mUserIds.length; ++i) { for (ComponentsBlocker cb : mComponentsBlockers[i].values()) { IoUtils.closeQuietly(cb); } } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/rules/RulesStorageManager.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.rules; import android.annotation.UserIdInt; import android.content.Context; import android.os.RemoteException; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import java.io.BufferedReader; import java.io.Closeable; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Objects; import io.github.muntashirakon.AppManager.compat.AppOpsManagerCompat; import io.github.muntashirakon.AppManager.compat.NetworkPolicyManagerCompat; import io.github.muntashirakon.AppManager.compat.PermissionCompat; import io.github.muntashirakon.AppManager.magisk.MagiskProcess; import io.github.muntashirakon.AppManager.rules.compontents.ComponentUtils; import io.github.muntashirakon.AppManager.rules.struct.AppOpRule; import io.github.muntashirakon.AppManager.rules.struct.BatteryOptimizationRule; import io.github.muntashirakon.AppManager.rules.struct.ComponentRule; import io.github.muntashirakon.AppManager.rules.struct.FreezeRule; import io.github.muntashirakon.AppManager.rules.struct.MagiskDenyListRule; import io.github.muntashirakon.AppManager.rules.struct.MagiskHideRule; import io.github.muntashirakon.AppManager.rules.struct.NetPolicyRule; import io.github.muntashirakon.AppManager.rules.struct.NotificationListenerRule; import io.github.muntashirakon.AppManager.rules.struct.PermissionRule; import io.github.muntashirakon.AppManager.rules.struct.RuleEntry; import io.github.muntashirakon.AppManager.rules.struct.SsaidRule; import io.github.muntashirakon.AppManager.rules.struct.UriGrantRule; import io.github.muntashirakon.AppManager.uri.UriManager; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.FreezeUtils; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.PathReader; import io.github.muntashirakon.io.Paths; public class RulesStorageManager implements Closeable { @NonNull private final ArrayList mEntries; @GuardedBy("entries") @NonNull protected String packageName; @GuardedBy("entries") protected boolean readOnly = true; @UserIdInt protected int userId; protected RulesStorageManager(@NonNull String packageName, @UserIdInt int userId) { this.packageName = packageName; this.userId = userId; mEntries = new ArrayList<>(); try { loadEntries(getDesiredFile(false), false); } catch (Throwable ignored) { } } public void setReadOnly() { this.readOnly = true; } public void setMutable() { this.readOnly = false; } @Override public void close() { if (!readOnly) commit(); } @GuardedBy("entries") public List getAll(Class type) { synchronized (mEntries) { List newEntries = new ArrayList<>(); for (RuleEntry entry : mEntries) if (type.isInstance(entry)) newEntries.add(type.cast(entry)); return newEntries; } } @GuardedBy("entries") public List getAll(List types) { synchronized (mEntries) { List newEntries = new ArrayList<>(); for (RuleEntry entry : mEntries) if (types.contains(entry.type)) newEntries.add(entry); return newEntries; } } @GuardedBy("entries") public List getAllComponents() { return getAll(ComponentRule.class); } @GuardedBy("entries") public List getAll() { synchronized (mEntries) { return mEntries; } } @GuardedBy("entries") public int entryCount() { synchronized (mEntries) { return mEntries.size(); } } @GuardedBy("entries") public void removeEntry(RuleEntry entry) { synchronized (mEntries) { mEntries.remove(entry); } } @GuardedBy("entries") @Nullable protected RuleEntry removeEntries(String name, RuleType type) { synchronized (mEntries) { Iterator entryIterator = mEntries.iterator(); RuleEntry entry = null; while (entryIterator.hasNext()) { entry = entryIterator.next(); if (entry.name.equals(name) && entry.type.equals(type)) { entryIterator.remove(); } } return entry; } } protected void setComponent(String name, RuleType componentType, @ComponentRule.ComponentStatus String componentStatus) { ComponentRule newRule = new ComponentRule(packageName, name, componentType, componentStatus); RuleEntry oldRule = addUniqueEntry(newRule); if (oldRule instanceof ComponentRule) { newRule.setLastComponentStatus(((ComponentRule) oldRule).getComponentStatus()); } } public void setAppOp(int op, @AppOpsManagerCompat.Mode int mode) { addUniqueEntry(new AppOpRule(packageName, op, mode)); } public void setPermission(String name, boolean isGranted, @PermissionCompat.PermissionFlags int flags) { addUniqueEntry(new PermissionRule(packageName, name, isGranted, flags)); } public void setNotificationListener(String name, boolean isGranted) { addUniqueEntry(new NotificationListenerRule(packageName, name, isGranted)); } public void setMagiskHide(@NonNull MagiskProcess magiskProcess) { addUniqueEntry(new MagiskHideRule(magiskProcess)); } public void setMagiskDenyList(MagiskProcess magiskProcess) { addUniqueEntry(new MagiskDenyListRule(magiskProcess)); } public void setBatteryOptimization(boolean willOptimize) { addUniqueEntry(new BatteryOptimizationRule(packageName, willOptimize)); } public void setNetPolicy(@NetworkPolicyManagerCompat.NetPolicy int netPolicy) { addUniqueEntry(new NetPolicyRule(packageName, netPolicy)); } public void setUriGrant(@NonNull UriManager.UriGrant uriGrant) { // There could be many UriGrants addEntryInternal(new UriGrantRule(packageName, uriGrant)); } public void setSsaid(@NonNull String ssaid) { addUniqueEntry(new SsaidRule(packageName, ssaid)); } public void setFreezeType(@FreezeUtils.FreezeMethod int freezeType) { addUniqueEntry(new FreezeRule(packageName, freezeType)); } /** * Add entry, remove old entries depending on entry {@link RuleType}. */ @GuardedBy("entries") public void addEntry(@NonNull RuleEntry entry) { synchronized (mEntries) { if (entry.type.equals(RuleType.URI_GRANT)) { // UriGrant is not unique addEntryInternal(entry); } else addUniqueEntry(entry); } } /** * Remove the exact entry if exists before adding it. */ @GuardedBy("entries") private void addEntryInternal(@NonNull RuleEntry entry) { synchronized (mEntries) { removeEntry(entry); mEntries.add(entry); } } /** * Remove all entries of the given name and type before adding the entry. */ @GuardedBy("entries") @Nullable private RuleEntry addUniqueEntry(@NonNull RuleEntry entry) { synchronized (mEntries) { RuleEntry previousEntry = removeEntries(entry.name, entry.type); mEntries.add(entry); return previousEntry; } } @GuardedBy("entries") protected void loadEntries(Path file, boolean isExternal) throws IOException { String dataRow; try (BufferedReader TSVFile = new BufferedReader(new PathReader(file))) { while ((dataRow = TSVFile.readLine()) != null) { RuleEntry entry = RuleEntry.unflattenFromString(packageName, dataRow, isExternal); synchronized (mEntries) { mEntries.add(entry); } } } } @WorkerThread @GuardedBy("entries") public void commit() { try { saveEntries(getDesiredFile(true), false); } catch (IOException | RemoteException ex) { ex.printStackTrace(); } } @WorkerThread @GuardedBy("entries") public void commitExternal(Path tsvRulesFile) { try { saveEntries(tsvRulesFile, true); } catch (IOException | RemoteException ex) { ex.printStackTrace(); } } @WorkerThread @GuardedBy("entries") protected void saveEntries(Path tsvRulesFile, boolean isExternal) throws IOException, RemoteException { synchronized (mEntries) { if (mEntries.isEmpty()) { tsvRulesFile.delete(); return; } try (OutputStream TSVFile = tsvRulesFile.openOutputStream()) { ComponentUtils.storeRules(TSVFile, mEntries, isExternal); } } } @NonNull public static Path getConfDir(@NonNull Context context) { return Objects.requireNonNull(Paths.build(context.getFilesDir(), "conf")); } @NonNull protected Path getDesiredFile(boolean create) throws IOException { Path confDir = getConfDir(ContextUtils.getContext()); if (!confDir.exists()) { confDir.mkdirs(); } if (create) { return confDir.findOrCreateFile(packageName + ".tsv", null); } return confDir.findFile(packageName + ".tsv"); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/rules/RulesTypeSelectionDialogFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.rules; import android.annotation.UserIdInt; import android.app.Dialog; import android.net.Uri; import android.os.Bundle; import android.os.PowerManager; import android.os.UserHandleHidden; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.os.BundleCompat; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.FragmentActivity; import java.io.IOException; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.settings.SettingsActivity; import io.github.muntashirakon.AppManager.utils.CpuUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.dialog.SearchableMultiChoiceDialogBuilder; public class RulesTypeSelectionDialogFragment extends DialogFragment { public static final String TAG = "RulesTypeSelectionDialogFragment"; public static final String ARG_MODE = "ARG_MODE"; // int public static final String ARG_URI = "ARG_URI"; // Uri public static final String ARG_PKG = "ARG_PKG"; // Package Names or null (for all) public static final String ARG_USERS = "ARG_USERS"; // Integer array of user handles @IntDef(value = { MODE_IMPORT, MODE_EXPORT }) public @interface Mode { } public static final int MODE_IMPORT = 1; public static final int MODE_EXPORT = 2; public static final RuleType[] RULE_TYPES = new RuleType[]{ RuleType.ACTIVITY, RuleType.SERVICE, RuleType.RECEIVER, RuleType.PROVIDER, RuleType.APP_OP, RuleType.PERMISSION, }; private FragmentActivity mActivity; @Nullable private Uri mUri; private List mPackages = null; private HashSet mSelectedTypes; @UserIdInt private int[] mUserIds; @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { mActivity = requireActivity(); Bundle args = requireArguments(); @Mode int mode = args.getInt(ARG_MODE, MODE_EXPORT); mPackages = args.getStringArrayList(ARG_PKG); mUri = BundleCompat.getParcelable(args, ARG_URI, Uri.class); mUserIds = args.getIntArray(ARG_USERS); if (mUserIds == null) mUserIds = new int[]{UserHandleHidden.myUserId()}; if (mUri == null) return super.onCreateDialog(savedInstanceState); List ruleIndexes = new ArrayList<>(); for (int i = 0; i < RULE_TYPES.length; ++i) { ruleIndexes.add(i); } mSelectedTypes = new HashSet<>(RULE_TYPES.length); return new SearchableMultiChoiceDialogBuilder<>(mActivity, ruleIndexes, R.array.rule_types) .setTitle(mode == MODE_IMPORT ? R.string.import_options : R.string.export_options) .addSelections(ruleIndexes) .setPositiveButton(mode == MODE_IMPORT ? R.string.pref_import : R.string.pref_export, (dialog1, which, selections) -> { for (int i : selections) { mSelectedTypes.add(RULE_TYPES[i]); } Log.d("TestImportExport", "Types: %s\nURI: %s", mSelectedTypes, mUri); if (mActivity instanceof SettingsActivity) { ((SettingsActivity) mActivity).progressIndicator.show(); } if (mode == MODE_IMPORT) { handleImport(); } else handleExport(); }) .setNegativeButton(getResources().getString(R.string.cancel), null) .create(); } private void handleExport() { if (mUri == null) { return; } WeakReference activityRef = new WeakReference<>(mActivity); ThreadUtils.postOnBackgroundThread(() -> { PowerManager.WakeLock wakeLock = CpuUtils.getPartialWakeLock("rules_exporter"); wakeLock.acquire(); try { RulesExporter exporter = new RulesExporter(new ArrayList<>(mSelectedTypes), mPackages, mUserIds); exporter.saveRules(mUri); ThreadUtils.postOnMainThread(() -> UIUtils.displayShortToast(R.string.the_export_was_successful)); } catch (IOException e) { ThreadUtils.postOnMainThread(() -> UIUtils.displayLongToast(R.string.export_failed)); } finally { CpuUtils.releaseWakeLock(wakeLock); } hideProgressBar(activityRef); }); } private void handleImport() { if (mUri == null) { return; } WeakReference activityRef = new WeakReference<>(mActivity); ThreadUtils.postOnBackgroundThread(() -> { PowerManager.WakeLock wakeLock = CpuUtils.getPartialWakeLock("rules_exporter"); wakeLock.acquire(); try (RulesImporter importer = new RulesImporter(new ArrayList<>(mSelectedTypes), mUserIds)) { importer.addRulesFromUri(mUri); if (mPackages != null) importer.setPackagesToImport(mPackages); importer.applyRules(true); ThreadUtils.postOnMainThread(() -> UIUtils.displayShortToast(R.string.the_import_was_successful)); } catch (IOException e) { ThreadUtils.postOnMainThread(() -> UIUtils.displayLongToast(R.string.import_failed)); } finally { CpuUtils.releaseWakeLock(wakeLock); } hideProgressBar(activityRef); }); } private void hideProgressBar(@NonNull WeakReference activityRef) { if (activityRef.get() instanceof SettingsActivity) { ThreadUtils.postOnMainThread(() -> { FragmentActivity activity = activityRef.get(); if (activity != null) { ((SettingsActivity) activity).progressIndicator.hide(); } }); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/rules/compontents/ComponentUtils.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.rules.compontents; import android.annotation.UserIdInt; import android.app.AppOpsManager; import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageInfo; import android.os.RemoteException; import android.util.Xml; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.annotation.WorkerThread; import org.xmlpull.v1.XmlPullParser; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; import io.github.muntashirakon.AppManager.StaticDataset; import io.github.muntashirakon.AppManager.compat.AppOpsManagerCompat; import io.github.muntashirakon.AppManager.compat.PermissionCompat; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.rules.RuleType; import io.github.muntashirakon.AppManager.rules.RulesStorageManager; import io.github.muntashirakon.AppManager.rules.struct.AppOpRule; import io.github.muntashirakon.AppManager.rules.struct.ComponentRule; import io.github.muntashirakon.AppManager.rules.struct.PermissionRule; import io.github.muntashirakon.AppManager.rules.struct.RuleEntry; import io.github.muntashirakon.AppManager.types.UserPackagePair; import io.github.muntashirakon.AppManager.utils.PackageUtils; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; public final class ComponentUtils { public static boolean isTracker(String componentName) { return StaticDataset.getSearchableTrackerSignatures().search(componentName).length > 0; } public static int getTrackerComponentsCountForPackage(PackageInfo packageInfo) { HashMap components = PackageUtils.collectComponentClassNames(packageInfo); return (int) components.keySet().stream() .filter(ComponentUtils::isTracker) .count(); } @NonNull public static Map getTrackerComponentsForPackage(PackageInfo packageInfo) { HashMap components = PackageUtils.collectComponentClassNames(packageInfo); return components.entrySet().stream() .filter(entry -> isTracker(entry.getKey())) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } @NonNull public static Map getTrackerComponentsForPackage(String packageName, @UserIdInt int userHandle) { HashMap components = PackageUtils.collectComponentClassNames(packageName, userHandle); return components.entrySet().stream() .filter(entry -> isTracker(entry.getKey())) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } public static void blockTrackingComponents(@NonNull UserPackagePair pair) { Map components = ComponentUtils.getTrackerComponentsForPackage(pair.getPackageName(), pair.getUserId()); try (ComponentsBlocker cb = ComponentsBlocker.getMutableInstance(pair.getPackageName(), pair.getUserId())) { for (String componentName : components.keySet()) { cb.addComponent(componentName, Objects.requireNonNull(components.get(componentName))); } cb.applyRules(true); } } @WorkerThread @NonNull public static List blockTrackingComponents(@NonNull Collection userPackagePairs) { List failedPkgList = new ArrayList<>(); for (UserPackagePair pair : userPackagePairs) { try { blockTrackingComponents(pair); } catch (Exception e) { e.printStackTrace(); failedPkgList.add(pair); } } return failedPkgList; } public static void unblockTrackingComponents(@NonNull UserPackagePair pair) { Map components = getTrackerComponentsForPackage(pair.getPackageName(), pair.getUserId()); try (ComponentsBlocker cb = ComponentsBlocker.getMutableInstance(pair.getPackageName(), pair.getUserId())) { for (String componentName : components.keySet()) { cb.removeComponent(componentName); } cb.applyRules(true); } } @WorkerThread @NonNull public static List unblockTrackingComponents(@NonNull Collection userPackagePairs) { List failedPkgList = new ArrayList<>(); for (UserPackagePair pair : userPackagePairs) { try { unblockTrackingComponents(pair); } catch (Exception e) { e.printStackTrace(); failedPkgList.add(pair); } } return failedPkgList; } public static void blockFilteredComponents(@NonNull UserPackagePair pair, String[] signatures) { HashMap components = PackageUtils.getFilteredComponents(pair.getPackageName(), pair.getUserId(), signatures); try (ComponentsBlocker cb = ComponentsBlocker.getMutableInstance(pair.getPackageName(), pair.getUserId())) { for (String componentName : components.keySet()) { cb.addComponent(componentName, Objects.requireNonNull(components.get(componentName))); } cb.applyRules(true); } } public static void unblockFilteredComponents(@NonNull UserPackagePair pair, String[] signatures) { HashMap components = PackageUtils.getFilteredComponents(pair.getPackageName(), pair.getUserId(), signatures); try (ComponentsBlocker cb = ComponentsBlocker.getMutableInstance(pair.getPackageName(), pair.getUserId())) { for (String componentName : components.keySet()) { cb.removeComponent(componentName); } cb.applyRules(true); } } public static void storeRules(@NonNull OutputStream os, @NonNull List rules, boolean isExternal) throws IOException { for (RuleEntry entry : rules) { os.write((entry.flattenToString(isExternal) + "\n").getBytes()); } } @NonNull public static List getAllPackagesWithRules(@NonNull Context context) { List packages = new ArrayList<>(); Path confDir = RulesStorageManager.getConfDir(context); Path[] paths = confDir.listFiles((dir, name) -> name.endsWith(".tsv")); for (Path path : paths) { packages.add(Paths.trimPathExtension(path.getUri().getLastPathSegment())); } return packages; } @WorkerThread public static void removeAllRules(@NonNull String packageName, int userHandle) { int uid = PackageUtils.getAppUid(new UserPackagePair(packageName, userHandle)); try (ComponentsBlocker cb = ComponentsBlocker.getMutableInstance(packageName, userHandle)) { // Remove all blocking rules for (ComponentRule entry : cb.getAllComponents()) { cb.removeComponent(entry.name); } cb.applyRules(true); // Reset configured app ops AppOpsManagerCompat appOpsManager = new AppOpsManagerCompat(); try { appOpsManager.resetAllModes(userHandle, packageName); for (AppOpRule entry : cb.getAll(AppOpRule.class)) { try { appOpsManager.setMode(entry.getOp(), uid, packageName, AppOpsManager.MODE_DEFAULT); cb.removeEntry(entry); } catch (Exception e) { e.printStackTrace(); } } } catch (Exception e) { e.printStackTrace(); } // Grant configured permissions for (PermissionRule entry : cb.getAll(PermissionRule.class)) { try { PermissionCompat.grantPermission(packageName, entry.name, userHandle); cb.removeEntry(entry); } catch (RemoteException e) { Log.e("ComponentUtils", "Cannot revoke permission %s for package %s", e, entry.name, packageName); } } } } @NonNull public static HashMap getIFWRulesForPackage(@NonNull String packageName) { return getIFWRulesForPackage(packageName, Paths.get(ComponentsBlocker.SYSTEM_RULES_PATH)); } @VisibleForTesting @NonNull public static HashMap getIFWRulesForPackage(@NonNull String packageName, @NonNull Path path) { HashMap rules = new HashMap<>(); Path[] files = path.listFiles((dir, name) -> { // For our case, name must start with package name to support apps like Watt, Blocker and MyAndroidTools, // and to prevent unwanted situation, such as when the contains unsupported tags such as intent-filter. return name.startsWith(packageName) && name.endsWith(".xml"); }); for (Path ifwRulesFile : files) { // Get file contents try (InputStream inputStream = ifwRulesFile.openInputStream()) { // Read rules rules.putAll(readIFWRules(inputStream, packageName)); } catch (IOException e) { e.printStackTrace(); } } return rules; } public static final String TAG_RULES = "rules"; public static final String TAG_ACTIVITY = "activity"; public static final String TAG_BROADCAST = "broadcast"; public static final String TAG_SERVICE = "service"; @NonNull public static HashMap readIFWRules(@NonNull InputStream inputStream, @NonNull String packageName) { HashMap rules = new HashMap<>(); XmlPullParser parser = Xml.newPullParser(); try { parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false); parser.setInput(inputStream, null); parser.nextTag(); parser.require(XmlPullParser.START_TAG, null, TAG_RULES); int event = parser.nextTag(); RuleType componentType = null; while (event != XmlPullParser.END_DOCUMENT) { String name = parser.getName(); switch (event) { case XmlPullParser.START_TAG: if (name.equals(TAG_ACTIVITY) || name.equals(TAG_BROADCAST) || name.equals(TAG_SERVICE)) { componentType = getComponentType(name); } break; case XmlPullParser.END_TAG: if (name.equals("component-filter")) { String fullKey = parser.getAttributeValue(null, "name"); ComponentName cn = ComponentName.unflattenFromString(fullKey); if (cn.getPackageName().equals(packageName)) { rules.put(cn.getClassName(), componentType); } } } event = parser.nextTag(); } } catch (Throwable ignore) { // The file contains errors, simply ignore } return rules; } /** * Get component type from TAG_* constants * * @param componentTag Name of the constant: one of the TAG_* * @return One of the {@link RuleType} */ @Nullable static RuleType getComponentType(@NonNull String componentTag) { switch (componentTag) { case TAG_ACTIVITY: return RuleType.ACTIVITY; case TAG_BROADCAST: return RuleType.RECEIVER; case TAG_SERVICE: return RuleType.SERVICE; default: return null; } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/rules/compontents/ComponentsBlocker.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.rules.compontents; import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT; import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED; import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED; import static android.content.pm.PackageManager.DONT_KILL_APP; import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.MATCH_DISABLED_COMPONENTS; import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.MATCH_UNINSTALLED_PACKAGES; import android.app.AppOpsManager; import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Build; import android.os.RemoteException; import android.system.ErrnoException; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import io.github.muntashirakon.AppManager.compat.AppOpsManagerCompat; import io.github.muntashirakon.AppManager.compat.ApplicationInfoCompat; import io.github.muntashirakon.AppManager.compat.PackageManagerCompat; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.permission.PermUtils; import io.github.muntashirakon.AppManager.permission.Permission; import io.github.muntashirakon.AppManager.rules.RuleType; import io.github.muntashirakon.AppManager.rules.RulesStorageManager; import io.github.muntashirakon.AppManager.rules.struct.AppOpRule; import io.github.muntashirakon.AppManager.rules.struct.ComponentRule; import io.github.muntashirakon.AppManager.rules.struct.PermissionRule; import io.github.muntashirakon.AppManager.rules.struct.RuleEntry; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.utils.PackageUtils; import io.github.muntashirakon.io.AtomicExtendedFile; import io.github.muntashirakon.io.Paths; /** * Block application components: activities, broadcasts, services and providers. *

* Activities, broadcasts and services are blocked via Intent Firewall (which is superior to * pm disable component). Rules for each package is saved as a separate tsv file * named after its package name and saved to {@code /data/data/${applicationId}/files/conf}. In case * of activities, broadcasts and services, the rules are finally saved to {@link #SYSTEM_RULES_PATH}. *

* Providers are blocked via {@link PackageManager#setComponentEnabledSetting(ComponentName, int, int)} * since there's no way to block them via Intent Firewall. Blocked providers are only kept in the * {@code /data/data/${applicationId}/files/conf} directory. * * @see IntentFirewall.java */ public final class ComponentsBlocker extends RulesStorageManager { public static final String TAG = "ComponentBlocker"; public static final String SYSTEM_RULES_PATH; static { SYSTEM_RULES_PATH = Build.VERSION.SDK_INT <= Build.VERSION_CODES.M ? "/data/secure/system/ifw" : "/data/system/ifw"; } /** * Get a new or existing IMMUTABLE instance of {@link ComponentsBlocker}. The existing instance * will only be returned if the existing instance has the same package name as the original. * This reads rules from the {@link #SYSTEM_RULES_PATH}. If reading rules is necessary, use * {@link #getInstance(String, int, boolean)} with the last argument set to true. It is also * possible to make this instance mutable by calling {@link #setMutable()} and once set mutable, * closing this instance will commit the changes automatically. To prevent this, * {@link #setReadOnly()} should be called before closing the instance. * * @param packageName The package whose instance is to be returned * @param userHandle The user to whom the rules belong * @return New or existing immutable instance for the package * @see #getInstance(String, int, boolean) * @see #getMutableInstance(String, int) */ @NonNull public static ComponentsBlocker getInstance(@NonNull String packageName, int userHandle) { return getInstance(packageName, userHandle, true); } /** * Get a new or existing MUTABLE instance of {@link ComponentsBlocker}. This DOES NOT read rules * from the {@link #SYSTEM_RULES_PATH}. This is essentially the same as calling * {@link #getInstance(String, int, boolean)} with the last argument set to {@code true} and * calling {@link #setMutable()} after that. Closing this instance will commit the changes * automatically. To prevent this, {@link #setReadOnly()} should be called before closing * the instance. * * @param packageName The package whose instance is to be returned * @param userHandle The user to whom the rules belong * @return New or existing mutable instance for the package * @see #getInstance(String, int) * @see #getInstance(String, int, boolean) */ @NonNull public static ComponentsBlocker getMutableInstance(@NonNull String packageName, int userHandle) { ComponentsBlocker componentsBlocker = getInstance(packageName, userHandle, false); componentsBlocker.readOnly = false; return componentsBlocker; } /** * Get a new or existing IMMUTABLE instance of {@link ComponentsBlocker}. The existing instance * will only be returned if the existing instance has the same package name as the original. It * is also possible to make this instance mutable by calling {@link #setMutable()} and once set * mutable, closing this instance will commit the changes automatically. To prevent this, * {@link #setReadOnly()} should be called before closing the instance. * * @param packageName The package whose instance is to be returned * @param userHandle The user to whom the rules belong * @param reloadFromDisk Whether to load rules from the {@link #SYSTEM_RULES_PATH} * @return New or existing immutable instance for the package * @see #getInstance(String, int) * @see #getMutableInstance(String, int) */ @NonNull public static ComponentsBlocker getInstance(@NonNull String packageName, int userHandle, boolean reloadFromDisk) { Objects.requireNonNull(packageName); ComponentsBlocker blocker = new ComponentsBlocker(packageName, userHandle); if (reloadFromDisk && SelfPermissions.canBlockByIFW()) { blocker.retrieveDisabledComponents(); blocker.invalidateComponents(); } blocker.readOnly = true; return blocker; } @NonNull private final AtomicExtendedFile mRulesFile; @Nullable private Set mComponents; @Nullable private PackageInfo mPackageInfo; private ComponentsBlocker(@NonNull String packageName, int userHandle) { super(packageName, userHandle); mRulesFile = new AtomicExtendedFile(Objects.requireNonNull(Paths.get(SYSTEM_RULES_PATH).getFile()) .getChildFile(packageName + ".xml")); try { mPackageInfo = PackageManagerCompat.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES | PackageManager.GET_RECEIVERS | PackageManager.GET_PROVIDERS | MATCH_DISABLED_COMPONENTS | MATCH_UNINSTALLED_PACKAGES | PackageManager.GET_SERVICES | PackageManagerCompat.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES, userHandle); } catch (Throwable e) { Log.e(TAG, e.getMessage(), e); } mComponents = mPackageInfo != null ? PackageUtils.collectComponentClassNames(mPackageInfo).keySet() : null; } /** * Reload package components */ public void reloadComponents() { mComponents = mPackageInfo != null ? PackageUtils.collectComponentClassNames(mPackageInfo).keySet() : null; } /** * Apply all rules configured within App Manager. This also includes {@link #SYSTEM_RULES_PATH}. * * @param context Application Context * @param userHandle The user to apply rules * @return {@code true} iff all rules are applied correctly. */ @WorkerThread public static boolean applyAllRules(@NonNull Context context, int userHandle) { // Apply all rules from conf folder File confPath = new File(context.getFilesDir(), "conf"); String[] packageNamesWithTSVExt = confPath.list((dir, name) -> name.endsWith(".tsv")); boolean isSuccessful = true; if (packageNamesWithTSVExt != null) { // Apply rules for (String packageNameWithTSVExt : packageNamesWithTSVExt) { try (ComponentsBlocker cb = getMutableInstance(Paths.trimPathExtension(packageNameWithTSVExt), userHandle)) { isSuccessful &= cb.applyRules(true); } } } return isSuccessful; } /** * Whether the given component is blocked. * * @param componentName The component name to check * @return {@code true} if blocked, {@code false} otherwise */ public boolean isComponentBlocked(String componentName) { ComponentRule cr = getComponent(componentName); return cr != null && cr.isBlocked(); } /** * Check if the given component exists in the rules. It does not necessarily mean that the * component is being blocked. * * @param componentName The component name to check * @return {@code true} if exists, {@code false} otherwise * @see ComponentsBlocker#isComponentBlocked(String) */ @GuardedBy("entries") public boolean hasComponentName(String componentName) { for (ComponentRule entry : getAllComponents()) if (entry.name.equals(componentName)) return true; return false; } /** * Get number of components among other rules. * * @return Number of components */ public int componentCount() { int count = 0; for (ComponentRule entry : getAllComponents()) { if (!entry.toBeRemoved()) ++count; } return count; } @Nullable public ComponentRule getComponent(String componentName) { for (ComponentRule rule : getAllComponents()) { if (rule.name.equals(componentName)) return rule; } return null; } /** * Add the given component to the rules list with user preferred component status, does nothing if the instance is * immutable. * * @param componentName The component to add * @param componentType Component type * @see #addEntry(RuleEntry) */ public void addComponent(String componentName, RuleType componentType) { if (!readOnly) setComponent(componentName, componentType, Prefs.Blocking.getDefaultBlockingMethod()); } /** * Add the given component to the rules list, does nothing if the instance is immutable. * * @param componentName The component to add * @param componentType Component type * @param componentStatus Component status * @see #addEntry(RuleEntry) */ public void addComponent(String componentName, RuleType componentType, @ComponentRule.ComponentStatus String componentStatus) { if (!readOnly) setComponent(componentName, componentType, componentStatus); } /** * Suggest removal of the given component from the rules, does nothing if the instance is * immutable or the component does not exist. The rules are only applied when {@link #commit()} * is called. * * @param componentName The component to remove * @see #removeEntry(RuleEntry) * @see #deleteComponent(String) */ public void removeComponent(String componentName) { if (readOnly) return; ComponentRule cr = getComponent(componentName); if (cr != null) { setComponent(componentName, cr.type, ComponentRule.COMPONENT_TO_BE_DEFAULTED); } } /** * Remove component from the list rules without triggering a component removal request, does nothing * if the instance is immutable or the component does not exist. The rules are only applied when * {@link #commit()} is called. * * @param componentName The component to remove * @see #removeEntry(RuleEntry) * @see #removeComponent(String) */ public void deleteComponent(String componentName) { if (readOnly) return; ComponentRule cr = getComponent(componentName); if (cr != null) { removeEntries(componentName, cr.type); } } /** * Save the disabled components in the {@link #SYSTEM_RULES_PATH}. * * @return {@code true} iff the components could be saved. */ private boolean saveDisabledComponents(boolean apply) { if (readOnly) { Log.e(TAG, "Read-only instance."); return false; } if (!apply || componentCount() == 0) { // No components set, delete if already exists mRulesFile.delete(); return true; } StringBuilder activities = new StringBuilder(); StringBuilder services = new StringBuilder(); StringBuilder receivers = new StringBuilder(); for (ComponentRule component : getAllComponents()) { // Ignore components requiring unblocking if (!component.isIfw()) continue; String componentFilter = " \n"; switch (component.type) { case ACTIVITY: activities.append(componentFilter); break; case RECEIVER: receivers.append(componentFilter); break; case SERVICE: services.append(componentFilter); break; case PROVIDER: } } String rules = "\n" + ((activities.length() == 0) ? "" : "\n" + activities + "\n") + ((services.length() == 0) ? "" : "\n" + services + "\n") + ((receivers.length() == 0) ? "" : "\n" + receivers + "\n") + ""; // Save rules FileOutputStream rulesStream = null; try { rulesStream = mRulesFile.startWrite(); Log.d(TAG, "Rules: %s", rules); rulesStream.write(rules.getBytes()); mRulesFile.finishWrite(rulesStream); //noinspection OctalInteger mRulesFile.getBaseFile().setMode(0666); return true; } catch (IOException e) { Log.e(TAG, "Failed to write rules for package %s", e, packageName); mRulesFile.failWrite(rulesStream); return false; } catch (ErrnoException e) { Log.w(TAG, "Failed to alter permission of IFW for package %s", e, packageName); return true; } } /** * Find if there is any component that needs blocking. Previous implementations checked for * rules file in the system IFW directory as well, but since all controls are now inside the app * itself, it's no longer deemed necessary to check the existence of the file. Besides, previous * implementation (which was similar to Watt's) did not take providers into account, which are * blocked via {@code pm}. * * @return {@code true} if there's no pending rules, {@code false} otherwise */ public boolean isRulesApplied() { List entries = getAllComponents(); for (ComponentRule entry : entries) { if (!entry.isApplied()) { return false; } } return true; } /** * Apply the currently modified rules if the argument apply is true. Since IFW is used, when * apply is true, the IFW rules are saved to {@link #SYSTEM_RULES_PATH} and components that are * set to be removed or unblocked will be removed. If apply is set to false, all rules will be * removed but before that all components will be set to their default state (ie., the state * described in the app manifest). * * @param apply Whether to apply the rules or remove them altogether. * @return {@code true} iff all rules are applied correctly. */ @WorkerThread public boolean applyRules(boolean apply) { // Check root. If no root is present, check if the app is test-only. if (!SelfPermissions.canModifyAppComponentStates(userId, packageName, mPackageInfo != null && ApplicationInfoCompat.isTestOnly(mPackageInfo.applicationInfo))) { return false; } // Validate components validateComponents(); // Save blocked IFW components or remove them based on the value of apply if (SelfPermissions.canBlockByIFW() && !saveDisabledComponents(apply)) { return false; } // Enable/disable components List allEntries = getAllComponents(); Log.d(TAG, "All: %s", allEntries); boolean isSuccessful = true; if (apply) { for (ComponentRule entry : allEntries) { if (entry.applyDefaultState()) { // Need to set component state to default first and do nothing try { PackageManagerCompat.setComponentEnabledSetting(entry.getComponentName(), COMPONENT_ENABLED_STATE_DEFAULT, DONT_KILL_APP, userId); removeEntry(entry); } catch (Throwable e) { isSuccessful = false; Log.e(TAG, "Could not enable component: %s/%s", e, packageName, entry.name); } } switch (entry.getComponentStatus()) { case ComponentRule.COMPONENT_TO_BE_DEFAULTED: // Set component state to default and remove it try { PackageManagerCompat.setComponentEnabledSetting( entry.getComponentName(), COMPONENT_ENABLED_STATE_DEFAULT, DONT_KILL_APP, userId); removeEntry(entry); } catch (Throwable e) { isSuccessful = false; Log.e(TAG, "Could not enable component: %s/%s", e, packageName, entry.name); } break; case ComponentRule.COMPONENT_TO_BE_ENABLED: // Enable components try { PackageManagerCompat.setComponentEnabledSetting( entry.getComponentName(), COMPONENT_ENABLED_STATE_ENABLED, DONT_KILL_APP, userId); setComponent(entry.name, entry.type, ComponentRule.COMPONENT_ENABLED); } catch (Throwable e) { isSuccessful = false; Log.e(TAG, "Could not disable component: %s/%s", e, packageName, entry.name); } break; case ComponentRule.COMPONENT_TO_BE_BLOCKED_IFW: setComponent(entry.name, entry.type, ComponentRule.COMPONENT_BLOCKED_IFW); break; case ComponentRule.COMPONENT_TO_BE_BLOCKED_IFW_DISABLE: case ComponentRule.COMPONENT_TO_BE_DISABLED: // Disable components try { PackageManagerCompat.setComponentEnabledSetting( entry.getComponentName(), COMPONENT_ENABLED_STATE_DISABLED, DONT_KILL_APP, userId); setComponent(entry.name, entry.type, entry.getCounterpartOfToBe()); } catch (Throwable e) { isSuccessful = false; Log.e(TAG, "Could not disable component: %s/%s", e, packageName, entry.name); } break; default: setComponent(entry.name, entry.type, entry.getCounterpartOfToBe()); } } } else { // Enable all, remove to be removed components and set others to be blocked for (ComponentRule entry : allEntries) { // Enable components if they're disabled by other methods. // IFW rules are already removed above. try { PackageManagerCompat.setComponentEnabledSetting(entry.getComponentName(), COMPONENT_ENABLED_STATE_DEFAULT, DONT_KILL_APP, userId); if (entry.toBeRemoved()) { removeEntry(entry); } else setComponent(entry.name, entry.type, entry.getToBe()); } catch (Throwable e) { isSuccessful = false; Log.e(TAG, "Could not enable component: %s/%s", e, packageName, entry.name); } } } return isSuccessful; } /** * Apply all configured app ops and permissions. * * @return {@code true} iff all the rules are applied correctly. */ public boolean applyAppOpsAndPerms() { if (mPackageInfo == null) { return false; } boolean isSuccessful = true; int uid = mPackageInfo.applicationInfo.uid; AppOpsManagerCompat appOpsManager = new AppOpsManagerCompat(); // Apply all app ops for (AppOpRule appOp : getAll(AppOpRule.class)) { try { appOpsManager.setMode(appOp.getOp(), uid, packageName, appOp.getMode()); } catch (Throwable e) { isSuccessful = false; Log.e(TAG, "Could not set mode %d for app op %d", e, appOp.getMode(), appOp.getOp()); } } // Apply all permissions for (PermissionRule permissionRule : getAll(PermissionRule.class)) { Permission permission = permissionRule.getPermission(true); try { permission.setAppOpAllowed(permission.getAppOp() != AppOpsManagerCompat.OP_NONE && appOpsManager .checkOperation(permission.getAppOp(), uid, packageName) == AppOpsManager.MODE_ALLOWED); if (permission.isGranted()) { PermUtils.grantPermission(mPackageInfo, permission, appOpsManager, true, true); } else { PermUtils.revokePermission(mPackageInfo, permission, appOpsManager, true); } } catch (Throwable e) { isSuccessful = false; Log.e(TAG, "Could not %s %s", e, (permission.isGranted() ? "grant" : "revoke"), permissionRule.name); } } return isSuccessful; } /** * Check if the components are up-to-date and remove the ones that are not up-to-date. */ private void validateComponents() { if (mComponents == null) { // No validation required return; } List allEntries = getAllComponents(); for (ComponentRule entry : allEntries) { if (!mComponents.contains(entry.name)) { // Remove non-existent components removeEntry(entry); } } } /** * Check all components that needs to be disabled/enabled and assign to be disabled/enabled if * necessary. */ public int invalidateComponents() { int invalidated = 0; boolean canCheckExistence = mComponents != null; List allEntries = getAllComponents(); for (ComponentRule entry : allEntries) { // First check if it actually exists if (canCheckExistence && !mComponents.contains(entry.name)) { removeEntry(entry); ++invalidated; continue; } try { int s = PackageManagerCompat.getComponentEnabledSetting(new ComponentName(entry.packageName, entry.name), userId); switch (entry.getComponentStatus()) { case ComponentRule.COMPONENT_BLOCKED_IFW_DISABLE: case ComponentRule.COMPONENT_DISABLED: // If component is enabled/defaulted, make it to be disabled if (s == COMPONENT_ENABLED_STATE_ENABLED || s == COMPONENT_ENABLED_STATE_DEFAULT) { addComponent(entry.name, entry.type, entry.getToBe()); ++invalidated; } break; case ComponentRule.COMPONENT_ENABLED: // If component is not enabled, make it to be enabled if (s != COMPONENT_ENABLED_STATE_ENABLED) { addComponent(entry.name, entry.type, entry.getToBe()); ++invalidated; } break; } } catch (Throwable ignore) { } } return invalidated; } /** * Retrieve a set of disabled components from the {@link #SYSTEM_RULES_PATH}. If they are * available add them to the rules, overridden if necessary. */ private void retrieveDisabledComponents() { Log.d(TAG, "Retrieving disabled components for package %s", packageName); if (!mRulesFile.exists() || mRulesFile.getBaseFile().length() == 0) { // System doesn't have any rules. // Load the rules saved inside App Manager for (ComponentRule entry : getAllComponents()) { setComponent(entry.name, entry.type, entry.getToBe()); } return; } try (InputStream rulesStream = mRulesFile.openRead()) { HashMap components = ComponentUtils.readIFWRules(rulesStream, packageName); for (Map.Entry component : components.entrySet()) { // Override existing rule for the component if it exists setComponent(component.getKey(), component.getValue(), ComponentRule.COMPONENT_BLOCKED_IFW_DISABLE); } Log.d(TAG, "Retrieved components for package %s", packageName); } catch (IOException | RemoteException ignored) { } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/rules/compontents/ExternalComponentsImporter.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.rules.compontents; import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.MATCH_DISABLED_COMPONENTS; import android.annotation.SuppressLint; import android.content.pm.ActivityInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ProviderInfo; import android.content.pm.ServiceInfo; import android.net.Uri; import android.os.RemoteException; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import org.json.JSONArray; import org.json.JSONObject; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import io.github.muntashirakon.AppManager.compat.AppOpsManagerCompat; import io.github.muntashirakon.AppManager.compat.PackageManagerCompat; import io.github.muntashirakon.AppManager.rules.RuleType; import io.github.muntashirakon.AppManager.types.UserPackagePair; import io.github.muntashirakon.AppManager.utils.PackageUtils; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; /** * Import components from external apps like Blocker, Watt */ public class ExternalComponentsImporter { public static void setModeToFilteredAppOps(@NonNull AppOpsManagerCompat appOpsManager, @NonNull UserPackagePair pair, int[] appOps, @AppOpsManagerCompat.Mode int mode) throws RemoteException { Collection appOpList; appOpList = PackageUtils.getFilteredAppOps(pair.getPackageName(), pair.getUserId(), appOps, mode); try (ComponentsBlocker cb = ComponentsBlocker.getMutableInstance(pair.getPackageName(), pair.getUserId())) { for (int appOp : appOpList) { appOpsManager.setMode(appOp, PackageUtils.getAppUid(pair), pair.getPackageName(), mode); cb.setAppOp(appOp, mode); } cb.applyRules(true); } } @WorkerThread @NonNull public static List applyFromExistingBlockList(@NonNull List packageNames, int userHandle) { List failedPkgList = new ArrayList<>(); HashMap components; Path rulesPath = Paths.get(ComponentsBlocker.SYSTEM_RULES_PATH); for (String packageName : packageNames) { components = PackageUtils.getUserDisabledComponentsForPackage(packageName, userHandle); try (ComponentsBlocker cb = ComponentsBlocker.getMutableInstance(packageName, userHandle)) { for (String componentName : components.keySet()) { cb.addComponent(componentName, components.get(componentName)); } // Remove IFW blocking rules if exists Path[] rulesFiles = rulesPath.listFiles((dir, name) -> name.startsWith(packageName) && name.endsWith("xml")); for (Path rulesFile : rulesFiles) { rulesFile.delete(); } cb.applyRules(true); } catch (Exception e) { e.printStackTrace(); failedPkgList.add(packageName); } } return failedPkgList; } @WorkerThread @NonNull public static List applyFromBlocker(@NonNull List uriList, int[] userHandles) { List failedFiles = new ArrayList<>(); for (Uri uri : uriList) { String filename = Paths.get(uri).getName(); try { for (int userHandle : userHandles) { applyFromBlocker(uri, userHandle); } } catch (Exception e) { failedFiles.add(filename); e.printStackTrace(); } } return failedFiles; } @WorkerThread @NonNull public static List applyFromWatt(@NonNull List uriList, int[] userHandles) { List failedFiles = new ArrayList<>(); for (Uri uri : uriList) { Path path = Paths.get(uri); String filename = path.getName(); try { for (int userHandle : userHandles) { applyFromWatt(Paths.trimPathExtension(filename), path, userHandle); } } catch (IOException e) { failedFiles.add(filename); e.printStackTrace(); } } return failedFiles; } /** * Watt only supports IFW, so copy them directly */ @WorkerThread private static void applyFromWatt(String packageName, Path path, int userHandle) throws IOException { try (InputStream rulesStream = path.openInputStream()) { try (ComponentsBlocker cb = ComponentsBlocker.getMutableInstance(packageName, userHandle)) { HashMap components = ComponentUtils.readIFWRules(rulesStream, packageName); for (String componentName : components.keySet()) { // Overwrite rules if exists cb.addComponent(componentName, components.get(componentName)); } cb.applyRules(true); } } } /** * Apply from blocker * * @param uri File URI */ @WorkerThread @SuppressLint("WrongConstant") private static void applyFromBlocker(Uri uri, int userHandle) throws Exception { String jsonString = Paths.get(uri).getContentAsString(); HashMap> packageComponents = new HashMap<>(); HashMap packageInfoList = new HashMap<>(); JSONObject jsonObject = new JSONObject(jsonString); JSONArray components = jsonObject.getJSONArray("components"); List uninstalledApps = new ArrayList<>(); for (int i = 0; i < components.length(); ++i) { JSONObject component = (JSONObject) components.get(i); String packageName = component.getString("packageName"); if (uninstalledApps.contains(packageName)) continue; if (!packageInfoList.containsKey(packageName)) { try { packageInfoList.put(packageName, PackageManagerCompat.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES | PackageManager.GET_RECEIVERS | PackageManager.GET_PROVIDERS | PackageManager.GET_SERVICES | MATCH_DISABLED_COMPONENTS | PackageManagerCompat.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES, userHandle)); } catch (Exception e) { // App not installed uninstalledApps.add(packageName); continue; } } String componentName = component.getString("name"); if (!packageComponents.containsKey(packageName)) { packageComponents.put(packageName, new HashMap<>()); } // Fetch package components using PackageInfo since the type used in Blocker can be wrong //noinspection ConstantConditions packageComponents.get(packageName).put(componentName, getType(componentName, packageInfoList.get(packageName))); } if (packageComponents.size() > 0) { for (String packageName : packageComponents.keySet()) { HashMap disabledComponents = packageComponents.get(packageName); //noinspection ConstantConditions if (disabledComponents.size() > 0) { try (ComponentsBlocker cb = ComponentsBlocker.getMutableInstance(packageName, userHandle)) { for (String component : disabledComponents.keySet()) { cb.addComponent(component, disabledComponents.get(component)); } cb.applyRules(true); if (!cb.isRulesApplied()) throw new Exception("Rules not applied for package " + packageName); } } } } } @Nullable private static RuleType getType(@NonNull String name, @NonNull PackageInfo packageInfo) { for (ActivityInfo activityInfo : packageInfo.activities) if (activityInfo.name.equals(name)) return RuleType.ACTIVITY; for (ProviderInfo providerInfo : packageInfo.providers) if (providerInfo.name.equals(name)) return RuleType.PROVIDER; for (ActivityInfo receiverInfo : packageInfo.receivers) if (receiverInfo.name.equals(name)) return RuleType.RECEIVER; for (ServiceInfo serviceInfo : packageInfo.services) if (serviceInfo.name.equals(name)) return RuleType.SERVICE; return null; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/rules/struct/AppOpRule.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.rules.struct; import androidx.annotation.NonNull; import java.util.Objects; import java.util.StringTokenizer; import io.github.muntashirakon.AppManager.compat.AppOpsManagerCompat; import io.github.muntashirakon.AppManager.rules.RuleType; public class AppOpRule extends RuleEntry { private final int mOp; @AppOpsManagerCompat.Mode private int mMode; public AppOpRule(@NonNull String packageName, int op, @AppOpsManagerCompat.Mode int mode) { super(packageName, String.valueOf(op), RuleType.APP_OP); mOp = op; mMode = mode; } public AppOpRule(@NonNull String packageName, String opInt, @NonNull StringTokenizer tokenizer) throws RuntimeException { super(packageName, opInt, RuleType.APP_OP); mOp = Integer.parseInt(opInt); if (tokenizer.hasMoreElements()) { mMode = Integer.parseInt(tokenizer.nextElement().toString()); } else throw new IllegalArgumentException("Invalid format: mode not found"); } public int getOp() { return mOp; } @AppOpsManagerCompat.Mode public int getMode() { return mMode; } public void setMode(@AppOpsManagerCompat.Mode int mode) { mMode = mode; } @NonNull @Override public String toString() { return "AppOpRule{" + "packageName='" + packageName + '\'' + ", op=" + mOp + ", mode=" + mMode + '}'; } @NonNull @Override public String flattenToString(boolean isExternal) { return addPackageWithTab(isExternal) + mOp + "\t" + type.name() + "\t" + mMode; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof AppOpRule)) return false; if (!super.equals(o)) return false; AppOpRule appOpRule = (AppOpRule) o; return getOp() == appOpRule.getOp() && getMode() == appOpRule.getMode(); } @Override public int hashCode() { return Objects.hash(super.hashCode(), getOp(), getMode()); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/rules/struct/BatteryOptimizationRule.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.rules.struct; import androidx.annotation.NonNull; import java.util.Objects; import java.util.StringTokenizer; import io.github.muntashirakon.AppManager.rules.RuleType; public class BatteryOptimizationRule extends RuleEntry { private boolean mEnabled; public BatteryOptimizationRule(@NonNull String packageName, boolean enabled) { super(packageName, STUB, RuleType.BATTERY_OPT); mEnabled = enabled; } public BatteryOptimizationRule(@NonNull String packageName, @NonNull StringTokenizer tokenizer) { super(packageName, STUB, RuleType.BATTERY_OPT); if (tokenizer.hasMoreElements()) { mEnabled = Boolean.parseBoolean(tokenizer.nextElement().toString()); } else throw new IllegalArgumentException("Invalid format: enabled not found"); } public boolean isEnabled() { return mEnabled; } public void setEnabled(boolean enabled) { mEnabled = enabled; } @NonNull @Override public String toString() { return "BatteryOptimizationRule{" + "packageName='" + packageName + '\'' + ", enabled=" + mEnabled + '}'; } @NonNull @Override public String flattenToString(boolean isExternal) { return addPackageWithTab(isExternal) + name + "\t" + type.name() + "\t" + mEnabled; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof BatteryOptimizationRule)) return false; if (!super.equals(o)) return false; BatteryOptimizationRule that = (BatteryOptimizationRule) o; return isEnabled() == that.isEnabled(); } @Override public int hashCode() { return Objects.hash(super.hashCode(), isEnabled()); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/rules/struct/ComponentRule.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.rules.struct; import android.content.ComponentName; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringDef; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Objects; import java.util.StringTokenizer; import io.github.muntashirakon.AppManager.rules.RuleType; public class ComponentRule extends RuleEntry { @StringDef({ COMPONENT_BLOCKED_IFW_DISABLE, COMPONENT_BLOCKED_IFW, COMPONENT_DISABLED, COMPONENT_ENABLED, COMPONENT_TO_BE_BLOCKED_IFW_DISABLE, COMPONENT_TO_BE_BLOCKED_IFW, COMPONENT_TO_BE_DISABLED, COMPONENT_TO_BE_ENABLED, COMPONENT_TO_BE_DEFAULTED }) @Retention(RetentionPolicy.SOURCE) public @interface ComponentStatus { } // One would want to use flags but couldn't in order to preserve compatibility /** * Component has been blocked with both IFW and PM. */ public static final String COMPONENT_BLOCKED_IFW_DISABLE = "true"; // To preserve compatibility /** * Component has been blocked with IFW. */ public static final String COMPONENT_BLOCKED_IFW = "ifw_true"; /** * Component has been disabled. */ public static final String COMPONENT_DISABLED = "dis_true"; /** * Component has been enabled. */ public static final String COMPONENT_ENABLED = "en_true"; /** * Component will be blocked with both IFW and PM. */ public static final String COMPONENT_TO_BE_BLOCKED_IFW_DISABLE = "false"; // To preserve compatibility /** * Component will be blocked with IFW. */ public static final String COMPONENT_TO_BE_BLOCKED_IFW = "ifw_false"; /** * Component will be disabled. */ public static final String COMPONENT_TO_BE_DISABLED = "dis_false"; /** * Component will be enabled. */ public static final String COMPONENT_TO_BE_ENABLED = "en_false"; /** * Component will be set to the default state, removed from IFW rules if exists and cleared from DB. */ public static final String COMPONENT_TO_BE_DEFAULTED = "unblocked"; @NonNull @ComponentStatus private final String mComponentStatus; @Nullable @ComponentStatus private String mLastComponentStatus; public ComponentRule(@NonNull String packageName, @NonNull String name, RuleType componentType, @NonNull @ComponentStatus String componentStatus) { super(packageName, name, componentType); mComponentStatus = fixComponentStatus(componentStatus); } public ComponentRule(@NonNull String packageName, @NonNull String name, RuleType componentType, @NonNull StringTokenizer tokenizer) throws IllegalArgumentException { super(packageName, name, componentType); if (tokenizer.hasMoreElements()) { mComponentStatus = fixComponentStatus(tokenizer.nextElement().toString()); } else throw new IllegalArgumentException("Invalid format: componentStatus not found"); } public ComponentName getComponentName() { return new ComponentName(packageName, name); } @NonNull @ComponentStatus public String getComponentStatus() { return mComponentStatus; } public void setLastComponentStatus(@Nullable String lastComponentStatus) { mLastComponentStatus = lastComponentStatus; } public boolean applyDefaultState() { if (mLastComponentStatus == null) { return false; } if (mComponentStatus.equals(COMPONENT_TO_BE_BLOCKED_IFW)) { return mLastComponentStatus.equals(COMPONENT_DISABLED) || mLastComponentStatus.equals(COMPONENT_BLOCKED_IFW_DISABLE); } return false; } public boolean toBeRemoved() { return mComponentStatus.equals(COMPONENT_TO_BE_DEFAULTED); } public boolean isBlocked() { return mComponentStatus.equals(COMPONENT_BLOCKED_IFW_DISABLE) || mComponentStatus.equals(COMPONENT_BLOCKED_IFW) || mComponentStatus.equals(COMPONENT_DISABLED); } public boolean isIfw() { return mComponentStatus.equals(COMPONENT_TO_BE_BLOCKED_IFW) || mComponentStatus.equals(COMPONENT_TO_BE_BLOCKED_IFW_DISABLE) || mComponentStatus.equals(COMPONENT_BLOCKED_IFW) || mComponentStatus.equals(COMPONENT_BLOCKED_IFW_DISABLE); } public boolean isApplied() { return !(mComponentStatus.equals(COMPONENT_TO_BE_BLOCKED_IFW_DISABLE) || mComponentStatus.equals(COMPONENT_TO_BE_BLOCKED_IFW) || mComponentStatus.equals(COMPONENT_TO_BE_DISABLED) || mComponentStatus.equals(COMPONENT_TO_BE_ENABLED) || mComponentStatus.equals(COMPONENT_TO_BE_DEFAULTED)); } @ComponentStatus public String getCounterpartOfToBe() { switch (mComponentStatus) { case COMPONENT_TO_BE_BLOCKED_IFW_DISABLE: return COMPONENT_BLOCKED_IFW_DISABLE; case COMPONENT_TO_BE_BLOCKED_IFW: return COMPONENT_BLOCKED_IFW; case COMPONENT_TO_BE_DISABLED: return COMPONENT_DISABLED; case COMPONENT_TO_BE_ENABLED: return COMPONENT_ENABLED; default: return mComponentStatus; } } @ComponentStatus public String getToBe() { switch (mComponentStatus) { case COMPONENT_BLOCKED_IFW_DISABLE: return COMPONENT_TO_BE_BLOCKED_IFW_DISABLE; case COMPONENT_BLOCKED_IFW: return COMPONENT_TO_BE_BLOCKED_IFW; case COMPONENT_DISABLED: return COMPONENT_TO_BE_DISABLED; case COMPONENT_ENABLED: return COMPONENT_TO_BE_ENABLED; default: return mComponentStatus; } } private String fixComponentStatus(@ComponentStatus String componentStatus) { if (type != RuleType.PROVIDER) { return componentStatus; } // Providers do not support IFW switch (componentStatus) { case COMPONENT_BLOCKED_IFW_DISABLE: return COMPONENT_DISABLED; case COMPONENT_BLOCKED_IFW: return COMPONENT_ENABLED; case COMPONENT_TO_BE_BLOCKED_IFW: case COMPONENT_TO_BE_BLOCKED_IFW_DISABLE: return COMPONENT_TO_BE_DISABLED; default: return componentStatus; } } @NonNull @Override public String toString() { return "ComponentRule{" + "packageName='" + packageName + '\'' + ", name='" + name + '\'' + ", type=" + type.name() + ", componentStatus='" + mComponentStatus + '\'' + '}'; } @NonNull @Override public String flattenToString(boolean isExternal) { return addPackageWithTab(isExternal) + name + "\t" + type.name() + "\t" + mComponentStatus; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof ComponentRule)) return false; if (!super.equals(o)) return false; ComponentRule rule = (ComponentRule) o; return getComponentStatus().equals(rule.getComponentStatus()); } @Override public int hashCode() { return Objects.hash(super.hashCode(), getComponentStatus()); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/rules/struct/FreezeRule.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.rules.struct; import androidx.annotation.NonNull; import java.util.Objects; import java.util.StringTokenizer; import io.github.muntashirakon.AppManager.rules.RuleType; import io.github.muntashirakon.AppManager.utils.FreezeUtils; public class FreezeRule extends RuleEntry { @FreezeUtils.FreezeMethod private int mFreezeType; public FreezeRule(@NonNull String packageName, @FreezeUtils.FreezeMethod int freezeType) { super(packageName, STUB, RuleType.FREEZE); mFreezeType = freezeType; } public FreezeRule(@NonNull String packageName, @NonNull StringTokenizer tokenizer) { super(packageName, STUB, RuleType.FREEZE); if (tokenizer.hasMoreElements()) { mFreezeType = Integer.parseInt(tokenizer.nextElement().toString()); } else throw new IllegalArgumentException("Invalid format: freeze_type not found"); } public int getFreezeType() { return mFreezeType; } public void setFreezeType(@FreezeUtils.FreezeMethod int freezeType) { mFreezeType = freezeType; } @NonNull @Override public String toString() { return "FreezeRule{" + "mFreezeType=" + mFreezeType + ", packageName='" + packageName + '\'' + '}'; } @NonNull @Override public String flattenToString(boolean isExternal) { return addPackageWithTab(isExternal) + name + "\t" + type.name() + "\t" + mFreezeType; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof FreezeRule)) return false; if (!super.equals(o)) return false; FreezeRule freezeRule = (FreezeRule) o; return getFreezeType() == freezeRule.getFreezeType(); } @Override public int hashCode() { return Objects.hash(super.hashCode(), getFreezeType()); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/rules/struct/MagiskDenyListRule.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.rules.struct; import androidx.annotation.NonNull; import java.util.Objects; import java.util.StringTokenizer; import io.github.muntashirakon.AppManager.magisk.MagiskProcess; import io.github.muntashirakon.AppManager.rules.RuleType; public class MagiskDenyListRule extends RuleEntry { @NonNull private final MagiskProcess mMagiskProcess; public MagiskDenyListRule(@NonNull MagiskProcess magiskProcess) { super(magiskProcess.packageName, magiskProcess.name, RuleType.MAGISK_DENY_LIST); mMagiskProcess = magiskProcess; } public MagiskDenyListRule(@NonNull String packageName, @NonNull String processName, @NonNull StringTokenizer tokenizer) throws IllegalArgumentException { super(packageName, processName, RuleType.MAGISK_DENY_LIST); mMagiskProcess = new MagiskProcess(packageName, name); mMagiskProcess.setAppZygote(name.endsWith("_zygote")); if (tokenizer.hasMoreElements()) { mMagiskProcess.setEnabled(Boolean.parseBoolean(tokenizer.nextElement().toString())); } else throw new IllegalArgumentException("Invalid format: isHidden not found"); if (tokenizer.hasMoreElements()) { mMagiskProcess.setIsolatedProcess(Boolean.parseBoolean(tokenizer.nextElement().toString())); } } public String getProcessName() { return name; } @NonNull public MagiskProcess getMagiskProcess() { return mMagiskProcess; } @NonNull @Override public String toString() { return "MagiskDenyListRule{" + "packageName='" + packageName + '\'' + "processName='" + name + '\'' + ", isDenied=" + mMagiskProcess.isEnabled() + ", isIsolated=" + mMagiskProcess.isIsolatedProcess() + ", isAppZygote=" + mMagiskProcess.isAppZygote() + '}'; } @NonNull @Override public String flattenToString(boolean isExternal) { return addPackageWithTab(isExternal) + name + "\t" + type.name() + "\t" + mMagiskProcess.isEnabled() + "\t" + mMagiskProcess.isIsolatedProcess(); } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof MagiskDenyListRule)) return false; if (!super.equals(o)) return false; MagiskDenyListRule that = (MagiskDenyListRule) o; return mMagiskProcess.isEnabled() == that.mMagiskProcess.isEnabled() && mMagiskProcess.isIsolatedProcess() == that.mMagiskProcess.isIsolatedProcess(); } @Override public int hashCode() { return Objects.hash(super.hashCode(), mMagiskProcess.isEnabled(), mMagiskProcess.isIsolatedProcess()); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/rules/struct/MagiskHideRule.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.rules.struct; import androidx.annotation.NonNull; import java.util.Objects; import java.util.StringTokenizer; import io.github.muntashirakon.AppManager.magisk.MagiskProcess; import io.github.muntashirakon.AppManager.rules.RuleType; public class MagiskHideRule extends RuleEntry { @NonNull private final MagiskProcess mMagiskProcess; public MagiskHideRule(@NonNull MagiskProcess magiskProcess) { super(magiskProcess.packageName, magiskProcess.name, RuleType.MAGISK_HIDE); mMagiskProcess = magiskProcess; } public MagiskHideRule(@NonNull String packageName, @NonNull String processName, @NonNull StringTokenizer tokenizer) throws IllegalArgumentException { super(packageName, processName.equals(STUB) ? packageName : processName, RuleType.MAGISK_HIDE); mMagiskProcess = new MagiskProcess(packageName, name); // name cannot be STUB mMagiskProcess.setAppZygote(name.endsWith("_zygote")); if (tokenizer.hasMoreElements()) { mMagiskProcess.setEnabled(Boolean.parseBoolean(tokenizer.nextElement().toString())); } else throw new IllegalArgumentException("Invalid format: isHidden not found"); if (tokenizer.hasMoreElements()) { mMagiskProcess.setIsolatedProcess(Boolean.parseBoolean(tokenizer.nextElement().toString())); } } @NonNull public MagiskProcess getMagiskProcess() { return mMagiskProcess; } @NonNull @Override public String toString() { return "MagiskHideRule{" + "packageName='" + packageName + '\'' + "processName='" + name + '\'' + ", isHidden=" + mMagiskProcess.isEnabled() + ", isIsolated=" + mMagiskProcess.isIsolatedProcess() + ", isAppZygote=" + mMagiskProcess.isAppZygote() + '}'; } @NonNull @Override public String flattenToString(boolean isExternal) { return addPackageWithTab(isExternal) + name + "\t" + type.name() + "\t" + mMagiskProcess.isEnabled() + "\t" + mMagiskProcess.isIsolatedProcess(); } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof MagiskHideRule)) return false; if (!super.equals(o)) return false; MagiskHideRule that = (MagiskHideRule) o; return mMagiskProcess.isEnabled() == that.mMagiskProcess.isEnabled() && mMagiskProcess.isIsolatedProcess() == that.mMagiskProcess.isIsolatedProcess(); } @Override public int hashCode() { return Objects.hash(super.hashCode(), mMagiskProcess.isEnabled(), mMagiskProcess.isIsolatedProcess()); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/rules/struct/NetPolicyRule.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.rules.struct; import androidx.annotation.NonNull; import java.util.Objects; import java.util.StringTokenizer; import io.github.muntashirakon.AppManager.compat.NetworkPolicyManagerCompat; import io.github.muntashirakon.AppManager.rules.RuleType; public class NetPolicyRule extends RuleEntry { @NetworkPolicyManagerCompat.NetPolicy private int mNetPolicies; public NetPolicyRule(@NonNull String packageName, @NetworkPolicyManagerCompat.NetPolicy int netPolicies) { super(packageName, STUB, RuleType.NET_POLICY); mNetPolicies = netPolicies; } public NetPolicyRule(@NonNull String packageName, @NonNull StringTokenizer tokenizer) { super(packageName, STUB, RuleType.NET_POLICY); if (tokenizer.hasMoreElements()) { mNetPolicies = Integer.parseInt(tokenizer.nextElement().toString()); } else throw new IllegalArgumentException("Invalid format: netPolicies not found"); } @NetworkPolicyManagerCompat.NetPolicy public int getPolicies() { return mNetPolicies; } public void setPolicies(@NetworkPolicyManagerCompat.NetPolicy int netPolicies) { mNetPolicies = netPolicies; } @NonNull @Override public String toString() { return "NetPolicyRule{" + "packageName='" + packageName + '\'' + ", netPolicies=" + mNetPolicies + '}'; } @NonNull @Override public String flattenToString(boolean isExternal) { return addPackageWithTab(isExternal) + name + "\t" + type.name() + "\t" + mNetPolicies; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof NetPolicyRule)) return false; if (!super.equals(o)) return false; NetPolicyRule that = (NetPolicyRule) o; return mNetPolicies == that.mNetPolicies; } @Override public int hashCode() { return Objects.hash(super.hashCode(), mNetPolicies); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/rules/struct/NotificationListenerRule.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.rules.struct; import androidx.annotation.NonNull; import java.util.Objects; import java.util.StringTokenizer; import io.github.muntashirakon.AppManager.rules.RuleType; public class NotificationListenerRule extends RuleEntry { private boolean mIsGranted; public NotificationListenerRule(@NonNull String packageName, String name, boolean isGranted) { super(packageName, name, RuleType.NOTIFICATION); this.mIsGranted = isGranted; } public NotificationListenerRule(@NonNull String packageName, String name, @NonNull StringTokenizer tokenizer) { super(packageName, name, RuleType.NOTIFICATION); if (tokenizer.hasMoreElements()) { mIsGranted = Boolean.parseBoolean(tokenizer.nextElement().toString()); } else throw new IllegalArgumentException("Invalid format: isGranted not found"); } public boolean isGranted() { return mIsGranted; } public void setGranted(boolean granted) { mIsGranted = granted; } @NonNull @Override public String toString() { return "NotificationListenerRule{" + "packageName='" + packageName + '\'' + ", name='" + name + '\'' + ", isGranted=" + mIsGranted + '}'; } @NonNull @Override public String flattenToString(boolean isExternal) { return addPackageWithTab(isExternal) + name + "\t" + type.name() + "\t" + mIsGranted; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof NotificationListenerRule)) return false; if (!super.equals(o)) return false; NotificationListenerRule that = (NotificationListenerRule) o; return isGranted() == that.isGranted(); } @Override public int hashCode() { return Objects.hash(super.hashCode(), isGranted()); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/rules/struct/PermissionRule.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.rules.struct; import android.content.pm.PermissionInfo; import android.os.RemoteException; import androidx.annotation.NonNull; import androidx.core.content.pm.PermissionInfoCompat; import java.util.Objects; import java.util.StringTokenizer; import io.github.muntashirakon.AppManager.compat.AppOpsManagerCompat; import io.github.muntashirakon.AppManager.compat.PermissionCompat; import io.github.muntashirakon.AppManager.permission.DevelopmentPermission; import io.github.muntashirakon.AppManager.permission.PermUtils; import io.github.muntashirakon.AppManager.permission.Permission; import io.github.muntashirakon.AppManager.permission.ReadOnlyPermission; import io.github.muntashirakon.AppManager.permission.RuntimePermission; import io.github.muntashirakon.AppManager.rules.RuleType; public class PermissionRule extends RuleEntry { private final int mAppOp; private boolean mIsGranted; @PermissionCompat.PermissionFlags private int mFlags; public PermissionRule(@NonNull String packageName, @NonNull String permName, boolean isGranted, @PermissionCompat.PermissionFlags int flags) { super(packageName, permName, RuleType.PERMISSION); mIsGranted = isGranted; mFlags = flags; mAppOp = AppOpsManagerCompat.permissionToOpCode(name); } public PermissionRule(@NonNull String packageName, @NonNull String permName, @NonNull StringTokenizer tokenizer) throws IllegalArgumentException { super(packageName, permName, RuleType.PERMISSION); if (tokenizer.hasMoreElements()) { mIsGranted = Boolean.parseBoolean(tokenizer.nextElement().toString()); } else throw new IllegalArgumentException("Invalid format: isGranted not found"); if (tokenizer.hasMoreElements()) { mFlags = Integer.parseInt(tokenizer.nextElement().toString()); } else { // Don't throw exception in order to provide backward compatibility mFlags = 0; } mAppOp = AppOpsManagerCompat.permissionToOpCode(name); } public boolean isGranted() { return mIsGranted; } public void setGranted(boolean granted) { mIsGranted = granted; } @PermissionCompat.PermissionFlags public int getFlags() { return mFlags; } public void setFlags(@PermissionCompat.PermissionFlags int flags) { mFlags = flags; } public int getAppOp() { return mAppOp; } public Permission getPermission(boolean appOpAllowed) { PermissionInfo permissionInfo = null; try { permissionInfo = PermissionCompat.getPermissionInfo(name, packageName, 0); } catch (RemoteException ignore) { } if (permissionInfo == null) { permissionInfo = new PermissionInfo(); permissionInfo.name = name; } int protection = PermissionInfoCompat.getProtection(permissionInfo); int protectionFlags = PermissionInfoCompat.getProtectionFlags(permissionInfo); if (protection == PermissionInfo.PROTECTION_DANGEROUS && PermUtils.systemSupportsRuntimePermissions()) { return new RuntimePermission(name, mIsGranted, mAppOp, appOpAllowed, mFlags); } else if ((protectionFlags & PermissionInfo.PROTECTION_FLAG_DEVELOPMENT) != 0) { return new DevelopmentPermission(name, mIsGranted, mAppOp, appOpAllowed, mFlags); } else { return new ReadOnlyPermission(name, mIsGranted, mAppOp, appOpAllowed, mFlags); } } @NonNull @Override public String toString() { return "PermissionRule{" + "packageName='" + packageName + '\'' + ", name='" + name + '\'' + ", isGranted=" + mIsGranted + ", flags=" + mFlags + '}'; } @NonNull @Override public String flattenToString(boolean isExternal) { return addPackageWithTab(isExternal) + name + "\t" + type.name() + "\t" + mIsGranted + "\t" + mFlags; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof PermissionRule)) return false; if (!super.equals(o)) return false; PermissionRule that = (PermissionRule) o; return isGranted() == that.isGranted() && getFlags() == that.getFlags(); } @Override public int hashCode() { return Objects.hash(super.hashCode(), isGranted(), getFlags()); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/rules/struct/RuleEntry.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.rules.struct; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.util.Objects; import java.util.StringTokenizer; import io.github.muntashirakon.AppManager.rules.RuleType; public abstract class RuleEntry { public static final String STUB = "STUB"; /** * Name of the entry, unique for {@link RuleType#ACTIVITY}, {@link RuleType#PROVIDER}, {@link RuleType#RECEIVER}, * {@link RuleType#SERVICE}, {@link RuleType#PERMISSION}, {@link RuleType#MAGISK_DENY_LIST} but not others. * In other cases, they can be {@link #STUB}. */ @NonNull public final String name; /** * The package name this rule belong to. */ @NonNull public final String packageName; /** * Type of the entry. */ @NonNull public final RuleType type; public RuleEntry(@NonNull String packageName, @NonNull String name, @NonNull RuleType type) { this.packageName = packageName; this.name = name; this.type = type; } @NonNull @Override public String toString() { return "Entry{" + "name='" + name + '\'' + ", type=" + type + '}'; } @NonNull public abstract String flattenToString(boolean isExternal); protected String addPackageWithTab(boolean isExternal) { return (isExternal ? packageName + "\t" : ""); } @NonNull public static RuleEntry unflattenFromString(@Nullable String packageName, @NonNull String ruleLine, boolean isExternal) throws IllegalArgumentException { StringTokenizer tokenizer = new StringTokenizer(ruleLine, "\t"); if (isExternal) { // External rules, the first part is the package name if (tokenizer.hasMoreElements()) { // Match package name String newPackageName = tokenizer.nextElement().toString(); if (packageName == null) packageName = newPackageName; if (!packageName.equals(newPackageName)) { throw new IllegalArgumentException("Invalid format: package names do not match."); } } else throw new IllegalArgumentException("Invalid format: packageName not found for external rule."); } if (packageName == null) { // packageName can't be empty throw new IllegalArgumentException("Package name cannot be empty."); } String name; RuleType type; if (tokenizer.hasMoreElements()) { name = tokenizer.nextElement().toString(); } else throw new IllegalArgumentException("Invalid format: name not found"); if (tokenizer.hasMoreElements()) { try { type = RuleType.valueOf(tokenizer.nextElement().toString()); } catch (Exception e) { throw new IllegalArgumentException("Invalid format: Invalid type"); } } else throw new IllegalArgumentException("Invalid format: entryType not found"); return getRuleEntry(packageName, name, type, tokenizer); } @NonNull private static RuleEntry getRuleEntry(@NonNull String packageName, @NonNull String name, @NonNull RuleType type, @NonNull StringTokenizer tokenizer) throws IllegalArgumentException { switch (type) { case ACTIVITY: case PROVIDER: case RECEIVER: case SERVICE: return new ComponentRule(packageName, name, type, tokenizer); case APP_OP: return new AppOpRule(packageName, name, tokenizer); case PERMISSION: return new PermissionRule(packageName, name, tokenizer); case MAGISK_HIDE: return new MagiskHideRule(packageName, name, tokenizer); case MAGISK_DENY_LIST: return new MagiskDenyListRule(packageName, name, tokenizer); case BATTERY_OPT: return new BatteryOptimizationRule(packageName, tokenizer); case NET_POLICY: return new NetPolicyRule(packageName, tokenizer); case NOTIFICATION: return new NotificationListenerRule(packageName, name, tokenizer); case URI_GRANT: return new UriGrantRule(packageName, tokenizer); case SSAID: return new SsaidRule(packageName, tokenizer); case FREEZE: return new FreezeRule(packageName, tokenizer); default: throw new IllegalArgumentException("Invalid type=" + type.name()); } } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof RuleEntry)) return false; RuleEntry ruleEntry = (RuleEntry) o; return name.equals(ruleEntry.name) && packageName.equals(ruleEntry.packageName) && type == ruleEntry.type; } @Override public int hashCode() { return Objects.hash(name, packageName, type); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/rules/struct/SsaidRule.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.rules.struct; import androidx.annotation.NonNull; import java.util.Objects; import java.util.StringTokenizer; import io.github.muntashirakon.AppManager.rules.RuleType; public class SsaidRule extends RuleEntry { @NonNull private String mSsaid; public SsaidRule(@NonNull String packageName, @NonNull String ssaid) { super(packageName, STUB, RuleType.SSAID); mSsaid = ssaid; } public SsaidRule(@NonNull String packageName, @NonNull StringTokenizer tokenizer) { super(packageName, STUB, RuleType.SSAID); if (tokenizer.hasMoreElements()) { mSsaid = tokenizer.nextElement().toString(); } else throw new IllegalArgumentException("Invalid format: ssaid not found"); } @NonNull public String getSsaid() { return mSsaid; } public void setSsaid(@NonNull String ssaid) { mSsaid = ssaid; } @NonNull @Override public String toString() { return "SsaidRule{" + "packageName='" + packageName + '\'' + ", ssaid='" + mSsaid + '\'' + '}'; } @NonNull @Override public String flattenToString(boolean isExternal) { return addPackageWithTab(isExternal) + name + "\t" + type.name() + "\t" + mSsaid; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof SsaidRule)) return false; if (!super.equals(o)) return false; SsaidRule ssaidRule = (SsaidRule) o; return getSsaid().equals(ssaidRule.getSsaid()); } @Override public int hashCode() { return Objects.hash(super.hashCode(), getSsaid()); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/rules/struct/UriGrantRule.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.rules.struct; import androidx.annotation.NonNull; import java.util.Objects; import java.util.StringTokenizer; import io.github.muntashirakon.AppManager.rules.RuleType; import io.github.muntashirakon.AppManager.uri.UriManager; public class UriGrantRule extends RuleEntry { @NonNull private final UriManager.UriGrant mUriGrant; public UriGrantRule(@NonNull String packageName, @NonNull UriManager.UriGrant uriGrant) { super(packageName, STUB, RuleType.URI_GRANT); mUriGrant = uriGrant; } public UriGrantRule(@NonNull String packageName, @NonNull StringTokenizer tokenizer) { super(packageName, STUB, RuleType.URI_GRANT); if (tokenizer.hasMoreElements()) { mUriGrant = UriManager.UriGrant.unflattenFromString(tokenizer.nextElement().toString()); } else throw new IllegalArgumentException("Invalid format: uriGrant not found"); } @NonNull public UriManager.UriGrant getUriGrant() { return mUriGrant; } @NonNull @Override public String toString() { return "UriGrantRule{" + "packageName='" + packageName + '\'' + ", uriGrant=" + mUriGrant + '}'; } @NonNull @Override public String flattenToString(boolean isExternal) { return addPackageWithTab(isExternal) + name + "\t" + type.name() + "\t" + mUriGrant.flattenToString(); } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof UriGrantRule)) return false; if (!super.equals(o)) return false; UriGrantRule that = (UriGrantRule) o; return getUriGrant().equals(that.getUriGrant()); } @Override public int hashCode() { return Objects.hash(super.hashCode(), getUriGrant()); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/runner/NormalShell.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.runner; import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; import com.topjohnwu.superuser.Shell; import java.io.InputStream; import java.util.ArrayList; import java.util.List; class NormalShell extends Runner { private final Shell mShell; public NormalShell(boolean isRoot) { if (isRoot == Shell.getShell().isRoot()) { mShell = Shell.getShell(); return; } int flags = isRoot ? Shell.FLAG_MOUNT_MASTER : Shell.FLAG_NON_ROOT_SHELL; mShell = Shell.Builder.create().setFlags(flags).setTimeout(10).build(); } @Override public boolean isRoot() { return mShell.isRoot(); } @WorkerThread @NonNull @Override protected synchronized Result runCommand() { List stdout = new ArrayList<>(); List stderr = new ArrayList<>(); Shell.Job job = mShell.newJob().add(commands.toArray(new String[0])).to(stdout, stderr); for (InputStream is : inputStreams) { job.add(is); } Shell.Result result = job.exec(); return new Result(stdout, stderr, result.getCode()); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/runner/PrivilegedShell.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.runner; import android.os.RemoteException; import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; import java.io.IOException; import java.io.InputStream; import io.github.muntashirakon.AppManager.IAMService; import io.github.muntashirakon.AppManager.IRemoteShell; import io.github.muntashirakon.AppManager.IShellResult; import io.github.muntashirakon.AppManager.ipc.LocalServices; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.utils.ParcelFileDescriptorUtil; class PrivilegedShell extends Runner { @Override public boolean isRoot() { // ADB shell in App Manager always runs in no-root return false; } @WorkerThread @NonNull @Override protected synchronized Result runCommand() { try { IAMService amService = LocalServices.getAmService(); IRemoteShell shell = amService.getShell(commands.toArray(new String[0])); for (InputStream is : inputStreams) { shell.addInputStream(ParcelFileDescriptorUtil.pipeFrom(is)); } IShellResult result = shell.exec(); return new Result(result.getStdout().getList(), result.getStderr().getList(), result.getExitCode()); } catch (RemoteException | IOException e) { Log.e(PrivilegedShell.class.getSimpleName(), e); return new Result(); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/runner/Runner.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.runner; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; import java.util.List; import io.github.muntashirakon.AppManager.ipc.LocalServices; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.settings.Ops; public abstract class Runner { public static final String TAG = Runner.class.getSimpleName(); public static class Result { private final List mStdout; private final List mStderr; private final int mExitCode; Result(@NonNull List stdout, @NonNull List stderr, int exitCode) { mStdout = stdout; mStderr = stderr; mExitCode = exitCode; // Print stderr if (stderr.size() > 0) { Log.e(TAG, TextUtils.join("\n", stderr)); } } public Result(int exitCode) { this(Collections.emptyList(), Collections.emptyList(), exitCode); } public Result() { this(1); } public boolean isSuccessful() { return mExitCode == 0; } public int getExitCode() { return mExitCode; } @NonNull public List getOutputAsList() { return mStdout; } @NonNull public List getOutputAsList(int firstIndex) { if (firstIndex >= mStdout.size()) { return Collections.emptyList(); } return mStdout.subList(firstIndex, mStdout.size()); } @NonNull public String getOutput() { return TextUtils.join("\n", mStdout); } public List getStderr() { return mStderr; } } private static NormalShell sRootShell; private static PrivilegedShell sPrivilegedShell; private static NormalShell sNoRootShell; @NonNull private static Runner getInstance() { if (Ops.isDirectRoot()) { return getRootInstance(); } else if (LocalServices.alive()) { return getPrivilegedInstance(); } else { return getNoRootInstance(); } } @NonNull static Runner getRootInstance() { if (sRootShell == null) { sRootShell = new NormalShell(true); Log.d(TAG, "RootShell"); } return sRootShell; } @NonNull private static Runner getPrivilegedInstance() { if (sPrivilegedShell == null) { sPrivilegedShell = new PrivilegedShell(); Log.d(TAG, "PrivilegedShell"); } return sPrivilegedShell; } private static Runner getNoRootInstance() { if (sNoRootShell == null) { sNoRootShell = new NormalShell(false); Log.d(TAG, "NoRootShell"); } return sNoRootShell; } @NonNull synchronized public static Result runCommand(@NonNull String command) { return runCommand(getInstance(), command, null); } @NonNull synchronized public static Result runCommand(@NonNull String[] command) { return runCommand(getInstance(), command, null); } @NonNull synchronized public static Result runCommand(@NonNull String command, @Nullable InputStream inputStream) { return runCommand(getInstance(), command, inputStream); } @NonNull synchronized public static Result runCommand(@NonNull String[] command, @Nullable InputStream inputStream) { return runCommand(getInstance(), command, inputStream); } @NonNull synchronized private static Result runCommand(@NonNull Runner runner, @NonNull String command, @Nullable InputStream inputStream) { return runner.run(command, inputStream); } @NonNull synchronized private static Result runCommand(@NonNull Runner runner, @NonNull String[] command, @Nullable InputStream inputStream) { StringBuilder cmd = new StringBuilder(); for (String part : command) { cmd.append(RunnerUtils.escape(part)).append(" "); } return runCommand(runner, cmd.toString(), inputStream); } protected final List commands; protected final List inputStreams; protected Runner() { this.commands = new ArrayList<>(); this.inputStreams = new ArrayList<>(); } public void addCommand(@NonNull String command) { commands.add(command); } public void add(@NonNull InputStream inputStream) { inputStreams.add(inputStream); } public void clear() { commands.clear(); inputStreams.clear(); } public abstract boolean isRoot(); @WorkerThread @NonNull protected abstract Result runCommand(); @NonNull private Result run(@NonNull String command, @Nullable InputStream inputStream) { try { clear(); addCommand(command); if (inputStream != null) { add(inputStream); } return runCommand(); } finally { clear(); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/runner/RunnerUtils.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.runner; import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.topjohnwu.superuser.Shell; import java.io.File; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.security.InvalidParameterException; import java.util.BitSet; import java.util.Collections; import java.util.HashMap; import java.util.Map; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.misc.NoOps; public final class RunnerUtils { public static final String TAG = RunnerUtils.class.getSimpleName(); public static final String CMD_AM = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? "cmd activity" : "am"; public static final String CMD_PM = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? "cmd package" : "pm"; private static final String EMPTY = ""; /** * Translator object for escaping Shell command language. * * @see Shell Command Language */ public static final LookupTranslator ESCAPE_XSI; static { final Map escapeXsiMap = new HashMap<>(); escapeXsiMap.put("|", "\\|"); escapeXsiMap.put("&", "\\&"); escapeXsiMap.put(";", "\\;"); escapeXsiMap.put("<", "\\<"); escapeXsiMap.put(">", "\\>"); escapeXsiMap.put("(", "\\("); escapeXsiMap.put(")", "\\)"); escapeXsiMap.put("$", "\\$"); escapeXsiMap.put("`", "\\`"); escapeXsiMap.put("\\", "\\\\"); escapeXsiMap.put("\"", "\\\""); escapeXsiMap.put("'", "\\'"); escapeXsiMap.put(" ", "\\ "); escapeXsiMap.put("\t", "\\\t"); escapeXsiMap.put("\r\n", EMPTY); escapeXsiMap.put("\n", EMPTY); escapeXsiMap.put("*", "\\*"); escapeXsiMap.put("?", "\\?"); escapeXsiMap.put("[", "\\["); escapeXsiMap.put("#", "\\#"); escapeXsiMap.put("~", "\\~"); escapeXsiMap.put("=", "\\="); escapeXsiMap.put("%", "\\%"); ESCAPE_XSI = new LookupTranslator(Collections.unmodifiableMap(escapeXsiMap)); } /** *

Escapes the characters in a {@code String} using XSI rules.

* *

Beware! In most cases you don't want to escape shell commands but use multi-argument * methods provided by {@link java.lang.ProcessBuilder} or {@link java.lang.Runtime#exec(String[])} * instead.

* *

Example:

*
     * input string: He didn't say, "Stop!"
     * output string: He\ didn\'t\ say,\ \"Stop!\"
     * 
* * @param input String to escape values in, may be null * @return String with escaped values, {@code null} if null string input * @see Shell Command Language */ public static String escape(final String input) { return ESCAPE_XSI.translate(input); } @NoOps public static boolean isRootGiven() { return Boolean.TRUE.equals(isAppGrantedRoot()); } @NoOps public static boolean isRootAvailable() { return !Boolean.FALSE.equals(isAppGrantedRoot()); } /** * @see Shell#isAppGrantedRoot() */ @Nullable @NoOps public static Boolean isAppGrantedRoot() { if (Runner.getRootInstance().isRoot()) { // Root granted return true; } // Check if root is available String pathEnv = System.getenv("PATH"); Log.d(TAG, "PATH=%s", pathEnv); if (pathEnv == null) return false; for (String pathDir : pathEnv.split(":")) { File suFile = new File(pathDir, "su"); Log.d(TAG, "SU(file=%s, exists=%s, executable=%s)", suFile, suFile.exists(), suFile.canExecute()); if (new File(pathDir, "su").canExecute()) { // Root available but App Manager is not granted root return null; } } return false; } /** * An API for translating text. * Its core use is to escape and unescape text. Because escaping and unescaping * is completely contextual, the API does not present two separate signatures. * * @since 1.0 */ public static class LookupTranslator { /** * The mapping to be used in translation. */ private final Map mLookupMap; /** * The first character of each key in the lookupMap. */ private final BitSet mPrefixSet; /** * The length of the shortest key in the lookupMap. */ private final int mShortest; /** * The length of the longest key in the lookupMap. */ private final int mLongest; /** * Define the lookup table to be used in translation *

* Note that, as of Lang 3.1 (the origin of this code), the key to the lookup * table is converted to a java.lang.String. This is because we need the key * to support hashCode and equals(Object), allowing it to be the key for a * HashMap. See LANG-882. * * @param lookupMap Map<CharSequence, CharSequence> table of translator * mappings */ public LookupTranslator(final Map lookupMap) { if (lookupMap == null) { throw new InvalidParameterException("lookupMap cannot be null"); } mLookupMap = new HashMap<>(); mPrefixSet = new BitSet(); int currentShortest = Integer.MAX_VALUE; int currentLongest = 0; for (final Map.Entry pair : lookupMap.entrySet()) { mLookupMap.put(pair.getKey().toString(), pair.getValue().toString()); mPrefixSet.set(pair.getKey().charAt(0)); final int sz = pair.getKey().length(); if (sz < currentShortest) { currentShortest = sz; } if (sz > currentLongest) { currentLongest = sz; } } mShortest = currentShortest; mLongest = currentLongest; } /** * Translate a set of codepoints, represented by an int index into a CharSequence, * into another set of codepoints. The number of codepoints consumed must be returned, * and the only IOExceptions thrown must be from interacting with the Writer so that * the top level API may reliably ignore StringWriter IOExceptions. * * @param input CharSequence that is being translated * @param index int representing the current point of translation * @param out Writer to translate the text to * @return int count of codepoints consumed * @throws IOException if and only if the Writer produces an IOException */ public int translate(@NonNull final CharSequence input, final int index, final Writer out) throws IOException { // check if translation exists for the input at position index if (mPrefixSet.get(input.charAt(index))) { int max = mLongest; if (index + mLongest > input.length()) { max = input.length() - index; } // implement greedy algorithm by trying maximum match first for (int i = max; i >= mShortest; i--) { final CharSequence subSeq = input.subSequence(index, index + i); final String result = mLookupMap.get(subSeq.toString()); if (result != null) { out.write(result); return i; } } } return 0; } /** * Helper for non-Writer usage. * * @param input CharSequence to be translated * @return String output of translation */ public final String translate(final CharSequence input) { if (input == null) { return null; } try { final StringWriter writer = new StringWriter(input.length() * 2); translate(input, writer); return writer.toString(); } catch (final IOException ioe) { // this should never ever happen while writing to a StringWriter throw new RuntimeException(ioe); } } /** * Translate an input onto a Writer. This is intentionally final as its algorithm is * tightly coupled with the abstract method of this class. * * @param input CharSequence that is being translated * @param out Writer to translate the text to * @throws IOException if and only if the Writer produces an IOException */ public final void translate(final CharSequence input, final Writer out) throws IOException { if (input == null) { return; } int pos = 0; final int len = input.length(); while (pos < len) { final int consumed = translate(input, pos, out); if (consumed == 0) { // inlined implementation of Character.toChars(Character.codePointAt(input, pos)) // avoids allocating temp char arrays and duplicate checks final char c1 = input.charAt(pos); out.write(c1); pos++; if (Character.isHighSurrogate(c1) && pos < len) { final char c2 = input.charAt(pos); if (Character.isLowSurrogate(c2)) { out.write(c2); pos++; } } continue; } // contract with translators is that they have to understand codepoints // and they just took care of a surrogate pair for (int pt = 0; pt < consumed; pt++) { pos += Character.charCount(Character.codePointAt(input, pos)); } } } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/runningapps/AppProcessItem.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.runningapps; import android.content.pm.PackageInfo; import android.os.Parcel; import androidx.annotation.NonNull; import androidx.core.os.ParcelCompat; import java.util.Objects; import io.github.muntashirakon.AppManager.ipc.ps.ProcessEntry; public class AppProcessItem extends ProcessItem { @NonNull public final PackageInfo packageInfo; public AppProcessItem(@NonNull ProcessEntry processEntry, @NonNull PackageInfo packageInfo) { super(processEntry); this.packageInfo = packageInfo; } protected AppProcessItem(@NonNull Parcel in) { super(in); packageInfo = Objects.requireNonNull(ParcelCompat.readParcelable(in, PackageInfo.class.getClassLoader(), PackageInfo.class)); } public static final Creator CREATOR = new Creator() { @NonNull @Override public AppProcessItem createFromParcel(Parcel in) { return new AppProcessItem(in); } @NonNull @Override public AppProcessItem[] newArray(int size) { return new AppProcessItem[size]; } }; @Override public void writeToParcel(@NonNull Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeParcelable(packageInfo, flags); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/runningapps/ProcessItem.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.runningapps; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.os.ParcelCompat; import java.util.Objects; import io.github.muntashirakon.AppManager.ipc.ps.ProcessEntry; import io.github.muntashirakon.AppManager.users.Owners; public class ProcessItem implements Parcelable { public final int pid; public final int ppid; public final long rss; public final int uid; public final String user; @Nullable public final String context; public String state; public String state_extra; public String name; @NonNull private final ProcessEntry mProcessEntry; public ProcessItem(@NonNull ProcessEntry processEntry) { mProcessEntry = processEntry; pid = processEntry.pid; ppid = processEntry.ppid; rss = processEntry.residentSetSize; uid = processEntry.users.fsUid; context = processEntry.seLinuxPolicy; user = Owners.getOwnerName(processEntry.users.fsUid); } /** * @see How do I get the total CPU usage of an application from /proc/pid/stat? */ public double getCpuTimeInPercent() { return mProcessEntry.cpuTimeConsumed * 100. / mProcessEntry.elapsedTime; } public long getCpuTimeInMillis() { return mProcessEntry.cpuTimeConsumed * 1000; } public String getCommandlineArgsAsString() { return mProcessEntry.name.replace('\u0000', ' '); } public String[] getCommandlineArgs() { return mProcessEntry.name.split("\u0000"); } public long getMemory() { return mProcessEntry.residentSetSize << 12; } public long getVirtualMemory() { return mProcessEntry.virtualMemorySize; } public long getSharedMemory() { return mProcessEntry.sharedMemory << 12; } public int getPriority() { return mProcessEntry.priority; } public int getThreadCount() { return mProcessEntry.threadCount; } protected ProcessItem(@NonNull Parcel in) { mProcessEntry = Objects.requireNonNull(ParcelCompat.readParcelable(in, ProcessEntry.class.getClassLoader(), ProcessEntry.class)); pid = mProcessEntry.pid; ppid = mProcessEntry.ppid; rss = mProcessEntry.residentSetSize; uid = mProcessEntry.users.fsUid; context = mProcessEntry.seLinuxPolicy; user = in.readString(); state = in.readString(); state_extra = in.readString(); name = in.readString(); } public static final Creator CREATOR = new Creator() { @NonNull @Override public ProcessItem createFromParcel(Parcel in) { return new ProcessItem(in); } @NonNull @Override public ProcessItem[] newArray(int size) { return new ProcessItem[size]; } }; @Override @NonNull public String toString() { return "ProcessItem{" + "pid=" + pid + ", ppid=" + ppid + ", rss=" + rss + ", user='" + user + '\'' + ", uid=" + uid + ", state='" + state + '\'' + ", state_extra='" + state_extra + '\'' + ", name='" + name + '\'' + ", context='" + context + '\'' + '}'; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof ProcessItem)) return false; ProcessItem that = (ProcessItem) o; return pid == that.pid; } @Override public int hashCode() { return Objects.hash(pid); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeParcelable(mProcessEntry, flags); dest.writeString(user); dest.writeString(state); dest.writeString(state_extra); dest.writeString(name); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/runningapps/ProcessParser.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.runningapps; import android.app.ActivityManager; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.annotation.WorkerThread; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Objects; import io.github.muntashirakon.AppManager.compat.ActivityManagerCompat; import io.github.muntashirakon.AppManager.compat.PackageManagerCompat; import io.github.muntashirakon.AppManager.ipc.LocalServices; import io.github.muntashirakon.AppManager.ipc.ps.ProcessEntry; import io.github.muntashirakon.AppManager.ipc.ps.Ps; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.PackageUtils; import io.github.muntashirakon.AppManager.utils.Utils; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; @WorkerThread public final class ProcessParser { private final Context mContext; private final PackageManager mPm; private HashMap mInstalledPackages; private HashMap mInstalledUidList; private final HashMap mRunningAppProcesses = new HashMap<>(50); ProcessParser() { if (Utils.isRoboUnitTest()) { mInstalledPackages = new HashMap<>(); mInstalledUidList = new HashMap<>(); mPm = null; mContext = null; } else { mContext = ContextUtils.getContext(); mPm = mContext.getPackageManager(); getInstalledPackages(); } } @SuppressWarnings("unchecked") @NonNull List parse() { List processItems = new ArrayList<>(); try { List processEntries; if (Paths.get("/proc/1").canRead() && LocalServices.alive()) { processEntries = (List) LocalServices.getAmService().getRunningProcesses().getList(); } else { Ps ps = new Ps(); ps.loadProcesses(); processEntries = ps.getProcesses(); } for (ProcessEntry processEntry : processEntries) { if (processEntry.seLinuxPolicy != null && processEntry.seLinuxPolicy.contains(":kernel:")) { continue; } try { processItems.addAll(parseProcess(processEntry)); } catch (Exception ignore) { } } } catch (Throwable th) { Log.e("ProcessParser", th); } return processItems; } @VisibleForTesting @NonNull HashMap parse(@NonNull Path procDir) { HashMap processItems = new HashMap<>(); Ps ps = new Ps(procDir); ps.loadProcesses(); List processEntries = ps.getProcesses(); for (ProcessEntry processEntry : processEntries) { try { ProcessItem processItem = parseProcess(processEntry).get(0); processItems.put(processItem.pid, processItem); } catch (Exception ignore) { } } return processItems; } @NonNull private List parseProcess(@NonNull ProcessEntry processEntry) { String packageName = getSupposedPackageName(processEntry.name); List processItems = new ArrayList<>(1); if (mRunningAppProcesses.containsKey(processEntry.pid)) { String[] pkgList = Objects.requireNonNull(mRunningAppProcesses.get(processEntry.pid)).pkgList; if (pkgList != null && pkgList.length > 0) { for (String pkgName : pkgList) { @NonNull PackageInfo packageInfo = Objects.requireNonNull(mInstalledPackages.get(pkgName)); ProcessItem processItem = new AppProcessItem(processEntry, packageInfo); processItem.name = mPm.getApplicationLabel(packageInfo.applicationInfo) + getProcessNameFilteringPackageName(processEntry.name, packageInfo.packageName); processItems.add(processItem); } } else { ProcessItem processItem = new ProcessItem(processEntry); processItem.name = getProcessName(processEntry.name); processItems.add(processItem); } } else if (mInstalledPackages.containsKey(packageName)) { @NonNull PackageInfo packageInfo = Objects.requireNonNull(mInstalledPackages.get(packageName)); ProcessItem processItem = new AppProcessItem(processEntry, packageInfo); processItem.name = mPm.getApplicationLabel(packageInfo.applicationInfo) + getProcessNameFilteringPackageName(processEntry.name, packageInfo.packageName); processItems.add(processItem); } else if (mInstalledUidList.containsKey(processEntry.users.fsUid)) { @NonNull PackageInfo packageInfo = Objects.requireNonNull(mInstalledUidList.get(processEntry.users.fsUid)); ProcessItem processItem = new AppProcessItem(processEntry, packageInfo); processItem.name = mPm.getApplicationLabel(packageInfo.applicationInfo) + getProcessNameFilteringPackageName(processEntry.name, packageInfo.packageName); processItems.add(processItem); } else { ProcessItem processItem = new ProcessItem(processEntry); processItem.name = getProcessName(processEntry.name); processItems.add(processItem); } for (ProcessItem processItem : processItems) { if (mContext == null) { processItem.state = processEntry.processState; processItem.state_extra = processEntry.processStatePlus; } else { processItem.state = mContext.getString(Utils.getProcessStateName(processEntry.processState)); processItem.state_extra = mContext.getString(Utils.getProcessStateExtraName( processEntry.processStatePlus)); } } return processItems; } private void getInstalledPackages() { List packageInfoList = PackageUtils.getAllPackages(PackageManagerCompat.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES); mInstalledPackages = new HashMap<>(packageInfoList.size()); for (PackageInfo info : packageInfoList) { mInstalledPackages.put(info.packageName, info); } mInstalledUidList = new HashMap<>(packageInfoList.size()); List duplicateUids = new ArrayList<>(); for (PackageInfo info : packageInfoList) { int uid = info.applicationInfo.uid; if (mInstalledUidList.containsKey(uid)) { // A shared user ID (other way to check user ID will not work since we're only interested in // duplicate values) duplicateUids.add(uid); } else mInstalledUidList.put(uid, info); } // Remove duplicate UIDs as they might create collisions for (int uid : duplicateUids) mInstalledUidList.remove(uid); List runningAppProcesses = ActivityManagerCompat.getRunningAppProcesses(); for (ActivityManager.RunningAppProcessInfo info : runningAppProcesses) { mRunningAppProcesses.put(info.pid, info); } } @NonNull public static String getSupposedPackageName(@NonNull String processName) { if (!processName.contains(":")) { return processName; } int colonIdx = processName.indexOf(':'); return processName.substring(0, colonIdx); } @NonNull public static String getProcessName(@NonNull String processName) { processName = processName.split("\u0000")[0]; if (!processName.startsWith("/")) return processName; int slashIndex = processName.lastIndexOf('/'); return processName.substring(slashIndex + 1); } @NonNull private static String getProcessNameFilteringPackageName(@NonNull String processName, @NonNull String packageName) { if (processName.equals(packageName)) { return ""; } processName = getProcessName(processName); int colonIdx = processName.indexOf(':'); return colonIdx < 0 ? (":" + processName) : processName.substring(colonIdx); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/runningapps/RunningAppDetails.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.runningapps; import android.content.Intent; import android.content.pm.PackageInfo; import android.os.Bundle; import android.os.UserHandleHidden; import android.text.TextUtils; import android.text.format.Formatter; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.LinearLayoutCompat; import androidx.core.os.BundleCompat; import com.google.android.material.button.MaterialButton; import java.util.Locale; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.details.AppDetailsActivity; import io.github.muntashirakon.AppManager.self.imagecache.ImageLoader; import io.github.muntashirakon.AppManager.utils.DateUtils; import io.github.muntashirakon.dialog.CapsuleBottomSheetDialogFragment; public class RunningAppDetails extends CapsuleBottomSheetDialogFragment { public static final String TAG = RunningAppDetails.class.getSimpleName(); public static final String ARG_PS_ITEM = "ps_item"; @NonNull public static RunningAppDetails getInstance(@NonNull ProcessItem processItem) { Bundle args = new Bundle(); args.putParcelable(ARG_PS_ITEM, processItem); RunningAppDetails fragment = new RunningAppDetails(); fragment.setArguments(args); return fragment; } @NonNull @Override public View initRootView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.dialog_running_app_details, container, false); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); ProcessItem processItem = BundleCompat.getParcelable(requireArguments(), ARG_PS_ITEM, ProcessItem.class); if (processItem == null) { dismiss(); return; } LinearLayoutCompat appContainer = view.findViewById(R.id.app_container); ImageView appIcon = view.findViewById(R.id.icon); MaterialButton openAppInfoButton = view.findViewById(R.id.info); TextView appLabel = view.findViewById(R.id.name); TextView packageName = view.findViewById(R.id.package_name); TextView processName = view.findViewById(R.id.process_name); TextView pid = view.findViewById(R.id.pid); TextView ppid = view.findViewById(R.id.ppid); TextView rss = view.findViewById(R.id.rss); TextView vsz = view.findViewById(R.id.vsz); TextView cpuPercent = view.findViewById(R.id.cpu_percent); TextView cpuTime = view.findViewById(R.id.cpu_time); TextView priority = view.findViewById(R.id.priority); TextView threads = view.findViewById(R.id.threads); TextView user = view.findViewById(R.id.user); TextView state = view.findViewById(R.id.state); TextView seLinuxContext = view.findViewById(R.id.selinux_context); TextView cliArgs = view.findViewById(R.id.cli_args); processName.setText(processItem.name); pid.setText(String.format(Locale.getDefault(), "%d", processItem.pid)); ppid.setText(String.format(Locale.getDefault(), "%d", processItem.ppid)); rss.setText(Formatter.formatFileSize(requireContext(), processItem.getMemory())); vsz.setText(Formatter.formatFileSize(requireContext(), processItem.getVirtualMemory())); cpuPercent.setText(String.format(Locale.getDefault(), "%.2f", processItem.getCpuTimeInPercent())); cpuTime.setText(DateUtils.getFormattedDuration(requireContext(), processItem.getCpuTimeInMillis(), false, true)); priority.setText(String.format(Locale.getDefault(), "%d", processItem.getPriority())); threads.setText(String.format(Locale.getDefault(), "%d", processItem.getThreadCount())); user.setText(String.format(Locale.getDefault(), "%s (%d)", processItem.user, processItem.uid)); CharSequence stateInfo; if (TextUtils.isEmpty(processItem.state_extra)) { stateInfo = processItem.state; } else { stateInfo = processItem.state + " (" + processItem.state_extra + ")"; } state.setText(stateInfo); seLinuxContext.setText(processItem.context); cliArgs.setText(processItem.getCommandlineArgsAsString()); if (processItem instanceof AppProcessItem) { PackageInfo packageInfo = ((AppProcessItem) processItem).packageInfo; appContainer.setVisibility(View.VISIBLE); ImageLoader.getInstance().displayImage(packageInfo.packageName, packageInfo.applicationInfo, appIcon); appLabel.setText(packageInfo.applicationInfo.loadLabel(requireContext().getPackageManager())); packageName.setText(packageInfo.packageName); openAppInfoButton.setOnClickListener(v -> { Intent appDetailsIntent = AppDetailsActivity.getIntent(requireContext(), packageInfo.packageName, UserHandleHidden.getUserId(processItem.uid)); startActivity(appDetailsIntent); dismiss(); }); } else { appContainer.setVisibility(View.GONE); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/runningapps/RunningAppsActivity.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.runningapps; import android.content.Intent; import android.os.Bundle; import android.os.Process; import android.text.TextUtils; import android.view.Menu; import android.view.MenuItem; import android.view.View; import androidx.activity.OnBackPressedCallback; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.core.content.ContextCompat; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.progressindicator.LinearProgressIndicator; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; import java.util.Timer; import java.util.TimerTask; import io.github.muntashirakon.AppManager.BaseActivity; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.batchops.BatchOpsManager; import io.github.muntashirakon.AppManager.batchops.BatchOpsService; import io.github.muntashirakon.AppManager.batchops.BatchQueueItem; import io.github.muntashirakon.AppManager.compat.ManifestCompat; import io.github.muntashirakon.AppManager.logcat.LogViewerActivity; import io.github.muntashirakon.AppManager.logcat.struct.SearchCriteria; import io.github.muntashirakon.AppManager.misc.AdvancedSearchView; import io.github.muntashirakon.AppManager.scanner.vt.VtFileReport; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.settings.FeatureController; import io.github.muntashirakon.AppManager.settings.Ops; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.multiselection.MultiSelectionActionsView; import io.github.muntashirakon.widget.MultiSelectionView; public class RunningAppsActivity extends BaseActivity implements MultiSelectionView.OnSelectionChangeListener, MultiSelectionActionsView.OnItemSelectedListener, AdvancedSearchView.OnQueryTextListener, MultiSelectionView.OnSelectionModeChangeListener { @IntDef(value = { SORT_BY_PID, SORT_BY_PROCESS_NAME, SORT_BY_APPS_FIRST, SORT_BY_MEMORY_USAGE, SORT_BY_CPU_TIME, }) @Retention(RetentionPolicy.SOURCE) public @interface SortOrder { } public static final int SORT_BY_PID = 0; public static final int SORT_BY_PROCESS_NAME = 1; public static final int SORT_BY_APPS_FIRST = 2; public static final int SORT_BY_MEMORY_USAGE = 3; public static final int SORT_BY_CPU_TIME = 4; @IntDef(value = { FILTER_NONE, FILTER_APPS, FILTER_USER_APPS }, flag = true) @Retention(RetentionPolicy.SOURCE) public @interface Filter { } public static final int FILTER_NONE = 0; public static final int FILTER_APPS = 1; public static final int FILTER_USER_APPS = 1 << 1; private static final int[] SORT_ORDER_IDS = new int[]{ R.id.action_sort_by_pid, R.id.action_sort_by_process_name, R.id.action_sort_by_apps_first, R.id.action_sort_by_memory_usage, R.id.action_sort_by_cpu_time, }; @Nullable RunningAppsViewModel model; private boolean mEnableKillForSystem = false; @Nullable private RunningAppsAdapter mAdapter; @Nullable private LinearProgressIndicator mProgressIndicator; @Nullable private MultiSelectionView mMultiSelectionView; @Nullable private Menu mSelectionMenu; private Timer mTimer; private final OnBackPressedCallback mOnBackPressedCallback = new OnBackPressedCallback(false) { @Override public void handleOnBackPressed() { if (mAdapter != null && mMultiSelectionView != null && mAdapter.isInSelectionMode()) { mMultiSelectionView.cancel(); return; } setEnabled(false); getOnBackPressedDispatcher().onBackPressed(); } }; @Override protected void onAuthenticated(Bundle savedInstanceState) { setContentView(R.layout.activity_running_apps); setSupportActionBar(findViewById(R.id.toolbar)); getOnBackPressedDispatcher().addCallback(this, mOnBackPressedCallback); ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayShowCustomEnabled(true); UIUtils.setupAdvancedSearchView(actionBar, this); } model = new ViewModelProvider(this).get(RunningAppsViewModel.class); mProgressIndicator = findViewById(R.id.progress_linear); mProgressIndicator.setVisibilityAfterHide(View.GONE); RecyclerView recyclerView = findViewById(R.id.scrollView); recyclerView.setLayoutManager(UIUtils.getGridLayoutAt450Dp(this)); mAdapter = new RunningAppsAdapter(this); mAdapter.setHasStableIds(false); recyclerView.setAdapter(mAdapter); // Recycler view is focused by default recyclerView.requestFocus(); mMultiSelectionView = findViewById(R.id.selection_view); mMultiSelectionView.setOnItemSelectedListener(this); mMultiSelectionView.setOnSelectionModeChangeListener(this); mMultiSelectionView.setOnSelectionChangeListener(this); mMultiSelectionView.setAdapter(mAdapter); mMultiSelectionView.updateCounter(true); mSelectionMenu = mMultiSelectionView.getMenu(); mSelectionMenu.findItem(R.id.action_scan_vt).setVisible(false); mEnableKillForSystem = Prefs.RunningApps.enableKillForSystemApps(); // Set observers model.observeKillProcess().observe(this, processInfo -> { if (processInfo.second /* is success */) { refresh(); } else { UIUtils.displayLongToast(R.string.failed_to_stop, processInfo.first.name /* process name */); } }); model.observeKillSelectedProcess().observe(this, processInfoList -> { if (!processInfoList.isEmpty()) { List processNames = new ArrayList() {{ for (ProcessItem processItem : processInfoList) { add(processItem.name); } }}; UIUtils.displayLongToast(R.string.failed_to_stop, TextUtils.join(", ", processNames)); } refresh(); }); model.observeForceStop().observe(this, applicationInfoBooleanPair -> { if (applicationInfoBooleanPair.second /* is success */) { refresh(); } else { UIUtils.displayLongToast(R.string.failed_to_stop, applicationInfoBooleanPair.first .loadLabel(getPackageManager())); } }); model.observePreventBackgroundRun().observe(this, applicationInfoBooleanPair -> { if (applicationInfoBooleanPair.second /* is success */) { refresh(); } else { UIUtils.displayLongToast(R.string.failed_to_prevent_background_run, applicationInfoBooleanPair.first .loadLabel(getPackageManager())); } }); model.observeProcessDetails().observe(this, processItem -> { RunningAppDetails fragment = RunningAppDetails.getInstance(processItem); fragment.show(getSupportFragmentManager(), RunningAppDetails.TAG); }); model.getVtFileUpload().observe(this, processItemVtFilePermalinkPair -> { ProcessItem processItem = processItemVtFilePermalinkPair.first; String permalink = processItemVtFilePermalinkPair.second; if (permalink == null) { // Started uploading UIUtils.displayShortToast(R.string.vt_uploading); if (Prefs.VirusTotal.promptBeforeUpload()) { new MaterialAlertDialogBuilder(this) .setTitle(R.string.scan_in_vt) .setMessage(R.string.vt_confirm_uploading_file) .setCancelable(false) .setPositiveButton(R.string.vt_confirm_upload_and_scan, (dialog, which) -> model.enableUploading()) .setNegativeButton(R.string.no, (dialog, which) -> model.disableUploading()) .show(); } else model.enableUploading(); } else { UIUtils.displayShortToast(R.string.vt_queued); } // TODO: 7/1/22 Use a separate fragment }); model.getVtFileReport().observe(this, processItemVtFileReportPair -> { ProcessItem processItem = processItemVtFileReportPair.first; VtFileReport vtFileReport = processItemVtFileReportPair.second; if (vtFileReport == null) { UIUtils.displayShortToast(R.string.vt_failed); } else { UIUtils.displayLongToast(getString(R.string.vt_success, vtFileReport.getPositives(), vtFileReport.getTotal())); } // TODO: 7/1/22 Use a separate fragment }); model.getProcessLiveData().observe(this, processList -> { if (mProgressIndicator != null) { mProgressIndicator.hide(); } if (mAdapter != null) { mAdapter.setDefaultList(processList); } }); model.getDeviceMemoryInfo().observe(this, deviceMemoryInfo -> { if (mAdapter != null) { mAdapter.setDeviceMemoryInfo(deviceMemoryInfo); } }); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.activity_running_apps_actions, menu); return super.onCreateOptionsMenu(menu); } @Override public boolean onPrepareOptionsMenu(@NonNull Menu menu) { if (model == null) return super.onPrepareOptionsMenu(menu); menu.findItem(SORT_ORDER_IDS[model.getSortOrder()]).setChecked(true); int filter = model.getFilter(); if ((filter & FILTER_APPS) != 0) { menu.findItem(R.id.action_filter_apps).setChecked(true); } if ((filter & FILTER_USER_APPS) != 0) { menu.findItem(R.id.action_filter_user_apps).setChecked(true); } return super.onPrepareOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int id = item.getItemId(); if (id == android.R.id.home) { finish(); return true; } if (model == null) return true; if (id == R.id.action_toggle_kill) { mEnableKillForSystem = !mEnableKillForSystem; Prefs.RunningApps.setEnableKillForSystemApps(mEnableKillForSystem); refresh(); } else if (id == R.id.action_sort_by_pid) { model.setSortOrder(SORT_BY_PID); item.setChecked(true); } else if (id == R.id.action_sort_by_process_name) { model.setSortOrder(SORT_BY_PROCESS_NAME); item.setChecked(true); } else if (id == R.id.action_sort_by_apps_first) { model.setSortOrder(SORT_BY_APPS_FIRST); item.setChecked(true); } else if (id == R.id.action_sort_by_memory_usage) { model.setSortOrder(SORT_BY_MEMORY_USAGE); item.setChecked(true); } else if (id == R.id.action_sort_by_cpu_time) { model.setSortOrder(SORT_BY_CPU_TIME); item.setChecked(true); // Filter } else if (id == R.id.action_filter_apps) { if (!item.isChecked()) model.addFilter(FILTER_APPS); else model.removeFilter(FILTER_APPS); item.setChecked(!item.isChecked()); } else if (id == R.id.action_filter_user_apps) { if (!item.isChecked()) model.addFilter(FILTER_USER_APPS); else model.removeFilter(FILTER_USER_APPS); item.setChecked(!item.isChecked()); } else return super.onOptionsItemSelected(item); return true; } @Override protected void onResume() { super.onResume(); mTimer = new Timer("running_apps"); mTimer.schedule(new TimerTask() { @Override public void run() { ThreadUtils.postOnMainThread(() -> { if (model != null) { model.loadProcesses(); model.loadMemoryInfo(); } }); } }, 0, 10_000); } @Override protected void onPause() { mTimer.cancel(); mTimer.purge(); super.onPause(); } @Override public boolean onQueryTextSubmit(String query, int type) { return false; } @Override public boolean onQueryTextChange(String newText, int type) { if (model != null) { model.setQuery(newText, type); return true; } return false; } @Override public void onSelectionModeEnabled() { mOnBackPressedCallback.setEnabled(true); } @Override public void onSelectionModeDisabled() { mOnBackPressedCallback.setEnabled(false); } @Override public boolean onNavigationItemSelected(@NonNull MenuItem item) { if (model == null || mAdapter == null) return true; ArrayList selectedItems = mAdapter.getSelectedItems(); int id = item.getItemId(); if (id == R.id.action_kill) { model.killSelectedProcesses(); } else if (id == R.id.action_force_stop) { handleBatchOpWithWarning(BatchOpsManager.OP_FORCE_STOP); } else if (id == R.id.action_disable_background) { handleBatchOpWithWarning(BatchOpsManager.OP_DISABLE_BACKGROUND); } else if (id == R.id.action_view_logs) { // Should be a singleton list if (selectedItems.size() == 1) { ProcessItem processItem = selectedItems.get(0); Intent logViewerIntent = new Intent(getApplicationContext(), LogViewerActivity.class) .putExtra(LogViewerActivity.EXTRA_FILTER, SearchCriteria.PID_KEYWORD + processItem.pid) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(logViewerIntent); } } return false; } @Override public boolean onSelectionChange(int selectionCount) { if (mSelectionMenu == null || mAdapter == null) { return false; } ArrayList selectedItems = mAdapter.getSelectedItems(); MenuItem kill = mSelectionMenu.findItem(R.id.action_kill); MenuItem forceStop = mSelectionMenu.findItem(R.id.action_force_stop); MenuItem preventBackground = mSelectionMenu.findItem(R.id.action_disable_background); MenuItem viewLogs = mSelectionMenu.findItem(R.id.action_view_logs); viewLogs.setEnabled(FeatureController.isLogViewerEnabled() && selectedItems.size() == 1); int appsCount = 0; for (Object item : selectedItems) { if (item instanceof AppProcessItem) { ++appsCount; } else break; } forceStop.setEnabled(appsCount != 0 && appsCount == selectedItems.size()); forceStop.setVisible(SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.FORCE_STOP_PACKAGES)); preventBackground.setEnabled(appsCount != 0 && appsCount == selectedItems.size()); boolean killEnabled = Ops.isWorkingUidRoot(); if (killEnabled && !mEnableKillForSystem) { for (ProcessItem item : selectedItems) { if (item.uid < Process.FIRST_APPLICATION_UID) { killEnabled = false; break; } } } kill.setEnabled(!selectedItems.isEmpty() && killEnabled); return true; } private void handleBatchOp(@BatchOpsManager.OpType int op) { if (model == null) return; if (mProgressIndicator != null) { mProgressIndicator.show(); } BatchOpsManager.Result input = new BatchOpsManager.Result(model.getSelectedPackagesWithUsers()); BatchQueueItem item = BatchQueueItem.getBatchOpQueue(op, input.getFailedPackages(), input.getAssociatedUsers(), null); Intent intent = BatchOpsService.getServiceIntent(this, item); ContextCompat.startForegroundService(this, intent); } private void handleBatchOpWithWarning(@BatchOpsManager.OpType int op) { new MaterialAlertDialogBuilder(this) .setTitle(R.string.are_you_sure) .setMessage(R.string.this_action_cannot_be_undone) .setPositiveButton(R.string.yes, (dialog, which) -> handleBatchOp(op)) .setNegativeButton(R.string.no, null) .show(); } void refresh() { if (mProgressIndicator == null || model == null) return; mProgressIndicator.show(); model.loadProcesses(); model.loadMemoryInfo(); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/runningapps/RunningAppsAdapter.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.runningapps; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.graphics.Color; import android.os.Process; import android.text.Spannable; import android.text.TextUtils; import android.text.format.Formatter; import android.text.style.ForegroundColorSpan; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.AttrRes; import androidx.annotation.NonNull; import androidx.appcompat.widget.LinearLayoutCompat; import androidx.appcompat.widget.PopupMenu; import com.google.android.material.button.MaterialButton; import com.google.android.material.card.MaterialCardView; import com.google.android.material.color.MaterialColors; import java.util.ArrayList; import java.util.List; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.compat.ManifestCompat; import io.github.muntashirakon.AppManager.logcat.LogViewerActivity; import io.github.muntashirakon.AppManager.logcat.struct.SearchCriteria; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.self.imagecache.ImageLoader; import io.github.muntashirakon.AppManager.settings.FeatureController; import io.github.muntashirakon.AppManager.settings.Ops; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.utils.LangUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.AppManager.utils.appearance.ColorCodes; import io.github.muntashirakon.io.Paths; import io.github.muntashirakon.proc.ProcMemoryInfo; import io.github.muntashirakon.util.AccessibilityUtils; import io.github.muntashirakon.util.AdapterUtils; import io.github.muntashirakon.widget.MultiSelectionView; public class RunningAppsAdapter extends MultiSelectionView.Adapter { private static final int VIEW_TYPE_MEMORY_INFO = 1; private static final int VIEW_TYPE_PROCESS_INFO = 2; private final RunningAppsActivity mActivity; private final RunningAppsViewModel mModel; private final int mQueryStringHighlightColor; private final Object mLock = new Object(); @NonNull private final List mProcessItems = new ArrayList<>(); private ProcMemoryInfo mProcMemoryInfo; RunningAppsAdapter(@NonNull RunningAppsActivity activity) { super(); mActivity = activity; mModel = activity.model; mQueryStringHighlightColor = ColorCodes.getQueryStringHighlightColor(activity); } void setDefaultList(@NonNull List processItems) { synchronized (mLock) { AdapterUtils.notifyDataSetChanged(this, 1, mProcessItems, processItems); } notifySelectionChange(); } public void setDeviceMemoryInfo(ProcMemoryInfo procMemoryInfo) { mProcMemoryInfo = procMemoryInfo; notifyItemChanged(0, AdapterUtils.STUB); } @Override public int getItemViewType(int position) { if (position == 0) return VIEW_TYPE_MEMORY_INFO; return VIEW_TYPE_PROCESS_INFO; } @NonNull @Override public MultiSelectionView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { if (viewType == VIEW_TYPE_MEMORY_INFO) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.header_running_apps_memory_info, parent, false); return new HeaderViewHolder(view); } View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_running_app, parent, false); return new BodyViewHolder(view); } @Override public void onBindViewHolder(@NonNull MultiSelectionView.ViewHolder holder, int position) { if (position == 0) { onBindViewHolder((HeaderViewHolder) holder); } else { onBindViewHolder((BodyViewHolder) holder, position); super.onBindViewHolder(holder, position); } } private void onBindViewHolder(@NonNull HeaderViewHolder holder) { if (mProcMemoryInfo == null) { return; } Context context = holder.itemView.getContext(); StringBuilder contentDescription = new StringBuilder(); // Memory long appMemory = mProcMemoryInfo.getApplicationMemory(); long cachedMemory = mProcMemoryInfo.getCachedMemory(); long buffers = mProcMemoryInfo.getBuffers(); long freeMemory = mProcMemoryInfo.getFreeMemory(); double total = appMemory + cachedMemory + buffers + freeMemory; boolean totalIsNonZero = total > 0; String fUsedMemory = Formatter.formatFileSize(context, mProcMemoryInfo.getUsedMemory()); String fTotalMemory = Formatter.formatFileSize(context, mProcMemoryInfo.getTotalMemory()); String fAvailableMemory = Formatter.formatFileSize(context, mProcMemoryInfo.getAvailableMemory()); String fAppMemory = Formatter.formatShortFileSize(context, appMemory); String fCachedMemory = Formatter.formatShortFileSize(context, cachedMemory); String fBuffers = Formatter.formatShortFileSize(context, buffers); String fFreeMemory = Formatter.formatShortFileSize(context, freeMemory); AdapterUtils.setVisible(holder.mMemoryInfoChart, totalIsNonZero); AdapterUtils.setVisible(holder.mMemoryShortInfoView, totalIsNonZero); AdapterUtils.setVisible(holder.mMemoryInfoView, totalIsNonZero); if (totalIsNonZero) { holder.mMemoryInfoChart.post(() -> { int width = holder.mMemoryInfoChart.getWidth(); setLayoutWidth(holder.mMemoryInfoChartChildren[0], (int) (width * appMemory / total)); setLayoutWidth(holder.mMemoryInfoChartChildren[1], (int) (width * cachedMemory / total)); setLayoutWidth(holder.mMemoryInfoChartChildren[2], (int) (width * buffers / total)); }); // Update content description for memory (free memory is not important for accessibility) contentDescription.append(context.getString(R.string.memory_usage_accessibility_description, fUsedMemory, fTotalMemory, fAvailableMemory, fAppMemory, fCachedMemory, fBuffers)); } else { contentDescription.append(context.getString(R.string.memory_usage_unavailable)); } holder.mMemoryShortInfoView.setText(UIUtils.getStyledKeyValue(context, R.string.memory, fUsedMemory + "/" + fTotalMemory + " (" + context.getString(R.string.available_memory, fAvailableMemory) + ")")); // Set color info Spannable memInfo = UIUtils.charSequenceToSpannable(context.getString(R.string.memory_chart_info, fAppMemory, fCachedMemory, fBuffers, fFreeMemory)); setColors(holder.itemView, memInfo, new int[]{com.google.android.material.R.attr.colorOnSurface, androidx.appcompat.R.attr.colorPrimary, com.google.android.material.R.attr.colorTertiary, com.google.android.material.R.attr.colorSurfaceVariant}); holder.mMemoryInfoView.setText(memInfo); // Swap long usedSwap = mProcMemoryInfo.getUsedSwap(); long totalSwap = mProcMemoryInfo.getTotalSwap(); String fUsedSwap = Formatter.formatFileSize(context, usedSwap); String fTotalSwap = Formatter.formatFileSize(context, totalSwap); boolean totalSwapIsNonZero = totalSwap > 0; AdapterUtils.setVisible(holder.mSwapInfoChart, totalSwapIsNonZero); AdapterUtils.setVisible(holder.mSwapShortInfoView, totalSwapIsNonZero); AdapterUtils.setVisible(holder.mSwapInfoView, totalSwapIsNonZero); if (totalSwapIsNonZero) { holder.mSwapInfoChart.post(() -> { int width = holder.mSwapInfoChart.getWidth(); setLayoutWidth(holder.mSwapInfoChartChildren[0], (int) (width * usedSwap / totalSwap)); }); // Update content description for swap contentDescription.append("\n\n"); contentDescription.append(context.getString(R.string.swap_usage_accessibility_description, fTotalSwap, fUsedSwap)); } holder.mSwapShortInfoView.setText(UIUtils.getStyledKeyValue(context, R.string.swap, fUsedSwap + "/" + fTotalSwap)); // Set color and size info Spannable swapInfo = UIUtils.charSequenceToSpannable(context.getString(R.string.swap_chart_info, Formatter .formatShortFileSize(context, usedSwap), Formatter.formatShortFileSize(context, totalSwap - usedSwap))); setColors(holder.itemView, swapInfo, new int[]{com.google.android.material.R.attr.colorOnSurface, com.google.android.material.R.attr.colorSurfaceVariant}); holder.mSwapInfoView.setText(swapInfo); holder.itemView.setContentDescription(contentDescription); } private void onBindViewHolder(@NonNull BodyViewHolder holder, int position) { ProcessItem processItem; synchronized (mLock) { processItem = mProcessItems.get(position); } ApplicationInfo applicationInfo; if (processItem instanceof AppProcessItem) { applicationInfo = ((AppProcessItem) processItem).packageInfo.applicationInfo; } else applicationInfo = null; String processName = processItem.name; // Load icon holder.icon.setTag(processName); ImageLoader.getInstance().displayImage(processName, applicationInfo, holder.icon); // Set process name holder.processName.setText(UIUtils.getHighlightedText(processName, mModel.getQuery(), mQueryStringHighlightColor)); // Set package name AdapterUtils.setVisible(holder.packageName, applicationInfo != null); if (applicationInfo != null) { holder.packageName.setText(UIUtils.getHighlightedText(applicationInfo.packageName, mModel.getQuery(), mQueryStringHighlightColor)); } // Set process IDs holder.processIds.setText(mActivity.getString(R.string.pid_and_ppid, processItem.pid, processItem.ppid)); // Set memory usage holder.memoryUsage.setText(mActivity.getString(R.string.memory_virtual_memory, Formatter.formatFileSize(mActivity, processItem.getMemory()), Formatter.formatFileSize(mActivity, processItem.getVirtualMemory()))); // Set user info String userInfo = mActivity.getString(R.string.user_and_uid, processItem.user, processItem.uid); String stateInfo; if (TextUtils.isEmpty(processItem.state_extra)) { stateInfo = mActivity.getString(R.string.process_state, processItem.state); } else { stateInfo = mActivity.getString(R.string.process_state_with_extra, processItem.state, processItem.state_extra); } holder.userAndStateInfo.setText(String.format("%s, %s", userInfo, stateInfo)); holder.selinuxContext.setText(String.format("SELinux%s %s", LangUtils.getSeparatorString(), processItem.context)); // Set more holder.more.setOnClickListener(v -> { PopupMenu popupMenu = new PopupMenu(mActivity, holder.more); popupMenu.setForceShowIcon(true); popupMenu.inflate(R.menu.activity_running_apps_popup_actions); Menu menu = popupMenu.getMenu(); // Set kill MenuItem killItem = menu.findItem(R.id.action_kill); if ((processItem.uid >= Process.FIRST_APPLICATION_UID || Prefs.RunningApps.enableKillForSystemApps()) && Ops.isWorkingUidRoot()) { killItem.setVisible(true).setOnMenuItemClickListener(item -> { mModel.killProcess(processItem); return true; }); } else killItem.setVisible(false); // Set view logs MenuItem viewLogsItem = menu.findItem(R.id.action_view_logs); if (FeatureController.isLogViewerEnabled()) { viewLogsItem.setVisible(true).setOnMenuItemClickListener(item -> { Intent logViewerIntent = new Intent(mActivity.getApplicationContext(), LogViewerActivity.class) .putExtra(LogViewerActivity.EXTRA_FILTER, SearchCriteria.PID_KEYWORD + processItem.pid) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); mActivity.startActivity(logViewerIntent); return true; }); } else viewLogsItem.setVisible(false); // Scan using VT MenuItem scanVtIem = menu.findItem(R.id.action_scan_vt); String firstCliArg = processItem.getCommandlineArgs()[0]; if (mModel.isVirusTotalAvailable() && (applicationInfo != null || Paths.get(firstCliArg).canRead())) { // TODO: 7/1/22 Check other arguments for files, too? scanVtIem.setVisible(true).setOnMenuItemClickListener(item -> { mModel.scanWithVt(processItem); return true; }); } else scanVtIem.setVisible(false); // Set force-stop MenuItem forceStopItem = menu.findItem(R.id.action_force_stop); if (applicationInfo != null) { forceStopItem.setOnMenuItemClickListener(item -> { mModel.forceStop(applicationInfo); return true; }) .setEnabled(SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.FORCE_STOP_PACKAGES)); } else forceStopItem.setEnabled(false); MenuItem bgItem = menu.findItem(R.id.action_disable_background); if (applicationInfo != null) { forceStopItem.setOnMenuItemClickListener(item -> { mModel.forceStop(applicationInfo); return true; }) .setVisible(SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.FORCE_STOP_PACKAGES)); if (mModel.canRunInBackground(applicationInfo)) { bgItem.setVisible(true).setOnMenuItemClickListener(item -> { mModel.preventBackgroundRun(applicationInfo); return true; }); } else bgItem.setVisible(false); } else { forceStopItem.setVisible(false); bgItem.setVisible(false); } // Display popup menu popupMenu.show(); }); // Set selections holder.icon.setOnClickListener(v -> { toggleSelection(position); AccessibilityUtils.requestAccessibilityFocus(holder.itemView); }); holder.itemView.setOnLongClickListener(v -> { ProcessItem lastSelectedItem = mModel.getLastSelectedItem(); int lastSelectedItemPosition = lastSelectedItem == null ? -1 : mProcessItems.indexOf(lastSelectedItem); if (lastSelectedItemPosition >= 0) { // Select from last selection to this selection selectRange(lastSelectedItemPosition + 1, position); } else { toggleSelection(position); AccessibilityUtils.requestAccessibilityFocus(holder.itemView); } return true; }); // Open process details holder.itemView.setOnClickListener(v -> { if (isInSelectionMode()) { toggleSelection(position); AccessibilityUtils.requestAccessibilityFocus(holder.itemView); } else { mModel.requestDisplayProcessDetails(processItem); } }); holder.itemView.setStrokeColor(Color.TRANSPARENT); } @Override public long getItemId(int position) { if (position == 0) { return mProcMemoryInfo != null ? mProcMemoryInfo.hashCode() : View.NO_ID; } synchronized (mLock) { return mProcessItems.get(position).hashCode(); } } @Override protected boolean select(int position) { if (position == 0) { return false; } synchronized (mLock) { mModel.select(mProcessItems.get(position)); return true; } } @Override protected boolean deselect(int position) { if (position == 0) { return false; } synchronized (mLock) { mModel.deselect(mProcessItems.get(position)); return true; } } @Override protected boolean isSelected(int position) { if (position == 0) { return false; } synchronized (mLock) { return mModel.isSelected(mProcessItems.get(position)); } } @Override protected boolean isSelectable(int position) { return position > 0; } @Override protected void cancelSelection() { super.cancelSelection(); mModel.clearSelections(); } @NonNull public ArrayList getSelectedItems() { return mModel.getSelections(); } @Override protected int getSelectedItemCount() { return mModel.getSelectionCount(); } @Override protected int getTotalItemCount() { return mModel.getTotalCount(); } @Override public int getItemCount() { synchronized (mLock) { return mProcessItems.size(); } } private static void setColors(@NonNull View v, @NonNull Spannable text, @NonNull @AttrRes int[] colors) { int idx = 0; for (int color : colors) { idx = text.toString().indexOf('●', idx); if (idx == -1) break; text.setSpan(new ForegroundColorSpan(MaterialColors.getColor(v, color)), idx, idx + 1, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); ++idx; } } private static void setLayoutWidth(@NonNull View view, int width) { ViewGroup.LayoutParams lp = view.getLayoutParams(); lp.width = width; view.setLayoutParams(lp); } static class HeaderViewHolder extends MultiSelectionView.ViewHolder { private final TextView mMemoryShortInfoView; private final TextView mMemoryInfoView; private final View[] mMemoryInfoChartChildren; private final LinearLayoutCompat mMemoryInfoChart; private final TextView mSwapShortInfoView; private final TextView mSwapInfoView; private final View[] mSwapInfoChartChildren; private final LinearLayoutCompat mSwapInfoChart; public HeaderViewHolder(@NonNull View itemView) { super(itemView); mMemoryShortInfoView = itemView.findViewById(R.id.memory_usage); mMemoryInfoView = itemView.findViewById(R.id.memory_usage_info); mMemoryInfoChart = itemView.findViewById(R.id.memory_usage_chart); int childCount = mMemoryInfoChart.getChildCount(); mMemoryInfoChartChildren = new View[childCount]; for (int i = 0; i < childCount; ++i) { mMemoryInfoChartChildren[i] = mMemoryInfoChart.getChildAt(i); } mSwapShortInfoView = itemView.findViewById(R.id.swap_usage); mSwapInfoView = itemView.findViewById(R.id.swap_usage_info); mSwapInfoChart = itemView.findViewById(R.id.swap_usage_chart); childCount = mSwapInfoChart.getChildCount(); mSwapInfoChartChildren = new View[childCount]; for (int i = 0; i < childCount; ++i) { mSwapInfoChartChildren[i] = mSwapInfoChart.getChildAt(i); } } } static class BodyViewHolder extends MultiSelectionView.ViewHolder { MaterialCardView itemView; ImageView icon; MaterialButton more; TextView processName; TextView packageName; TextView processIds; TextView memoryUsage; TextView userAndStateInfo; TextView selinuxContext; public BodyViewHolder(@NonNull View itemView) { super(itemView); this.itemView = (MaterialCardView) itemView; icon = itemView.findViewById(R.id.icon); more = itemView.findViewById(R.id.more); processName = itemView.findViewById(R.id.process_name); packageName = itemView.findViewById(R.id.package_name); processIds = itemView.findViewById(R.id.process_ids); memoryUsage = itemView.findViewById(R.id.memory_usage); userAndStateInfo = itemView.findViewById(R.id.user_state_info); selinuxContext = itemView.findViewById(R.id.selinux_context); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/runningapps/RunningAppsViewModel.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.runningapps; import android.app.AppOpsManager; import android.app.Application; import android.content.pm.ApplicationInfo; import android.os.Build; import android.os.RemoteException; import android.os.UserHandleHidden; import android.text.TextUtils; import androidx.annotation.AnyThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import androidx.core.util.Pair; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import io.github.muntashirakon.AppManager.compat.AppOpsManagerCompat; import io.github.muntashirakon.AppManager.compat.PackageManagerCompat; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.misc.AdvancedSearchView; import io.github.muntashirakon.AppManager.runner.Runner; import io.github.muntashirakon.AppManager.scanner.vt.VirusTotal; import io.github.muntashirakon.AppManager.scanner.vt.VtFileReport; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.types.UserPackagePair; import io.github.muntashirakon.AppManager.utils.DigestUtils; import io.github.muntashirakon.AppManager.utils.MultithreadedExecutor; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; import io.github.muntashirakon.proc.ProcFs; import io.github.muntashirakon.proc.ProcMemoryInfo; public class RunningAppsViewModel extends AndroidViewModel { @RunningAppsActivity.SortOrder private int mSortOrder; @RunningAppsActivity.Filter private int mFilter; private final MultithreadedExecutor mExecutor = MultithreadedExecutor.getNewInstance(); @Nullable private final VirusTotal mVt; public RunningAppsViewModel(@NonNull Application application) { super(application); mSortOrder = Prefs.RunningApps.getSortOrder(); mFilter = Prefs.RunningApps.getFilters(); mVt = VirusTotal.getInstance(); } @Override protected void onCleared() { mExecutor.shutdownNow(); super.onCleared(); } public boolean isVirusTotalAvailable() { return mVt != null; } // Null = Uploading, NonNull = Queued private final MutableLiveData> mVtFileUpload = new MutableLiveData<>(); // Null = Failed, NonNull = Result generated private final MutableLiveData> mVtFileReport = new MutableLiveData<>(); public MutableLiveData> getVtFileReport() { return mVtFileReport; } public MutableLiveData> getVtFileUpload() { return mVtFileUpload; } @AnyThread public void scanWithVt(@NonNull ProcessItem processItem) { String file; if (processItem instanceof AppProcessItem) { file = ((AppProcessItem) processItem).packageInfo.applicationInfo.publicSourceDir; } else file = processItem.getCommandlineArgs()[0]; if (mVt == null || file == null) { mVtFileReport.postValue(new Pair<>(processItem, null)); return; } mExecutor.submit(() -> { Path proxyFile = Paths.get(file); if (!proxyFile.canRead()) { mVtFileReport.postValue(new Pair<>(processItem, null)); return; } String sha256 = DigestUtils.getHexDigest(DigestUtils.SHA_256, proxyFile); try { mVt.fetchFileReportOrScan(proxyFile, sha256, new VirusTotal.FullScanResponseInterface() { @Override public boolean uploadFile() { mUploadingEnabled = false; mUploadingEnabledWatcher = new CountDownLatch(1); mVtFileUpload.postValue(new Pair<>(processItem, null)); try { mUploadingEnabledWatcher.await(2, TimeUnit.MINUTES); } catch (InterruptedException ignore) { } return mUploadingEnabled; } @Override public void onUploadInitiated() { } @Override public void onUploadCompleted(@NonNull String permalink) { mVtFileUpload.postValue(new Pair<>(processItem, permalink)); } @Override public void onReportReceived(@NonNull VtFileReport report) { mVtFileReport.postValue(new Pair<>(processItem, report)); } }); } catch (IOException e) { e.printStackTrace(); mVtFileReport.postValue(new Pair<>(processItem, null)); } }); } @NonNull private final MutableLiveData> mProcessLiveData = new MutableLiveData<>(); @NonNull public LiveData> getProcessLiveData() { return mProcessLiveData; } @NonNull private final MutableLiveData mProcessItemLiveData = new MutableLiveData<>(); @NonNull public LiveData observeProcessDetails() { return mProcessItemLiveData; } @AnyThread public void requestDisplayProcessDetails(@NonNull ProcessItem processItem) { mProcessItemLiveData.postValue(processItem); } @NonNull private final List mProcessList = new ArrayList<>(); @AnyThread public void loadProcesses() { mExecutor.submit(() -> { synchronized (mProcessList) { try { mProcessList.clear(); mProcessList.addAll(new ProcessParser().parse()); filterAndSort(); } catch (Throwable th) { Log.e("RunningApps", th); } } }); } @NonNull private final MutableLiveData mDeviceMemoryInfo = new MutableLiveData<>(); @NonNull public MutableLiveData getDeviceMemoryInfo() { return mDeviceMemoryInfo; } @AnyThread public void loadMemoryInfo() { mExecutor.submit(() -> mDeviceMemoryInfo.postValue(ProcFs.getInstance().getMemoryInfo())); } private final MutableLiveData> mKillProcessResult = new MutableLiveData<>(); private final MutableLiveData> mKillSelectedProcessesResult = new MutableLiveData<>(); public void killProcess(ProcessItem processItem) { mExecutor.submit(() -> mKillProcessResult.postValue(new Pair<>(processItem, Runner.runCommand( new String[]{"kill", "-9", String.valueOf(processItem.pid)}).isSuccessful()))); } public LiveData> observeKillProcess() { return mKillProcessResult; } public void killSelectedProcesses() { mExecutor.submit(() -> { List failedProcesses = new ArrayList<>(); for (ProcessItem processItem : mSelectedItems) { if (!Runner.runCommand(new String[]{"kill", "-9", String.valueOf(processItem.pid)}).isSuccessful()) { failedProcesses.add(processItem); } } mKillSelectedProcessesResult.postValue(failedProcesses); }); } public LiveData> observeKillSelectedProcess() { return mKillSelectedProcessesResult; } private final MutableLiveData> mForceStopAppResult = new MutableLiveData<>(); public void forceStop(@NonNull ApplicationInfo info) { mExecutor.submit(() -> { try { PackageManagerCompat.forceStopPackage(info.packageName, UserHandleHidden.getUserId(info.uid)); mForceStopAppResult.postValue(new Pair<>(info, true)); } catch (SecurityException e) { e.printStackTrace(); mForceStopAppResult.postValue(new Pair<>(info, false)); } }); } public LiveData> observeForceStop() { return mForceStopAppResult; } private final MutableLiveData> mPreventBackgroundRunResult = new MutableLiveData<>(); public boolean canRunInBackground(@NonNull ApplicationInfo info) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { return true; } try { AppOpsManagerCompat appOpsManager = new AppOpsManagerCompat(); boolean canRun; { int mode = appOpsManager.checkOperation(AppOpsManagerCompat.OP_RUN_IN_BACKGROUND, info.uid, info.packageName); canRun = (mode != AppOpsManager.MODE_IGNORED && mode != AppOpsManager.MODE_ERRORED); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { int mode = appOpsManager.checkOperation(AppOpsManagerCompat.OP_RUN_ANY_IN_BACKGROUND, info.uid, info.packageName); canRun |= (mode != AppOpsManager.MODE_IGNORED && mode != AppOpsManager.MODE_ERRORED); } return canRun; } catch (RemoteException | SecurityException e) { return true; } } public void preventBackgroundRun(@NonNull ApplicationInfo info) { mExecutor.submit(() -> { try { AppOpsManagerCompat appOpsService = new AppOpsManagerCompat(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { appOpsService.setMode(AppOpsManagerCompat.OP_RUN_IN_BACKGROUND, info.uid, info.packageName, AppOpsManager.MODE_IGNORED); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { appOpsService.setMode(AppOpsManagerCompat.OP_RUN_ANY_IN_BACKGROUND, info.uid, info.packageName, AppOpsManager.MODE_IGNORED); } // TODO: 14/2/23 Store it to the rules mPreventBackgroundRunResult.postValue(new Pair<>(info, true)); } catch (RemoteException e) { e.printStackTrace(); mPreventBackgroundRunResult.postValue(new Pair<>(info, false)); } }); } public LiveData> observePreventBackgroundRun() { return mPreventBackgroundRunResult; } public int getTotalCount() { return mProcessList.size(); } private String mQuery; @AdvancedSearchView.SearchType private int mQueryType; public void setQuery(@Nullable String query, int searchType) { if (query == null) { mQuery = null; } else if (searchType == AdvancedSearchView.SEARCH_TYPE_PREFIX) { mQuery = query; } else { mQuery = query.toLowerCase(Locale.ROOT); } mQueryType = searchType; mExecutor.submit(this::filterAndSort); } public String getQuery() { return mQuery; } public void setSortOrder(int sortOrder) { mSortOrder = sortOrder; Prefs.RunningApps.setSortOrder(mSortOrder); mExecutor.submit(this::filterAndSort); } public int getSortOrder() { return mSortOrder; } public void addFilter(int filter) { mFilter |= filter; Prefs.RunningApps.setFilters(mFilter); mExecutor.submit(this::filterAndSort); } public void removeFilter(int filter) { mFilter &= ~filter; Prefs.RunningApps.setFilters(mFilter); mExecutor.submit(this::filterAndSort); } public int getFilter() { return mFilter; } @WorkerThread public void filterAndSort() { List filteredProcessList = new ArrayList<>(); // Apply filters // There are 3 filters with “and” relations: apps > user apps > query boolean filterUserApps = (mFilter & RunningAppsActivity.FILTER_USER_APPS) != 0; // If user apps filter is enabled, disable it since it'll be just an overhead boolean filterApps = !filterUserApps && (mFilter & RunningAppsActivity.FILTER_APPS) != 0; ApplicationInfo info; for (ProcessItem item : mProcessList) { // Filter by apps if (filterApps && !(item instanceof AppProcessItem)) { continue; } // Filter by user apps if (filterUserApps) { if (item instanceof AppProcessItem) { info = ((AppProcessItem) item).packageInfo.applicationInfo; if ((info.flags & ApplicationInfo.FLAG_SYSTEM) != 0) continue; // else it's an user app } else continue; } filteredProcessList.add(item); } // Apply searching if (!TextUtils.isEmpty(mQuery)) { filteredProcessList = AdvancedSearchView.matches(mQuery, filteredProcessList, (AdvancedSearchView.ChoicesGenerator) item -> { if (item instanceof AppProcessItem) { return Arrays.asList(item.name.toLowerCase(Locale.getDefault()), ((AppProcessItem) item).packageInfo.packageName.toLowerCase(Locale.getDefault())); } return Collections.singletonList(item.name.toLowerCase(Locale.getDefault())); }, mQueryType); } // Apply sorts // Sort by pid first //noinspection ComparatorCombinators Collections.sort(filteredProcessList, (o1, o2) -> Integer.compare(o1.pid, o2.pid)); if (mSortOrder != RunningAppsActivity.SORT_BY_PID) { Collections.sort(filteredProcessList, (o1, o2) -> { ProcessItem p1 = Objects.requireNonNull(o1); ProcessItem p2 = Objects.requireNonNull(o2); switch (mSortOrder) { case RunningAppsActivity.SORT_BY_APPS_FIRST: return -Boolean.compare(p1 instanceof AppProcessItem, p2 instanceof AppProcessItem); case RunningAppsActivity.SORT_BY_MEMORY_USAGE: return -Long.compare(p1.rss, p2.rss); case RunningAppsActivity.SORT_BY_PROCESS_NAME: return p1.name.compareToIgnoreCase(p2.name); case RunningAppsActivity.SORT_BY_CPU_TIME: return -Long.compare(p1.getCpuTimeInMillis(), p1.getCpuTimeInMillis()); case RunningAppsActivity.SORT_BY_PID: default: return Integer.compare(p1.pid, p2.pid); } }); } mProcessLiveData.postValue(filteredProcessList); } private final Set mSelectedItems = new LinkedHashSet<>(); @Nullable public ProcessItem getLastSelectedItem() { // Last selected package is the same as the last added package. Iterator it = mSelectedItems.iterator(); ProcessItem lastItem = null; while (it.hasNext()) { lastItem = it.next(); } return lastItem; } public int getSelectionCount() { return mSelectedItems.size(); } public boolean isSelected(@NonNull ProcessItem processItem) { return mSelectedItems.contains(processItem); } public void select(@Nullable ProcessItem processItem) { if (processItem != null) { mSelectedItems.add(processItem); } } public void deselect(@Nullable ProcessItem processItem) { if (processItem != null) { mSelectedItems.remove(processItem); } } public ArrayList getSelections() { return new ArrayList<>(mSelectedItems); } @NonNull public ArrayList getSelectedPackagesWithUsers() { ArrayList userPackagePairs = new ArrayList<>(); for (ProcessItem processItem : mSelectedItems) { if (processItem instanceof AppProcessItem) { ApplicationInfo applicationInfo = ((AppProcessItem) processItem).packageInfo.applicationInfo; userPackagePairs.add(new UserPackagePair(applicationInfo.packageName, UserHandleHidden.getUserId(applicationInfo.uid))); } } return userPackagePairs; } public void clearSelections() { mSelectedItems.clear(); } private boolean mUploadingEnabled; private CountDownLatch mUploadingEnabledWatcher; public void enableUploading() { mUploadingEnabled = true; if (mUploadingEnabledWatcher != null) { mUploadingEnabledWatcher.countDown(); } } public void disableUploading() { mUploadingEnabled = false; if (mUploadingEnabledWatcher != null) { mUploadingEnabledWatcher.countDown(); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/scanner/ClassListingFragment.java ================================================ // SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.scanner; import static io.github.muntashirakon.AppManager.misc.AdvancedSearchView.SEARCH_TYPE_REGEX; import android.app.Activity; import android.content.Intent; import android.graphics.Typeface; import android.os.Bundle; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.Filter; import android.widget.Filterable; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.core.view.MenuProvider; import androidx.fragment.app.Fragment; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.ViewModelProvider; import com.google.android.material.card.MaterialCardView; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Objects; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.editor.CodeEditorActivity; import io.github.muntashirakon.AppManager.misc.AdvancedSearchView; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.AppManager.utils.appearance.ColorCodes; import io.github.muntashirakon.util.AdapterUtils; import io.github.muntashirakon.util.UiUtils; import io.github.muntashirakon.widget.RecyclerView; public class ClassListingFragment extends Fragment implements AdvancedSearchView.OnQueryTextListener, MenuProvider { private TextView mEmptyView; private boolean mTrackerClassesOnly; private ClassListingAdapter mClassListingAdapter; private List mAllClasses; private List mTrackerClasses; private ScannerViewModel mViewModel; private ScannerActivity mActivity; @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_class_lister, container, false); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { mViewModel = new ViewModelProvider(requireActivity()).get(ScannerViewModel.class); mActivity = (ScannerActivity) requireActivity(); mAllClasses = mViewModel.getAllClasses(); mTrackerClasses = mViewModel.getTrackerClasses(); if (mAllClasses == null) { mActivity.getOnBackPressedDispatcher().onBackPressed(); return; } if (mTrackerClasses == null) { mTrackerClasses = Collections.emptyList(); } mTrackerClassesOnly = false; RecyclerView listView = view.findViewById(R.id.list_item); UiUtils.applyWindowInsetsAsPaddingNoTop(listView); mEmptyView = view.findViewById(android.R.id.empty); listView.setEmptyView(mEmptyView); mClassListingAdapter = new ClassListingAdapter(mActivity, mViewModel); listView.setLayoutManager(UIUtils.getGridLayoutAt450Dp(mActivity)); listView.setAdapter(mClassListingAdapter); mActivity.addMenuProvider(this, getViewLifecycleOwner(), Lifecycle.State.RESUMED); showProgress(true); setAdapterList(); } @Override public void onResume() { super.onResume(); if (mClassListingAdapter != null && !TextUtils.isEmpty(mClassListingAdapter.mConstraint)) { mClassListingAdapter.filter(); } } @UiThread private void setAdapterList() { if (!mTrackerClassesOnly) { mClassListingAdapter.setDefaultList(mTrackerClasses); mActivity.setSubtitle(getString(R.string.tracker_classes)); } else { mClassListingAdapter.setDefaultList(mAllClasses); mActivity.setSubtitle(getString(R.string.all_classes)); } showProgress(false); } @Override public boolean onQueryTextChange(String newText, @AdvancedSearchView.SearchType int type) { if (mClassListingAdapter != null) { mClassListingAdapter.filter(newText, type); } return true; } @Override public boolean onQueryTextSubmit(String query, int type) { return false; } @Override public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { inflater.inflate(R.menu.fragment_class_lister_actions, menu); AdvancedSearchView searchView = (AdvancedSearchView) menu.findItem(R.id.action_search).getActionView(); Objects.requireNonNull(searchView).setOnQueryTextListener(this); } @Override public boolean onMenuItemSelected(@NonNull MenuItem item) { int id = item.getItemId(); if (id == R.id.action_toggle_class_listing) { mTrackerClassesOnly = !mTrackerClassesOnly; setAdapterList(); } else return false; return true; } private void showProgress(boolean willShow) { mActivity.showProgress(willShow); mEmptyView.setText(willShow ? R.string.loading : R.string.no_tracker_class); } static class ClassListingAdapter extends RecyclerView.Adapter implements Filterable { private Filter mFilter; private String mConstraint; @AdvancedSearchView.SearchType private int mFilterType = AdvancedSearchView.SEARCH_TYPE_CONTAINS; private List mDefaultList; private final Activity mActivity; private final ScannerViewModel mViewModel; @NonNull private final List mAdapterList = new ArrayList<>(); private final int mCardColor0; private final int mCardColor1; private final int mQueryStringHighlightColor; ClassListingAdapter(@NonNull Activity activity, @NonNull ScannerViewModel viewModel) { mActivity = activity; mViewModel = viewModel; mCardColor0 = ColorCodes.getListItemColor0(activity); mCardColor1 = ColorCodes.getListItemColor1(activity); mQueryStringHighlightColor = ColorCodes.getQueryStringHighlightColor(activity); } @UiThread void setDefaultList(@NonNull List list) { mDefaultList = list; filter(); } void filter() { if (!TextUtils.isEmpty(mConstraint)) { filter(mConstraint, mFilterType); } else { AdapterUtils.notifyDataSetChanged(this, mAdapterList, mDefaultList); } } void filter(String query, @AdvancedSearchView.SearchType int filterType) { mConstraint = query; mFilterType = filterType; getFilter().filter(mConstraint); } @Override public int getItemCount() { synchronized (mAdapterList) { return mAdapterList.size(); } } @Override public long getItemId(int position) { synchronized (mAdapterList) { return mDefaultList.indexOf(mAdapterList.get(position)); } } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(io.github.muntashirakon.ui.R.layout.m3_preference, parent, false); return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { String className; synchronized (mAdapterList) { className = mAdapterList.get(position); } TextView textView = holder.classNameView; textView.setTypeface(Typeface.MONOSPACE); if (mConstraint != null && className.toLowerCase(Locale.ROOT).contains(mConstraint)) { // Highlight searched query textView.setText(UIUtils.getHighlightedText(className, mConstraint, mQueryStringHighlightColor)); } else { textView.setText(className); } holder.itemView.setCardBackgroundColor(position % 2 == 0 ? mCardColor1 : mCardColor0); holder.itemView.setOnClickListener(v -> { try { Intent intent = CodeEditorActivity.getIntent(mActivity, mViewModel.getUriFromClassName(className), null, null, true) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); mActivity.startActivity(intent); } catch (Exception e) { e.printStackTrace(); UIUtils.displayLongToast(e.toString()); } }); } @Override public Filter getFilter() { if (mFilter == null) mFilter = new Filter() { @Override protected FilterResults performFiltering(CharSequence charSequence) { String constraint = mFilterType == SEARCH_TYPE_REGEX ? charSequence.toString() : charSequence.toString().toLowerCase(Locale.ROOT); FilterResults filterResults = new FilterResults(); if (constraint.isEmpty()) { filterResults.count = 0; filterResults.values = null; return filterResults; } List list = AdvancedSearchView.matches( constraint, mDefaultList, (AdvancedSearchView.ChoiceGenerator) object -> mFilterType == SEARCH_TYPE_REGEX ? object : object.toLowerCase(Locale.ROOT), mFilterType); filterResults.count = list.size(); filterResults.values = list; return filterResults; } @Override protected void publishResults(CharSequence charSequence, FilterResults filterResults) { synchronized (mAdapterList) { if (filterResults.values == null) { AdapterUtils.notifyDataSetChanged(ClassListingAdapter.this, mAdapterList, mDefaultList); } else { //noinspection unchecked AdapterUtils.notifyDataSetChanged(ClassListingAdapter.this, mAdapterList, (List) filterResults.values); } } } }; return mFilter; } public static class ViewHolder extends RecyclerView.ViewHolder { final MaterialCardView itemView; final TextView classNameView; public ViewHolder(@NonNull View itemView) { super(itemView); this.itemView = (MaterialCardView) itemView; itemView.findViewById(android.R.id.title).setVisibility(View.GONE); itemView.findViewById(R.id.icon_frame).setVisibility(View.GONE); classNameView = itemView.findViewById(android.R.id.summary); } } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/scanner/LibraryInfoDialog.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.scanner; import android.os.Bundle; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.dialog.BottomSheetAlertDialogFragment; public class LibraryInfoDialog extends BottomSheetAlertDialogFragment { public static final String TAG = LibraryInfoDialog.class.getSimpleName(); @NonNull public static LibraryInfoDialog getInstance(@NonNull CharSequence subtitle, @NonNull CharSequence message) { LibraryInfoDialog dialog = new LibraryInfoDialog(); dialog.setArguments(getArgs(null, subtitle, message)); return dialog; } @Override public void onBodyInitialized(@NonNull View bodyView, @Nullable Bundle savedInstanceState) { super.onBodyInitialized(bodyView, savedInstanceState); setTitle(R.string.lib_details); setMessageIsSelectable(true); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/scanner/NativeLibraries.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.scanner; import android.content.Context; import android.text.format.Formatter; import androidx.annotation.AnyThread; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.Collection; import java.util.Enumeration; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipInputStream; import aosp.libcore.util.HexEncoding; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.utils.LangUtils; import io.github.muntashirakon.util.LocalizedString; public class NativeLibraries { public static final String TAG = NativeLibraries.class.getSimpleName(); private static final int ELF_MAGIC = 0x7f454c46; // 0x7f ELF public static abstract class NativeLib implements LocalizedString { @NonNull private final String mPath; @NonNull private final String mName; private final long mSize; private final byte[] mMagic; protected NativeLib(@NonNull String path, long size, byte[] magic) { mPath = path; mName = new File(path).getName(); mSize = size; mMagic = magic; } @NonNull public String getPath() { return mPath; } @NonNull public String getName() { return mName; } public long getSize() { return mSize; } public byte[] getMagic() { return mMagic; } @NonNull public static NativeLib parse(@NonNull String path, long size, @NonNull InputStream is) throws IOException { byte[] header = new byte[20]; // First 20 bytes is enough is.read(header); ByteBuffer buffer = ByteBuffer.wrap(header); int magic = buffer.getInt(); if (magic != ELF_MAGIC) { // Invalid library Log.w(TAG, "Invalid header magic 0x%x at path %s", magic, path); return new InvalidLib(path, size, header); } ElfLib elfLib = new ElfLib(path, size); elfLib.mArch = buffer.get(); // EI_CLASS elfLib.mEndianness = buffer.get(); // EI_DATA if (elfLib.mEndianness == ElfLib.ENDIANNESS_LITTLE_ENDIAN) { buffer.order(ByteOrder.LITTLE_ENDIAN); } buffer.position(16); elfLib.mType = buffer.getChar(); // e_type elfLib.mIsa = buffer.getChar(); // e_machine return elfLib; } } public static class InvalidLib extends NativeLib { protected InvalidLib(@NonNull String path, long size, byte[] magic) { super(path, size, magic); } @NonNull @Override public CharSequence toLocalizedString(@NonNull Context context) { StringBuilder sb = new StringBuilder(); if (getSize() != -1) { sb.append(Formatter.formatFileSize(context, getSize())).append(", "); } sb.append("Magic") .append(LangUtils.getSeparatorString()) .append(HexEncoding.encodeToString(getMagic())) .append("\n") .append(getPath()); return sb; } @NonNull @Override public String toString() { return "InvalidLib{" + "mPath='" + getPath() + '\'' + ", mName='" + getName() + '\'' + '}'; } } public static class ElfLib extends NativeLib { public static final int ARCH_NONE = 0; // ELFCLASSNONE public static final int ARCH_32BIT = 1; // ELFCLASS32 public static final int ARCH_64BIT = 2; // ELFCLASS64 @IntDef({ARCH_NONE, ARCH_32BIT, ARCH_64BIT}) @Retention(RetentionPolicy.SOURCE) public @interface Arch { } public static final int ENDIANNESS_NONE = 0; // ELFDATANONE public static final int ENDIANNESS_LITTLE_ENDIAN = 1; // ELFDATA2LSB public static final int ENDIANNESS_BIG_ENDIAN = 2; // ELFDATA2MSB @IntDef({ENDIANNESS_NONE, ENDIANNESS_LITTLE_ENDIAN, ENDIANNESS_BIG_ENDIAN}) @Retention(RetentionPolicy.SOURCE) public @interface Endianness { } public static final int TYPE_NONE = 0; public static final int TYPE_REL = 1; public static final int TYPE_EXEC = 2; public static final int TYPE_DYN = 3; public static final int TYPE_CORE = 4; @IntDef({TYPE_NONE, TYPE_REL, TYPE_EXEC, TYPE_DYN, TYPE_CORE}) @Retention(RetentionPolicy.SOURCE) public @interface Type { } @Arch private int mArch; @Endianness private int mEndianness; @Type private int mType; private int mIsa; private ElfLib(@NonNull String path, long size) { super(path, size, new byte[]{0x7f, 0x45, 0x4c, 0x46}); } @Arch public int getArch() { return mArch; } @Endianness public int getEndianness() { return mEndianness; } @Type public int getType() { return mType; } public int getIsa() { return mIsa; } public String getIsaString() { // https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/elf-em.h switch (mIsa) { case 0: return "Unknown"; case 3: return "x86"; case 8: return "MIPS"; case 40: return "ARM"; case 62: return "x86_64"; case 92: return "OpenRISC"; case 183: return "AArch64"; case 0xF3: return "RISC-V"; default: // Not available in Android, but just in case return String.format("Unknown(0x%x)", mIsa); } } @NonNull @Override public String toString() { return "ElfLib{" + "mPath='" + getPath() + '\'' + ", mName='" + getName() + '\'' + ", mArch=" + mArch + ", mEndianness=" + mEndianness + ", mType=" + mType + ", mIsa=" + getIsaString() + '}'; } @NonNull @Override public CharSequence toLocalizedString(@NonNull Context context) { StringBuilder sb = new StringBuilder(); if (getSize() != -1) { sb.append(Formatter.formatFileSize(context, getSize())).append(", "); } switch (mArch) { case ARCH_32BIT: sb.append(context.getString(R.string.binary_32_bit)).append(", "); break; case ARCH_64BIT: sb.append(context.getString(R.string.binary_64_bit)).append(", "); break; case ARCH_NONE: break; } switch (mEndianness) { case ENDIANNESS_BIG_ENDIAN: sb.append(context.getString(R.string.endianness_big_endian)).append(", "); break; case ENDIANNESS_LITTLE_ENDIAN: sb.append(context.getString(R.string.endianness_little_endian)).append(", "); break; case ENDIANNESS_NONE: break; } switch (mType) { case TYPE_NONE: case TYPE_CORE: case TYPE_REL: // Not available in Android break; case TYPE_DYN: sb.append(context.getString(R.string.so_type_shared_library)).append(", "); break; case TYPE_EXEC: sb.append(context.getString(R.string.so_type_executable)).append(", "); break; } sb.append(getIsaString()).append("\n").append(getPath()); return sb; } } private final List mLibs = new ArrayList<>(); private final Set mUniqueLibs = new HashSet<>(); @WorkerThread public NativeLibraries(@NonNull File apkFile) throws IOException { try (ZipFile zipFile = new ZipFile(apkFile)) { Enumeration zipEntries = zipFile.entries(); while (zipEntries.hasMoreElements()) { ZipEntry zipEntry = zipEntries.nextElement(); if (zipEntry.getName().endsWith(".so")) { try (InputStream is = zipFile.getInputStream(zipEntry)) { NativeLib nativeLib = NativeLib.parse(zipEntry.getName(), zipEntry.getSize(), is); mLibs.add(nativeLib); mUniqueLibs.add(nativeLib.getName()); } catch (IOException e) { Log.w(TAG, "Could not load native library %s", e, zipEntry.getName()); } } } } } @WorkerThread public NativeLibraries(@NonNull InputStream apkInputStream) throws IOException { try (ZipInputStream zipInputStream = new ZipInputStream(apkInputStream)) { ZipEntry zipEntry; while ((zipEntry = zipInputStream.getNextEntry()) != null) { if (zipEntry.getName().endsWith(".so")) { try { NativeLib nativeLib = NativeLib.parse(zipEntry.getName(), zipEntry.getSize(), zipInputStream); mLibs.add(nativeLib); mUniqueLibs.add(nativeLib.getName()); } catch (IOException e) { Log.w(TAG, "Could not load native library %s", e, zipEntry.getName()); } } } } } @AnyThread public NativeLibraries(@NonNull ZipFile zipFile) throws IOException { Enumeration zipEntries = zipFile.entries(); while (zipEntries.hasMoreElements()) { ZipEntry zipEntry = zipEntries.nextElement(); if (!zipEntry.isDirectory() && zipEntry.getName().endsWith(".so")) { try (InputStream is = zipFile.getInputStream(zipEntry)) { NativeLib nativeLib = NativeLib.parse(zipEntry.getName(), zipEntry.getSize(), is); mLibs.add(nativeLib); mUniqueLibs.add(nativeLib.getName()); } catch (IOException e) { Log.w(TAG, "Could not load native library %s", e, zipEntry.getName()); } } } } @NonNull public List getLibs() { return mLibs; } @NonNull public Collection getUniqueLibs() { return mUniqueLibs; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/scanner/Pithus.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.scanner; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import java.io.File; import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; public class Pithus { private static final String BASE_URL = "https://beta.pithus.org/report"; @WorkerThread @Nullable public static String resolveReport(@NonNull String sha256Sum) throws IOException { URL url = new URL(BASE_URL + File.separator + sha256Sum); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setInstanceFollowRedirects(false); connection.setUseCaches(false); connection.setRequestMethod("GET"); if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { return url.toString(); } return null; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/scanner/ScannerActivity.java ================================================ // SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.scanner; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.os.ParcelFileDescriptor; import android.view.Menu; import android.view.MenuItem; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.appcompat.app.ActionBar; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import com.google.android.material.progressindicator.LinearProgressIndicator; import java.io.File; import java.io.FileNotFoundException; import io.github.muntashirakon.AppManager.BaseActivity; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.apk.installer.PackageInstallerActivity; import io.github.muntashirakon.AppManager.fm.FmProvider; import io.github.muntashirakon.AppManager.intercept.IntentCompat; import io.github.muntashirakon.AppManager.settings.FeatureController; import io.github.muntashirakon.AppManager.utils.FileUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.io.IoUtils; // Copyright 2015 Google, Inc. public class ScannerActivity extends BaseActivity { public static final String EXTRA_IS_EXTERNAL = "is_external"; @Nullable private ActionBar mActionBar; @Nullable private LinearProgressIndicator mProgressIndicator; @Nullable private ParcelFileDescriptor mFd; @Nullable private Uri mApkUri; private boolean mIsExternalApk; @Override protected void onDestroy() { FileUtils.deleteSilently(getCodeCacheDir()); IoUtils.closeQuietly(mFd); super.onDestroy(); } @Override protected void onAuthenticated(Bundle savedInstanceState) { setContentView(R.layout.activity_fm); setSupportActionBar(findViewById(R.id.toolbar)); ScannerViewModel model = new ViewModelProvider(this).get(ScannerViewModel.class); mActionBar = getSupportActionBar(); Intent intent = getIntent(); mIsExternalApk = intent.getBooleanExtra(EXTRA_IS_EXTERNAL, true); mProgressIndicator = findViewById(R.id.progress_linear); mProgressIndicator.setVisibilityAfterHide(View.GONE); showProgress(true); mApkUri = IntentCompat.getDataUri(intent); if (mApkUri == null) { UIUtils.displayShortToast(R.string.error); finish(); return; } File apkFile = null; if (Intent.ACTION_VIEW.equals(intent.getAction())) { if (!FmProvider.AUTHORITY.equals(mApkUri.getAuthority())) { try { mFd = FileUtils.getFdFromUri(this, mApkUri, "r"); apkFile = FileUtils.getFileFromFd(mFd); } catch (FileNotFoundException e) { e.printStackTrace(); } } } else { String path = mApkUri.getPath(); if (path != null) apkFile = new File(path); } model.setApkFile(apkFile); model.setApkUri(mApkUri); getSupportFragmentManager() .beginTransaction() .setCustomAnimations( R.animator.enter_from_left, R.animator.enter_from_right, R.animator.exit_from_right, R.animator.exit_from_left ) .replace(R.id.main_layout, new ScannerFragment()) .commit(); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.activity_scanner, menu); return super.onCreateOptionsMenu(menu); } @Override public boolean onPrepareOptionsMenu(@NonNull Menu menu) { menu.findItem(R.id.action_install).setVisible(mIsExternalApk && FeatureController.isInstallerEnabled()); return super.onPrepareOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int id = item.getItemId(); if (id == android.R.id.home) { getOnBackPressedDispatcher().onBackPressed(); return true; } else if (id == R.id.action_install) { if (mApkUri != null) { startActivity(PackageInstallerActivity.getLaunchableInstance(getApplicationContext(), mApkUri)); return true; } } return super.onOptionsItemSelected(item); } public void setSubtitle(CharSequence subtitle) { if (mActionBar != null) { mActionBar.setSubtitle(subtitle); } } public void setSubtitle(@StringRes int subtitle) { if (mActionBar != null) { mActionBar.setSubtitle(subtitle); } } void showProgress(boolean willShow) { if (mProgressIndicator == null) { return; } if (willShow) { mProgressIndicator.show(); } else { mProgressIndicator.hide(); } } public void loadNewFragment(Fragment fragment) { getSupportFragmentManager() .beginTransaction() .setCustomAnimations( R.animator.enter_from_left, R.animator.enter_from_right, R.animator.exit_from_right, R.animator.exit_from_left ) .replace(R.id.main_layout, fragment) .addToBackStack(null) .commit(); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/scanner/ScannerFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.scanner; import static io.github.muntashirakon.AppManager.utils.UIUtils.getColoredText; import static io.github.muntashirakon.AppManager.utils.UIUtils.getMonospacedText; import static io.github.muntashirakon.AppManager.utils.UIUtils.getPrimaryText; import static io.github.muntashirakon.AppManager.utils.UIUtils.getSmallerText; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.os.Bundle; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextUtils; import android.util.Pair; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.collection.ArrayMap; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import com.google.android.material.card.MaterialCardView; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.io.File; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.regex.Pattern; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.scanner.vt.VtFileReport; import io.github.muntashirakon.AppManager.scanner.vt.VtAvEngineResult; import io.github.muntashirakon.AppManager.settings.FeatureController; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.utils.ArrayUtils; import io.github.muntashirakon.AppManager.utils.DateUtils; import io.github.muntashirakon.AppManager.utils.DigestUtils; import io.github.muntashirakon.AppManager.utils.LangUtils; import io.github.muntashirakon.AppManager.utils.PackageUtils; import io.github.muntashirakon.AppManager.utils.TextUtilsCompat; import io.github.muntashirakon.AppManager.utils.appearance.ColorCodes; import io.github.muntashirakon.dialog.SearchableMultiChoiceDialogBuilder; import io.github.muntashirakon.util.UiUtils; public class ScannerFragment extends Fragment { private CharSequence mAppName; private ScannerViewModel mViewModel; private ScannerActivity mActivity; private MaterialCardView mVtContainerView; private TextView mVtTitleView; private TextView mVtDescriptionView; private TextView pithusDescriptionView; @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_scanner, container, false); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { mViewModel = new ViewModelProvider(requireActivity()).get(ScannerViewModel.class); mActivity = (ScannerActivity) requireActivity(); int cardColor = ColorCodes.getListItemColor1(mActivity); MaterialCardView classesView = view.findViewById(R.id.classes); classesView.setCardBackgroundColor(cardColor); MaterialCardView trackersView = view.findViewById(R.id.tracker); trackersView.setCardBackgroundColor(cardColor); mVtContainerView = view.findViewById(R.id.vt); mVtContainerView.setCardBackgroundColor(cardColor); mVtTitleView = view.findViewById(R.id.vt_title); mVtDescriptionView = view.findViewById(R.id.vt_description); MaterialCardView pithusContainerView = view.findViewById(R.id.pithus); pithusContainerView.setCardBackgroundColor(cardColor); pithusDescriptionView = view.findViewById(R.id.pithus_description); MaterialCardView libsView = view.findViewById(R.id.libs); libsView.setCardBackgroundColor(cardColor); MaterialCardView apkInfoView = view.findViewById(R.id.apk); apkInfoView.setCardBackgroundColor(cardColor); MaterialCardView signaturesView = view.findViewById(R.id.signatures); signaturesView.setCardBackgroundColor(cardColor); MaterialCardView missingLibsView = view.findViewById(R.id.missing_libs); missingLibsView.setCardBackgroundColor(cardColor); // VirusTotal if (!FeatureController.isVirusTotalEnabled() || Prefs.VirusTotal.getApiKey() == null) { mVtContainerView.setVisibility(View.GONE); view.findViewById(R.id.vt_disclaimer).setVisibility(View.GONE); } // Pithus if (!FeatureController.isInternetEnabled()) { pithusContainerView.setVisibility(View.GONE); } // Checksum mViewModel.apkChecksumsLiveData().observe(getViewLifecycleOwner(), checksums -> { if (checksums == null) { return; } List lines = new ArrayList<>(); for (Pair digest : checksums) { lines.add(new SpannableStringBuilder() .append(getPrimaryText(mActivity, digest.first + LangUtils.getSeparatorString())) .append(getMonospacedText(digest.second))); } ((TextView) view.findViewById(R.id.apk_title)).setText(R.string.apk_checksums); ((TextView) view.findViewById(R.id.apk_description)).setText(TextUtilsCompat.joinSpannable("\n", lines)); }); // Package info: Title & subtitle mViewModel.packageInfoLiveData().observe(getViewLifecycleOwner(), packageInfo -> { if (packageInfo != null) { String archiveFilePath = mViewModel.getApkFile().getAbsolutePath(); final ApplicationInfo applicationInfo = packageInfo.applicationInfo; applicationInfo.publicSourceDir = archiveFilePath; applicationInfo.sourceDir = archiveFilePath; mAppName = applicationInfo.loadLabel(mActivity.getPackageManager()); } else { File apkFile = mViewModel.getApkFile(); mAppName = apkFile != null ? apkFile.getName() : mViewModel.getApkUri().getLastPathSegment(); } mActivity.setTitle(mAppName); mActivity.setSubtitle(R.string.scanner); }); // APK verifier result mViewModel.apkVerifierResultLiveData().observe(getViewLifecycleOwner(), result -> { TextView checksumDescription = view.findViewById(R.id.checksum_description); SpannableStringBuilder builder = new SpannableStringBuilder(); builder.append(PackageUtils.getApkVerifierInfo(result, mActivity)); List certificates = result.getSignerCertificates(); if (certificates != null && !certificates.isEmpty()) { builder.append(getCertificateInfo(mActivity, certificates)); } checksumDescription.setText(builder); }); // List all classes mViewModel.allClassesLiveData().observe(getViewLifecycleOwner(), allClasses -> { ((TextView) view.findViewById(R.id.classes_title)).setText(getResources().getQuantityString(R.plurals.classes, allClasses.size(), allClasses.size())); classesView.setOnClickListener(v -> mActivity.loadNewFragment(new ClassListingFragment())); }); // List tracker classes mViewModel.trackerClassesLiveData().observe(getViewLifecycleOwner(), trackerClasses -> setTrackerInfo(trackerClasses, view)); // List library classes mViewModel.libraryClassesLiveData().observe(getViewLifecycleOwner(), libraryClasses -> { setLibraryInfo(libraryClasses, view); // Progress is dismissed here because this will take the largest time mActivity.showProgress(false); }); // List missing classes mViewModel.missingClassesLiveData().observe(getViewLifecycleOwner(), missingClasses -> { if (!missingClasses.isEmpty()) { ((TextView) view.findViewById(R.id.missing_libs_title)).setText(getResources().getQuantityString(R.plurals.missing_signatures, missingClasses.size(), missingClasses.size())); missingLibsView.setVisibility(View.VISIBLE); missingLibsView.setOnClickListener(v2 -> new SearchableMultiChoiceDialogBuilder<>(mActivity, missingClasses, ArrayUtils.toCharSequence(missingClasses)) .setTitle(R.string.signatures) .showSelectAll(false) .setNegativeButton(R.string.ok, null) .setNeutralButton(R.string.send_selected, (dialog, which, selectedItems) -> { String message = "Package: " + mViewModel.getPackageName() + "\n" + "Signatures: " + selectedItems; Intent i = new Intent(Intent.ACTION_SEND); i.setType("message/rfc822"); i.putExtra(Intent.EXTRA_EMAIL, new String[]{"am4android@riseup.net"}); i.putExtra(Intent.EXTRA_SUBJECT, "App Manager: Missing signatures"); i.putExtra(Intent.EXTRA_TEXT, message); startActivity(Intent.createChooser(i, getText(R.string.signatures))); }) .show()); } }); mViewModel.vtFileUploadLiveData().observe(getViewLifecycleOwner(), permalink -> { if (permalink == null) { // Uploading mVtTitleView.setText(R.string.vt_uploading); if (Prefs.VirusTotal.promptBeforeUpload()) { new MaterialAlertDialogBuilder(mActivity) .setTitle(R.string.scan_in_vt) .setMessage(R.string.vt_confirm_uploading_file) .setCancelable(false) .setPositiveButton(R.string.vt_confirm_upload_and_scan, (dialog, which) -> mViewModel.enableUploading()) .setNegativeButton(R.string.no, (dialog, which) -> mViewModel.disableUploading()) .show(); } else mViewModel.enableUploading(); } else { // Upload completed and queued mVtTitleView.setText(R.string.vt_queued); mVtDescriptionView.setText(permalink); } }); mViewModel.vtFileReportLiveData().observe(getViewLifecycleOwner(), vtFileReport -> { if (vtFileReport == null) { // Failed mVtTitleView.setText(R.string.vt_failed); mVtDescriptionView.setText(null); mVtContainerView.setOnClickListener(null); } else { // Successful publishVirusTotalReport(vtFileReport); } }); mViewModel.getPithusReportLiveData().observe(getViewLifecycleOwner(), url -> { if (url != null) { // Report available pithusDescriptionView.setText(url); } else { // Report unavailable pithusDescriptionView.setText(R.string.report_not_available); } }); // Load summary for the APK file mViewModel.loadSummary(); } private void publishVirusTotalReport(@NonNull VtFileReport vtFileReport) { int positives = Objects.requireNonNull(vtFileReport.getPositives()); CharSequence resultSummary = getString(R.string.vt_success, positives, vtFileReport.getTotal()); @ColorInt int color; if (positives <= 3) { color = ColorCodes.getVirusTotalSafeIndicatorColor(mActivity); } else if (positives <= 12) { color = ColorCodes.getVirusTotalUnsafeIndicatorColor(mActivity); } else color = ColorCodes.getVirusTotalExtremelyUnsafeIndicatorColor(mActivity); CharSequence scanDate = getString(R.string.vt_scan_date, DateUtils.formatDateTime(mActivity, vtFileReport.scanDate)); String permalink = vtFileReport.permalink; Spanned result; List vtFileReportScanItems = vtFileReport.results; if (!vtFileReportScanItems.isEmpty()) { int colorUnsafe = ColorCodes.getVirusTotalExtremelyUnsafeIndicatorColor(mActivity); int colorSafe = ColorCodes.getVirusTotalSafeIndicatorColor(mActivity); ArrayList detectedList = new ArrayList<>(); ArrayList suspiciousList = new ArrayList<>(); ArrayList undetectedList = new ArrayList<>(); ArrayList neutralList = new ArrayList<>(); for (VtAvEngineResult item : vtFileReportScanItems) { SpannableStringBuilder sb = new SpannableStringBuilder(); Spannable title = getPrimaryText(mActivity, item.engineName); if (item.category < VtAvEngineResult.CAT_UNDETECTED) { sb.append(title); neutralList.add(sb); } else if (item.category < VtAvEngineResult.CAT_SUSPICIOUS) { sb.append(getColoredText(title, colorSafe)); undetectedList.add(sb); } else if (item.category == VtAvEngineResult.CAT_SUSPICIOUS) { sb.append(getColoredText(title, colorUnsafe)); suspiciousList.add(sb); } else { // malicious sb.append(getColoredText(title, colorUnsafe)); detectedList.add(sb); } sb.append(getSmallerText(" (" + item.engineVersion + ")")); if (item.result != null) { sb.append("\n").append(item.result); } } detectedList.addAll(suspiciousList); detectedList.addAll(undetectedList); detectedList.addAll(neutralList); result = UiUtils.getOrderedList(detectedList); } else result = null; mVtTitleView.setText(getColoredText(resultSummary, color)); if (result != null) { mVtDescriptionView.setText(R.string.tap_to_see_details); mVtContainerView.setOnClickListener(v -> { VirusTotalDialog fragment = VirusTotalDialog.getInstance(resultSummary, scanDate, result, permalink); fragment.show(getParentFragmentManager(), VirusTotalDialog.TAG); }); } } @NonNull private Map getNativeLibraryInfo(boolean trackerOnly) { Collection nativeLibsInApk = mViewModel.getNativeLibraries(); if (nativeLibsInApk.isEmpty()) return new HashMap<>(); String[] libNames = getResources().getStringArray(R.array.lib_native_names); String[] libSignatures = getResources().getStringArray(R.array.lib_native_signatures); int[] isTracker = getResources().getIntArray(R.array.lib_native_is_tracker); // The following array is directly mapped to the arrays above @SuppressWarnings("unchecked") List[] matchedLibs = new List[libSignatures.length]; Map foundNativeLibInfoMap = new ArrayMap<>(); for (int i = 0; i < libSignatures.length; ++i) { if (trackerOnly && isTracker[i] == 0) continue; Pattern pattern = Pattern.compile(libSignatures[i]); for (String lib : nativeLibsInApk) { if (pattern.matcher(lib).find()) { if (matchedLibs[i] == null) { matchedLibs[i] = new ArrayList<>(); } matchedLibs[i].add(lib); } } if (matchedLibs[i] == null) continue; SpannableStringBuilder builder = foundNativeLibInfoMap.get(libNames[i]); if (builder == null) { builder = new SpannableStringBuilder(getPrimaryText(mActivity, libNames[i])); foundNativeLibInfoMap.put(libNames[i], builder); } for (String lib : matchedLibs[i]) { builder.append("\n").append(getMonospacedText(lib)); } } return foundNativeLibInfoMap; } private void setTrackerInfo(@NonNull List trackerInfoList, @NonNull View view) { Map foundTrackerInfoMap = new ArrayMap<>(); foundTrackerInfoMap.putAll(getNativeLibraryInfo(true)); boolean hasSecondDegree = false; // Iterate over signatures again but this time list only the found ones. for (SignatureInfo trackerInfo : trackerInfoList) { if (foundTrackerInfoMap.get(trackerInfo.label) == null) { foundTrackerInfoMap.put(trackerInfo.label, new SpannableStringBuilder() .append(getPrimaryText(mActivity, trackerInfo.label))); } //noinspection ConstantConditions Never null here foundTrackerInfoMap.get(trackerInfo.label) .append("\n") .append(getMonospacedText(trackerInfo.signature)) .append(getSmallerText(" (" + trackerInfo.getCount() + ")")); if (!hasSecondDegree) { hasSecondDegree = trackerInfo.label.startsWith("²"); } } Set foundTrackerNames = foundTrackerInfoMap.keySet(); List foundTrackerInfo = new ArrayList<>(foundTrackerInfoMap.values()); Collections.sort(foundTrackerInfo, (o1, o2) -> o1.toString().compareToIgnoreCase(o2.toString())); SpannableStringBuilder trackerList = new SpannableStringBuilder(UiUtils.getOrderedList(foundTrackerInfo)); SpannableStringBuilder foundTrackerList = new SpannableStringBuilder(); int totalTrackersFound = foundTrackerInfoMap.size(); if (totalTrackersFound > 0) { foundTrackerList.append(getString(R.string.found_trackers)).append(" ").append( TextUtilsCompat.joinSpannable(", ", foundTrackerNames)); } int totalTrackerClasses = mViewModel.getTrackerClasses().size(); // Get summary CharSequence summary; if (totalTrackersFound == 0) { summary = getString(R.string.no_tracker_found); } else if (totalTrackersFound == 1) { summary = getResources().getQuantityString(R.plurals.tracker_and_classes, totalTrackerClasses, totalTrackerClasses); } else if (totalTrackersFound == 2) { summary = getResources().getQuantityString(R.plurals.two_trackers_and_classes, totalTrackerClasses, totalTrackerClasses); } else { summary = getResources().getQuantityString(R.plurals.other_trackers_and_classes, totalTrackersFound, totalTrackersFound, totalTrackerClasses); } // Add colours CharSequence coloredSummary; if (totalTrackersFound == 0) { coloredSummary = getColoredText(summary, ColorCodes.getScannerNoTrackerIndicatorColor(mActivity)); } else { coloredSummary = getColoredText(summary, ColorCodes.getScannerTrackerIndicatorColor(mActivity)); } TextView trackerInfoTitle = view.findViewById(R.id.tracker_title); TextView trackerInfoDescription = view.findViewById(R.id.tracker_description); trackerInfoTitle.setText(coloredSummary); if (totalTrackersFound == 0) { trackerInfoDescription.setVisibility(View.GONE); return; } trackerInfoDescription.setVisibility(View.VISIBLE); trackerInfoDescription.setText(foundTrackerList); MaterialCardView trackersView = view.findViewById(R.id.tracker); boolean finalHasSecondDegree = hasSecondDegree; trackersView.setOnClickListener(v -> { TrackerInfoDialog fragment = TrackerInfoDialog.getInstance(coloredSummary, trackerList, finalHasSecondDegree); fragment.show(getParentFragmentManager(), TrackerInfoDialog.TAG); }); } private void setLibraryInfo(@NonNull List libraryInfoList, @NonNull View view) { Map foundLibInfoMap = new ArrayMap<>(); foundLibInfoMap.putAll(getNativeLibraryInfo(false)); // Iterate over signatures again but this time list only the found ones. for (SignatureInfo libraryInfo : libraryInfoList) { if (foundLibInfoMap.get(libraryInfo.label) == null) { // Add the lib info since it isn't added already foundLibInfoMap.put(libraryInfo.label, new SpannableStringBuilder() .append(getPrimaryText(mActivity, libraryInfo.label)) .append(getSmallerText(" (" + libraryInfo.type + ")"))); } //noinspection ConstantConditions Never null here foundLibInfoMap.get(libraryInfo.label) .append("\n") .append(getMonospacedText(libraryInfo.signature)) .append(getSmallerText(" (" + libraryInfo.getCount() + ")")); } Set foundLibNames = foundLibInfoMap.keySet(); List foundLibInfoList = new ArrayList<>(foundLibInfoMap.values()); int totalLibsFound = foundLibInfoList.size(); Collections.sort(foundLibInfoList, (o1, o2) -> o1.toString().compareToIgnoreCase(o2.toString())); Spanned foundLibsInfo = UiUtils.getOrderedList(foundLibInfoList); String summary; if (totalLibsFound == 0) { summary = getString(R.string.no_libs); } else { summary = getResources().getQuantityString(R.plurals.libraries, totalLibsFound, totalLibsFound); } ((TextView) view.findViewById(R.id.libs_title)).setText(summary); ((TextView) view.findViewById(R.id.libs_description)).setText(TextUtils.join(", ", foundLibNames)); if (totalLibsFound == 0) return; MaterialCardView libsView = view.findViewById(R.id.libs); libsView.setOnClickListener(v -> { LibraryInfoDialog fragment = LibraryInfoDialog.getInstance(summary, foundLibsInfo); fragment.show(getParentFragmentManager(), LibraryInfoDialog.TAG); }); } @NonNull private static Spannable getCertificateInfo(@NonNull Context context, @NonNull List certificates) { SpannableStringBuilder builder = new SpannableStringBuilder(); for (X509Certificate cert : certificates) { try { if (builder.length() > 0) builder.append("\n\n"); builder.append(getPrimaryText(context, context.getString(R.string.issuer) + LangUtils.getSeparatorString())) .append(cert.getIssuerX500Principal().getName()).append("\n") .append(getPrimaryText(context, context.getString(R.string.algorithm) + LangUtils.getSeparatorString())) .append(cert.getSigAlgName()).append("\n"); // Checksums builder.append(getPrimaryText(context, context.getString(R.string.checksums))); Pair[] digests = DigestUtils.getDigests(cert.getEncoded()); for (Pair digest : digests) { builder.append("\n") .append(getPrimaryText(context, digest.first + LangUtils.getSeparatorString())) .append(getMonospacedText(digest.second)); } } catch (CertificateEncodingException e) { e.printStackTrace(); } } return builder; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/scanner/ScannerViewModel.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.scanner; import android.app.Application; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.util.Pair; import androidx.annotation.AnyThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import com.android.apksig.ApkVerifier; import com.android.apksig.apk.ApkFormatException; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicIntegerArray; import java.util.regex.Pattern; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.StaticDataset; import io.github.muntashirakon.AppManager.fm.ContentType2; import io.github.muntashirakon.AppManager.scanner.vt.VirusTotal; import io.github.muntashirakon.AppManager.scanner.vt.VtFileReport; import io.github.muntashirakon.AppManager.self.filecache.FileCache; import io.github.muntashirakon.AppManager.settings.FeatureController; import io.github.muntashirakon.AppManager.utils.DigestUtils; import io.github.muntashirakon.AppManager.utils.ExUtils; import io.github.muntashirakon.AppManager.utils.FileUtils; import io.github.muntashirakon.AppManager.utils.MultithreadedExecutor; import io.github.muntashirakon.algo.AhoCorasick; import io.github.muntashirakon.io.IoUtils; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; import io.github.muntashirakon.io.fs.DexFileSystem; import io.github.muntashirakon.io.fs.VirtualFileSystem; public class ScannerViewModel extends AndroidViewModel implements VirusTotal.FullScanResponseInterface { private static final Pattern SIG_TO_IGNORE = Pattern.compile("^(android(|x)|com\\.android|com\\.google\\.android|java(|x)|j\\$\\.(util|time)|\\w\\d?(\\.\\w\\d?)+)\\..*$"); private File mApkFile; private boolean mIsSummaryLoaded = false; private Uri mApkUri; private int mDexVfsId; @Nullable private final VirusTotal mVt; @Nullable private String mPackageName; private List mAllClasses; private List mTrackerClasses; private Collection mNativeLibraries; private CountDownLatch mWaitForFile; private final FileCache mFileCache = new FileCache(); private final MultithreadedExecutor mExecutor = MultithreadedExecutor.getNewInstance(); private final MutableLiveData[]> mApkChecksumsLiveData = new MutableLiveData<>(); private final MutableLiveData mApkVerifierResultLiveData = new MutableLiveData<>(); private final MutableLiveData mPackageInfoLiveData = new MutableLiveData<>(); private final MutableLiveData> mAllClassesLiveData = new MutableLiveData<>(); private final MutableLiveData> mTrackerClassesLiveData = new MutableLiveData<>(); private final MutableLiveData> mLibraryClassesLiveData = new MutableLiveData<>(); private final MutableLiveData> mMissingClassesLiveData = new MutableLiveData<>(); // Null = Uploading, NonNull = Queued private final MutableLiveData mVtFileUploadLiveData = new MutableLiveData<>(); // Null = Failed, NonNull = Result generated private final MutableLiveData mVtFileReportLiveData = new MutableLiveData<>(); private final MutableLiveData mPithusReportLiveData = new MutableLiveData<>(); public ScannerViewModel(@NonNull Application application) { super(application); mVt = VirusTotal.getInstance(); } @Override protected void onCleared() { super.onCleared(); mExecutor.shutdownNow(); IoUtils.closeQuietly(mFileCache); try { VirtualFileSystem.unmount(mDexVfsId); } catch (Throwable e) { e.printStackTrace(); } } @AnyThread public void loadSummary() { if (mIsSummaryLoaded) return; mIsSummaryLoaded = true; mWaitForFile = new CountDownLatch(1); // Cache files mExecutor.submit(() -> { Thread.currentThread().setPriority(Thread.MAX_PRIORITY); try { cacheFileIfRequired(); } finally { mWaitForFile.countDown(); } }); // Generate APK checksums mExecutor.submit(this::generateApkChecksumsAndFetchScanReports); // Verify APK mExecutor.submit(this::loadApkVerifierResult); // Load package info mExecutor.submit(this::loadPackageInfo); // Load all classes mExecutor.submit(() -> { Thread.currentThread().setPriority(Thread.MAX_PRIORITY); loadAllClasses(); }); } @NonNull public LiveData[]> apkChecksumsLiveData() { return mApkChecksumsLiveData; } @NonNull public LiveData apkVerifierResultLiveData() { return mApkVerifierResultLiveData; } @NonNull public LiveData packageInfoLiveData() { return mPackageInfoLiveData; } @NonNull public LiveData> allClassesLiveData() { return mAllClassesLiveData; } public LiveData> trackerClassesLiveData() { return mTrackerClassesLiveData; } public LiveData> libraryClassesLiveData() { return mLibraryClassesLiveData; } public LiveData> missingClassesLiveData() { return mMissingClassesLiveData; } public LiveData vtFileReportLiveData() { return mVtFileReportLiveData; } public LiveData vtFileUploadLiveData() { return mVtFileUploadLiveData; } public LiveData getPithusReportLiveData() { return mPithusReportLiveData; } public List getTrackerClasses() { return mTrackerClasses; } public void setTrackerClasses(List trackerClasses) { mTrackerClasses = trackerClasses; } @Nullable public File getApkFile() { return mApkFile; } @Nullable public String getPackageName() { return mPackageName; } public void setApkFile(@Nullable File apkFile) { mApkFile = apkFile; } public Uri getApkUri() { return mApkUri; } public void setApkUri(@NonNull Uri apkUri) { mApkUri = apkUri; } public List getAllClasses() { return mAllClasses; } public Collection getNativeLibraries() { return mNativeLibraries; } public Uri getUriFromClassName(String className) throws FileNotFoundException { Path fsRoot = VirtualFileSystem.getFsRoot(mDexVfsId); if (fsRoot == null) { throw new FileNotFoundException("FS Root not found."); } return fsRoot.findFile(className.replace('.', '/') + ".smali").getUri(); } @WorkerThread private void cacheFileIfRequired() { // Test if this path is readable if (mApkFile == null || !FileUtils.canReadUnprivileged(mApkFile)) { // Not readable, cache the file try { mApkFile = mFileCache.getCachedFile(Paths.get(mApkUri)); } catch (IOException e) { e.printStackTrace(); } } } @WorkerThread private void generateApkChecksumsAndFetchScanReports() { waitForFile(); Path file = Paths.getUnprivileged(mApkFile); String pithusReportUrl = null; Pair[] digests = ExUtils.exceptionAsNull(() -> DigestUtils.getDigests(file)); mApkChecksumsLiveData.postValue(digests); if (digests != null && FeatureController.isInternetEnabled()) { String sha256 = digests[2].second; pithusReportUrl = ExUtils.exceptionAsNull(() -> Pithus.resolveReport(sha256)); } mPithusReportLiveData.postValue(pithusReportUrl); if (mVt != null && digests != null && FeatureController.isVirusTotalEnabled()) { String md5 = digests[0].second; try { mVt.fetchFileReportOrScan(file, md5, this); } catch (IOException e) { e.printStackTrace(); mVtFileReportLiveData.postValue(null); } } else mVtFileReportLiveData.postValue(null); } private void loadApkVerifierResult() { waitForFile(); try { ApkVerifier.Builder builder = new ApkVerifier.Builder(mApkFile) .setMaxCheckedPlatformVersion(Build.VERSION.SDK_INT); ApkVerifier apkVerifier = builder.build(); ApkVerifier.Result apkVerifierResult = apkVerifier.verify(); mApkVerifierResultLiveData.postValue(apkVerifierResult); } catch (IOException | ApkFormatException | NoSuchAlgorithmException e) { e.printStackTrace(); } } @WorkerThread private void loadPackageInfo() { waitForFile(); PackageManager pm = getApplication().getPackageManager(); PackageInfo packageInfo = pm.getPackageArchiveInfo(mApkFile.getAbsolutePath(), 0); if (packageInfo != null) { mPackageName = packageInfo.packageName; } mPackageInfoLiveData.postValue(packageInfo); } @WorkerThread private void loadAllClasses() { waitForFile(); try { NativeLibraries nativeLibraries = new NativeLibraries(mApkFile); mNativeLibraries = nativeLibraries.getUniqueLibs(); } catch (Throwable e) { mNativeLibraries = Collections.emptyList(); } try { mDexVfsId = VirtualFileSystem.mount(Uri.fromFile(mApkFile), Paths.getUnprivileged(mApkFile), ContentType2.DEX.getMimeType()); DexFileSystem dfs = (DexFileSystem) Objects.requireNonNull(VirtualFileSystem.getFileSystem(mDexVfsId)); mAllClasses = dfs.getDexClasses().getBaseClassNames(); Collections.sort(mAllClasses); } catch (Throwable e) { e.printStackTrace(); mAllClasses = Collections.emptyList(); } mAllClassesLiveData.postValue(mAllClasses); // Load tracker and library info loadTrackers(); loadLibraries(); } @WorkerThread private void loadTrackers() { if (mAllClasses == null) return; List trackerInfoList = new ArrayList<>(); String[] trackerNames = StaticDataset.getTrackerNames(); String[] trackerSignatures = StaticDataset.getTrackerCodeSignatures(); AtomicIntegerArray signatureCount = new AtomicIntegerArray(trackerSignatures.length); mTrackerClasses = new ArrayList<>(); AhoCorasick aho = StaticDataset.getSearchableTrackerSignatures(); { // Iterate over all classes ConcurrentLinkedQueue matchedClasses = new ConcurrentLinkedQueue<>(); mAllClasses.parallelStream() .filter(className -> className.length() > 8 && className.contains(".")) .forEach(className -> { int[] matches = aho.search(className); if (matches.length > 0) { matchedClasses.add(className); for (int idx : matches) { signatureCount.incrementAndGet(idx); } } }); mTrackerClasses.addAll(matchedClasses); } // Iterate over signatures again but this time list only the found ones. for (int i = 0; i < trackerSignatures.length; i++) { if (signatureCount.get(i) == 0) continue; SignatureInfo signatureInfo = new SignatureInfo(trackerSignatures[i], trackerNames[i]); signatureInfo.setCount(signatureCount.get(i)); trackerInfoList.add(signatureInfo); } mTrackerClassesLiveData.postValue(trackerInfoList); } public void loadLibraries() { if (mAllClasses == null) return; List libraryInfoList = new ArrayList<>(); ArrayList missingLibs = new ArrayList<>(); String[] libNames = getApplication().getResources().getStringArray(R.array.lib_names); String[] libSignatures = getApplication().getResources().getStringArray(R.array.lib_signatures); String[] libTypes = getApplication().getResources().getStringArray(R.array.lib_types); // The following array is directly mapped to the arrays above AtomicIntegerArray signatureCount = new AtomicIntegerArray(libSignatures.length); try (AhoCorasick aho = new AhoCorasick(libSignatures)) { // Iterate over all classes ConcurrentLinkedQueue missingClasses = new ConcurrentLinkedQueue<>(); mAllClasses.parallelStream() .filter(className -> className.length() > 8 && className.contains(".")) .forEach(className -> { int[] matches = aho.search(className); if (matches.length > 0) { for (int idx : matches) { signatureCount.incrementAndGet(idx); } } else if ((mPackageName != null && !className.startsWith(mPackageName)) && !SIG_TO_IGNORE.matcher(className).matches()) { missingClasses.add(className); } }); missingLibs.addAll(missingClasses); } // Iterate over signatures again but this time list only the found ones. for (int i = 0; i < libSignatures.length; i++) { if (signatureCount.get(i) == 0) continue; SignatureInfo signatureInfo = new SignatureInfo(libSignatures[i], libNames[i], libTypes[i]); signatureInfo.setCount(signatureCount.get(i)); libraryInfoList.add(signatureInfo); } mLibraryClassesLiveData.postValue(libraryInfoList); if (BuildConfig.DEBUG) { mMissingClassesLiveData.postValue(missingLibs); } } @WorkerThread private void waitForFile() { try { mWaitForFile.await(); } catch (InterruptedException e) { e.printStackTrace(); } } private boolean mUploadingEnabled; private CountDownLatch mUploadingEnabledWatcher; public void enableUploading() { mUploadingEnabled = true; if (mUploadingEnabledWatcher != null) { mUploadingEnabledWatcher.countDown(); } } public void disableUploading() { mUploadingEnabled = false; if (mUploadingEnabledWatcher != null) { mUploadingEnabledWatcher.countDown(); } } @Override public boolean uploadFile() { mUploadingEnabled = false; mUploadingEnabledWatcher = new CountDownLatch(1); mVtFileUploadLiveData.postValue(null); try { mUploadingEnabledWatcher.await(2, TimeUnit.MINUTES); } catch (InterruptedException ignore) { } return mUploadingEnabled; } @Override public void onUploadInitiated() { } @Override public void onUploadCompleted(@NonNull String permalink) { mVtFileUploadLiveData.postValue(permalink); } @Override public void onReportReceived(@NonNull VtFileReport report) { mVtFileReportLiveData.postValue(report); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/scanner/SignatureInfo.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.scanner; import androidx.annotation.NonNull; import java.util.ArrayList; import java.util.List; public class SignatureInfo { public final String signature; public final String label; public final String type; public final List classes; private int mCount; public SignatureInfo(@NonNull String signature, @NonNull String label) { this.signature = signature; this.label = label; this.type = "Tracker"; // Arbitrary type this.classes = new ArrayList<>(); } public SignatureInfo(@NonNull String signature, @NonNull String label, @NonNull String type) { this.signature = signature; this.label = label; this.type = type; this.classes = new ArrayList<>(); } public void setCount(int count) { mCount = count; } public int getCount() { return mCount; } public void addClass(@NonNull String className) { classes.add(className); } @NonNull @Override public String toString() { return "SignatureInfo{" + "signature='" + signature + '\'' + ", label='" + label + '\'' + ", type='" + type + '\'' + ", classes=" + classes + ", mCount=" + mCount + '}'; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/scanner/TrackerInfoDialog.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.scanner; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.text.method.LinkMovementMethod; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.LinearLayoutCompat; import androidx.lifecycle.ViewModelProvider; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.dialog.BottomSheetAlertDialogFragment; import io.github.muntashirakon.util.UiUtils; import io.github.muntashirakon.widget.MaterialAlertView; public class TrackerInfoDialog extends BottomSheetAlertDialogFragment { public static final String TAG = TrackerInfoDialog.class.getSimpleName(); private static final String ARG_HAS_SECOND_DEGREE = "sec_deg"; @NonNull public static TrackerInfoDialog getInstance(@NonNull CharSequence subtitle, @NonNull CharSequence message, boolean hasSecondDegree) { TrackerInfoDialog dialog = new TrackerInfoDialog(); Bundle args = getArgs(null, subtitle, message); args.putBoolean(ARG_HAS_SECOND_DEGREE, hasSecondDegree); dialog.setArguments(args); return dialog; } @Override public void onBodyInitialized(@NonNull View bodyView, @Nullable Bundle savedInstanceState) { super.onBodyInitialized(bodyView, savedInstanceState); ScannerViewModel viewModel = new ViewModelProvider(requireActivity()).get(ScannerViewModel.class); String packageName = viewModel.getPackageName(); boolean hasSecondDegree = requireArguments().getBoolean(ARG_HAS_SECOND_DEGREE, false); setTitle(R.string.tracker_details); if (packageName != null) { setEndIcon(R.drawable.ic_exodusprivacy, R.string.exodus_link, v -> { Uri exodus_link = Uri.parse(String.format( "https://reports.exodus-privacy.eu.org/en/reports/%s/latest/", packageName)); Intent intent = new Intent(Intent.ACTION_VIEW, exodus_link); if (intent.resolveActivity(requireContext().getPackageManager()) != null) { startActivity(intent); } }); } setMessageIsSelectable(true); setMessageMovementMethod(LinkMovementMethod.getInstance()); if (hasSecondDegree) { MaterialAlertView alertView = new MaterialAlertView(bodyView.getContext()); alertView.setAlertType(MaterialAlertView.ALERT_TYPE_INFO); alertView.setText(R.string.second_degree_tracker_note); alertView.setMovementMethod(LinkMovementMethod.getInstance()); LinearLayoutCompat.LayoutParams layoutParams = new LinearLayoutCompat.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); layoutParams.bottomMargin = layoutParams.topMargin = UiUtils.dpToPx(bodyView.getContext(), 8); layoutParams.leftMargin = layoutParams.rightMargin = UiUtils.dpToPx(bodyView.getContext(), 16); prependView(alertView, layoutParams); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/scanner/VirusTotalDialog.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.scanner; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.dialog.BottomSheetAlertDialogFragment; public class VirusTotalDialog extends BottomSheetAlertDialogFragment { public static final String TAG = VirusTotalDialog.class.getSimpleName(); private static final String ARG_PERMALINK = "permalink"; @NonNull public static VirusTotalDialog getInstance(@NonNull CharSequence title, @NonNull CharSequence subtitle, @NonNull CharSequence message, @Nullable String permalink) { VirusTotalDialog dialog = new VirusTotalDialog(); Bundle args = getArgs(title, subtitle, message); args.putString(ARG_PERMALINK, permalink); dialog.setArguments(args); return dialog; } @Override public void onBodyInitialized(@NonNull View bodyView, @Nullable Bundle savedInstanceState) { super.onBodyInitialized(bodyView, savedInstanceState); String permalink = requireArguments().getString(ARG_PERMALINK); if (permalink != null) { setEndIcon(R.drawable.ic_vt, R.string.vt_permalink, v -> { Uri vtPermalink = Uri.parse(permalink); Intent linkIntent = new Intent(Intent.ACTION_VIEW, vtPermalink); if (linkIntent.resolveActivity(requireContext().getPackageManager()) != null) { startActivity(linkIntent); } }); } setMessageIsSelectable(true); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/scanner/vt/VirusTotal.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.scanner.vt; import android.os.PowerManager; import android.os.SystemClock; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import org.json.JSONException; import org.json.JSONObject; import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.Objects; import io.github.muntashirakon.AppManager.settings.FeatureController; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.utils.CpuUtils; import io.github.muntashirakon.AppManager.utils.ExUtils; import io.github.muntashirakon.io.IoUtils; import io.github.muntashirakon.io.Path; public class VirusTotal { public interface FullScanResponseInterface { boolean uploadFile(); void onUploadInitiated(); void onUploadCompleted(@NonNull String permalink); void onReportReceived(@NonNull VtFileReport report); } public static class ResponseV3 { @Nullable public final T response; @Nullable public final VtError error; public final int httpCode; public ResponseV3(@Nullable T response, @Nullable VtError error) { // The params are mutually exclusive assert (response != null && error == null) || (response == null && error != null); this.response = response; this.error = error; if (error != null) { httpCode = error.httpErrorCode; } else httpCode = HttpURLConnection.HTTP_OK; } public boolean shouldRetry() { // It should only retry when the quota is exceeded, or the resource is not found, or // the resource is not yet available if (error == null || error.code == null) { return false; } return (error.code.equals("NotAvailableYet") || error.code.equals("NotFoundError") || error.code.equals("QuotaExceededError")); } @NonNull @Override public String toString() { return "ResponseV3{" + "response=" + response + ", error=" + error + ", httpCode=" + httpCode + '}'; } } protected static final String FORM_DATA_BOUNDARY = "--AppManagerDataBoundary9f3d77ed3a"; protected static final String API_V3_PREFIX = "https://www.virustotal.com/api/v3"; protected static final String URL_FILE_UPLOAD = API_V3_PREFIX + "/files"; protected static final String URL_LARGE_FILE_UPLOAD = API_V3_PREFIX + "/files/upload_url"; protected static final String URL_FILE_REPORT = API_V3_PREFIX + "/files/"; @Nullable public static VirusTotal getInstance() { String apiKey = Prefs.VirusTotal.getApiKey(); if (FeatureController.isVirusTotalEnabled() && apiKey != null) { return new VirusTotal(apiKey); } return null; } private final String mApiKey; public VirusTotal(@NonNull String apiKey) { mApiKey = Objects.requireNonNull(apiKey); } public void fetchFileReportOrScan(@NonNull Path file, @NonNull String checksum, @NonNull FullScanResponseInterface response) throws IOException { ResponseV3 responseReport = fetchFileReport(checksum); if (responseReport.response != null && responseReport.response.hasReport()) { // A report is found response.onReportReceived(responseReport.response); return; } // No report found: either failed or still queued boolean queued = responseReport.response != null && !responseReport.response.hasReport(); if (!queued && !responseReport.shouldRetry()) { // Retry is not available throw new FileNotFoundException("Fetch error: " + responseReport.error); } // Scan or retry VtError error = Objects.requireNonNull(responseReport.error); boolean waitFirst = false; if ("NotFoundError".equals(error.code)) { // Initiate scan if (!response.uploadFile()) { // Scanning disabled throw new FileNotFoundException("File not found in VirusTotal."); } waitFirst = true; PowerManager.WakeLock wakeLock = CpuUtils.getPartialWakeLock("vt_upload"); wakeLock.acquire(); try { long fileSize = file.length(); if (fileSize > 650_000_000) { throw new IOException("APK is larger than 650 MB."); } boolean largeFile = fileSize > 32_000_000L; response.onUploadInitiated(); String filename = file.getName(); ResponseV3 uploadResponse; try (InputStream is = file.openInputStream()) { uploadResponse = largeFile ? uploadLargeFile(filename, is) : uploadFile(filename, is); } if (uploadResponse.response != null) { response.onUploadCompleted(getPermalink(checksum)); } } finally { CpuUtils.releaseWakeLock(wakeLock); } } int waitDuration = 60_000; while (queued || responseReport.shouldRetry()) { if (waitFirst) { // Effectively makes it a do-while loop waitFirst = false; } else { responseReport = fetchFileReport(checksum); queued = responseReport.response != null && !responseReport.response.hasReport(); } // Wait for result: First wait for 1 minute, then for 30 seconds // We won't do it less than 30 seconds since the API has a limit of 4 request/minute SystemClock.sleep(waitDuration); // TODO: 23/5/22 Wait duration should be according to the fileSize waitDuration = 30_000; } if (responseReport.response != null) { response.onReportReceived(responseReport.response); } else { throw new IOException("Scan error: " + responseReport.error); } } @WorkerThread @NonNull public ResponseV3 uploadFile(@NonNull String filename, @NonNull InputStream is) throws IOException { return uploadFile(filename, is, null); } @WorkerThread @NonNull public ResponseV3 uploadFile(@NonNull String filename, @NonNull InputStream is, @Nullable String password) throws IOException { URL url = new URL(URL_FILE_UPLOAD); return uploadAnyFile(url, filename, is, password); } @WorkerThread @NonNull public ResponseV3 uploadLargeFile(@NonNull String filename, @NonNull InputStream is) throws IOException { return uploadLargeFile(filename, is, null); } @WorkerThread @NonNull public ResponseV3 uploadLargeFile(@NonNull String filename, @NonNull InputStream is, @Nullable String password) throws IOException { // First retrieve the upload URL URL url = new URL(URL_LARGE_FILE_UPLOAD); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); try { connection.setUseCaches(false); connection.setRequestMethod("POST"); connection.setDoInput(true); // Set headers connection.setRequestProperty("accept", "application/json"); connection.setRequestProperty("x-apikey", mApiKey); // Response int status = connection.getResponseCode(); if (status < 300) { // Success // Upload the actual file URL uploadUrl = getLargeFileUploadUrl(connection); return uploadAnyFile(uploadUrl, filename, is, password); } else { // Failed return new ResponseV3<>(null, getErrorResponse(connection)); } } finally { connection.disconnect(); } } @WorkerThread @NonNull public ResponseV3 uploadAnyFile(@NonNull URL uploadUrl, @NonNull String filename, @NonNull InputStream is, @Nullable String password) throws IOException { HttpURLConnection connection = (HttpURLConnection) uploadUrl.openConnection(); try { connection.setUseCaches(false); connection.setDoOutput(true); connection.setRequestMethod("POST"); connection.setDoInput(true); // Set headers connection.setRequestProperty("accept", "application/json"); connection.setRequestProperty("x-apikey", mApiKey); connection.setRequestProperty("content-type", "multipart/form-data; boundary=" + FORM_DATA_BOUNDARY); // Set form data OutputStream outputStream = connection.getOutputStream(); if (password != null) { addMultipartFormData(outputStream, "password", password); } addMultipartFormData(outputStream, "file", filename, is); outputStream.write("\r\n".getBytes(StandardCharsets.UTF_8)); outputStream.write(("--" + FORM_DATA_BOUNDARY + "--\r\n").getBytes(StandardCharsets.UTF_8)); outputStream.flush(); // Response int status = connection.getResponseCode(); if (status < 300) { // Success // Example response: { // "data": { // "type": "analysis", // "id": "base64_hash", // "links": { // "self": "https://www.virustotal.com/api/v3/analyses/base64_hash" // } // } //} return new ResponseV3<>(getAnalysisId(connection), null); } else { // Failed return new ResponseV3<>(null, getErrorResponse(connection)); } } finally { connection.disconnect(); } } @WorkerThread @NonNull public ResponseV3 fetchFileReport(@NonNull String id) throws IOException { URL url = new URL(URL_FILE_REPORT + id); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); try { connection.setUseCaches(false); connection.setRequestMethod("GET"); connection.setDoInput(true); // Set headers connection.setRequestProperty("accept", "application/json"); connection.setRequestProperty("x-apikey", mApiKey); // Response int status = connection.getResponseCode(); if (status < 300) { // Success try { JSONObject jsonObject = new JSONObject(getResponseV3(connection)); return new ResponseV3<>(new VtFileReport(jsonObject), null); } catch (JSONException e) { throw new IOException(e); } } else { // Failed return new ResponseV3<>(null, getErrorResponse(connection)); } } finally { connection.disconnect(); } } @NonNull public static String getPermalink(@NonNull String id) { return "https://www.virustotal.com/gui/file/" + id; } @NonNull public static String getAnalysisId(@NonNull HttpURLConnection connection) throws IOException { // https://docs.virustotal.com/reference/files-scan try { JSONObject dataObject = new JSONObject(getResponseV3(connection)) .getJSONObject("data"); assert dataObject.getString("type").equals("analysis"); return dataObject.getString("id"); } catch (JSONException e) { throw new IOException(e); } } @NonNull public static URL getLargeFileUploadUrl(@NonNull HttpURLConnection connection) throws IOException { // https://docs.virustotal.com/reference/files-upload-url try { return new URL(new JSONObject(getResponseV3(connection)).getString("data")); } catch (JSONException e) { throw new IOException(e); } } public static void addMultipartFormData(@NonNull OutputStream os, @NonNull String key, String value) throws IOException { os.write(("--" + FORM_DATA_BOUNDARY + "\r\n").getBytes(StandardCharsets.UTF_8)); os.write(("Content-Disposition: form-data; name=\"" + key + "\"\r\n").getBytes(StandardCharsets.UTF_8)); os.write(("Content-Type: text/plain; charset=UTF-8\r\n").getBytes(StandardCharsets.UTF_8)); os.write(("\r\n" + value + "\r\n").getBytes(StandardCharsets.UTF_8)); } public static void addMultipartFormData(@NonNull OutputStream os, @NonNull String key, @NonNull String filename, @NonNull InputStream is) throws IOException { os.write(("--" + FORM_DATA_BOUNDARY + "\r\n").getBytes(StandardCharsets.UTF_8)); os.write(("Content-Disposition: form-data; name=\"" + key + "\"; filename=\"" + filename + "\"\r\n") .getBytes(StandardCharsets.UTF_8)); os.write(("Content-Type: application/octet-stream\r\n").getBytes(StandardCharsets.UTF_8)); os.write(("Content-Transfer-Encoding: chunked\r\n\r\n").getBytes(StandardCharsets.UTF_8)); IoUtils.copy(is, os); } @WorkerThread @NonNull public static String getResponseV3(@NonNull HttpURLConnection connection) throws IOException { StringBuilder response = new StringBuilder(); try (BufferedReader reader = new BufferedReader(new InputStreamReader( connection.getInputStream(), StandardCharsets.UTF_8))) { String line; while ((line = reader.readLine()) != null) { response.append(line); } } return response.toString(); } @WorkerThread @NonNull public static VtError getErrorResponse(@NonNull HttpURLConnection connection) throws IOException { int status = connection.getResponseCode(); // First try input stream String inResponse = ExUtils.exceptionAsNull(() -> getResponseV3(connection)); if (inResponse != null) { return new VtError(status, inResponse); } // Try error stream StringBuilder response; InputStream errorStream = connection.getErrorStream(); if (errorStream == null) { response = null; } else { response = new StringBuilder(); try { try (BufferedReader reader = new BufferedReader(new InputStreamReader( errorStream, StandardCharsets.UTF_8))) { String line; while ((line = reader.readLine()) != null) { response.append(line); } } } catch (IOException e) { e.printStackTrace(); } } return new VtError(status, response != null ? response.toString() : null); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/scanner/vt/VtAvEngineResult.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.scanner.vt; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.json.JSONException; import org.json.JSONObject; import io.github.muntashirakon.AppManager.utils.JSONUtils; public class VtAvEngineResult { public static final int CAT_UNSUPPORTED = 0; public static final int CAT_TIMEOUT = 1; public static final int CAT_FAILURE = 2; public static final int CAT_UNDETECTED = 3; public static final int CAT_HARMLESS = 4; public static final int CAT_SUSPICIOUS = 5; public static final int CAT_MALICIOUS = 6; @IntDef({CAT_UNSUPPORTED, CAT_TIMEOUT, CAT_FAILURE, CAT_UNDETECTED, CAT_HARMLESS, CAT_SUSPICIOUS, CAT_MALICIOUS}) public @interface Category { } @NonNull private final String internalCategory; @Category public final int category; @NonNull public final String engineName; @Nullable public final String engineUpdate; @Nullable public final String engineVersion; @NonNull public final String method; @Nullable public final String result; public VtAvEngineResult(@NonNull JSONObject avResult) throws JSONException { internalCategory = avResult.getString("category"); category = getCategory(internalCategory); engineName = avResult.getString("engine_name"); engineUpdate = JSONUtils.optString(avResult, "engine_update", null); engineVersion = JSONUtils.optString(avResult, "engine_version", null); method = avResult.getString("method"); result = JSONUtils.optString(avResult, "result", null); } @NonNull @Override public String toString() { return "VtFileReportScanItem{" + "category='" + internalCategory + '\'' + ", engineName='" + engineName + '\'' + ", engineUpdate='" + engineUpdate + '\'' + ", engineVersion='" + engineVersion + '\'' + ", method='" + method + '\'' + ", result='" + result + '\'' + '}'; } @Category private static int getCategory(@NonNull String internalCategory) { switch (internalCategory) { case "confirmed-timeout": case "timeout": return CAT_TIMEOUT; case "harmless": return CAT_HARMLESS; case "undetected": return CAT_UNDETECTED; case "suspicious": return CAT_SUSPICIOUS; case "malicious": return CAT_MALICIOUS; case "type-unsupported": return CAT_UNSUPPORTED; case "failure": default: return CAT_FAILURE; } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/scanner/vt/VtAvEngineStats.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.scanner.vt; import androidx.annotation.NonNull; import org.json.JSONException; import org.json.JSONObject; public class VtAvEngineStats { public final int confirmedTimeout; public final int failure; public final int harmless; public final int malicious; public final int suspicious; public final int timeout; public final int unsupported; public final int undetected; private final int mTotal; private final int mDetected; public VtAvEngineStats(@NonNull JSONObject stats) throws JSONException { confirmedTimeout = stats.getInt("confirmed-timeout"); failure = stats.getInt("failure"); harmless = stats.getInt("harmless"); malicious = stats.getInt("malicious"); suspicious = stats.getInt("suspicious"); timeout = stats.getInt("timeout"); unsupported = stats.getInt("type-unsupported"); undetected = stats.getInt("undetected"); mTotal = harmless + malicious + suspicious + undetected; mDetected = malicious; } public int getTotal() { return mTotal; } public int getDetected() { return mDetected; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/scanner/vt/VtError.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.scanner.vt; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.json.JSONException; import org.json.JSONObject; public class VtError { public final int httpErrorCode; public final String code; public final String message; public VtError(int httpErrorCode, @Nullable String rawJson) { this.httpErrorCode = httpErrorCode; if (TextUtils.isEmpty(rawJson)) { code = null; message = null; } else { String code = null; String message = null; try { JSONObject errorObject = new JSONObject(rawJson).optJSONObject("error"); if (errorObject != null) { code = errorObject.getString("code"); message = errorObject.getString("message"); } } catch (JSONException e) { e.printStackTrace(); } this.code = code; this.message = message; } } @NonNull @Override public String toString() { return "VtError{" + "httpErrorCode=" + httpErrorCode + ", code='" + code + '\'' + ", message='" + message + '\'' + '}'; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/scanner/vt/VtFileReport.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.scanner.vt; import androidx.annotation.NonNull; import java.util.ArrayList; import java.util.Iterator; import org.json.JSONException; import org.json.JSONObject; public class VtFileReport { @NonNull public final ArrayList results; @NonNull public final VtAvEngineStats stats; @NonNull public final String scanId; public final long scanDate; @NonNull public final String permalink; public VtFileReport(@NonNull JSONObject jsonObject) throws JSONException { // Doc: https://docs.virustotal.com/reference/files // Currently, we are only interested in data.attributes.last_analysis_date, // data.attributes.last_analysis_stats, data.attributes.last_analysis_results. JSONObject data = jsonObject.getJSONObject("data"); assert data.getString("type").equals("file"); scanId = data.getString("id"); permalink = VirusTotal.getPermalink(scanId); JSONObject attrs = data.getJSONObject("attributes"); scanDate = attrs.optLong("last_analysis_date") * 1_000; stats = new VtAvEngineStats(attrs.getJSONObject("last_analysis_stats")); JSONObject jsonResults = attrs.getJSONObject("last_analysis_results"); Iterator avEnginesIt = jsonResults.keys(); results = new ArrayList<>(); while (avEnginesIt.hasNext()) { results.add(new VtAvEngineResult(jsonResults.getJSONObject(avEnginesIt.next()))); } } public boolean hasReport() { return scanDate != 0; } public int getTotal() { return stats.getTotal(); } public Integer getPositives() { return stats.getDetected(); } @NonNull @Override public String toString() { return "VtFileReport{" + "results=" + results + ", stats=" + stats + ", scanId='" + scanId + '\'' + ", scanDate=" + scanDate + ", permalink='" + permalink + '\'' + '}'; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/self/BootReceiver.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.self; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import androidx.core.content.ContextCompat; import io.github.muntashirakon.AppManager.self.filecache.InternalCacheCleanerService; import io.github.muntashirakon.AppManager.servermanager.WifiWaitService; import io.github.muntashirakon.AppManager.settings.Ops; public class BootReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) { if (Ops.getMode().equals(Ops.MODE_ADB_WIFI)) { // Connect ADB Intent serviceIntent = new Intent(context, WifiWaitService.class); ContextCompat.startForegroundService(context, serviceIntent); } // Schedule cache cleaning InternalCacheCleanerService.scheduleAlarm(context.getApplicationContext()); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/self/Migration.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.self; import androidx.annotation.NonNull; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; import io.github.muntashirakon.AppManager.utils.ThreadUtils; class Migration { private final List mMigrationTasks = new ArrayList<>(); public void addTask(@NonNull MigrationTask migrationTask) { mMigrationTasks.add(migrationTask); } public void migrate(long fromVersion) { if (fromVersion == 0) { // This is a new version, no migration needed return; } List migrationTasks = mMigrationTasks.stream() // Any tasks with toVersion > fromVersion hasn't been run yet .filter(task -> task.toVersion > fromVersion) // Migration performed in ascending order .sorted(Comparator.comparingLong((MigrationTask task) -> task.fromVersion) .thenComparingLong(task -> task.toVersion)) .collect(Collectors.toList()); for (MigrationTask migrationTask : migrationTasks) { if (migrationTask.mainThread) { ThreadUtils.postOnMainThread(migrationTask); } else migrationTask.run(); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/self/MigrationTask.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.self; import android.content.Context; import io.github.muntashirakon.AppManager.utils.ContextUtils; abstract class MigrationTask implements Runnable { public final long fromVersion; public final long toVersion; public final boolean mainThread; public final Context context; public MigrationTask(long fromVersion, int toVersion) { this(fromVersion, toVersion, false); } public MigrationTask(long fromVersion, long toVersion, boolean mainThread) { this.fromVersion = fromVersion; this.toVersion = toVersion; this.mainThread = mainThread; this.context = ContextUtils.getContext(); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/self/Migrations.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.self; import android.annotation.SuppressLint; import androidx.annotation.WorkerThread; import java.io.File; import io.github.muntashirakon.AppManager.db.AppsDb; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.servermanager.ServerConfig; import io.github.muntashirakon.AppManager.settings.FeatureController; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.FileUtils; import io.github.muntashirakon.io.Paths; @SuppressLint("StaticFieldLeak") public class Migrations { public static final String TAG = Migrations.class.getSimpleName(); private static final MigrationTask MIGRATE_FROM_ALL_VERSION_TO_3_0_0 = new MigrationTask(409, 410) { @Override public void run() { Log.d(TAG, "Running MIGRATE_FROM_ALL_VERSION_TO_3_0_0 from %d to %d", fromVersion, toVersion); // Delete am database, am.jar File internalFilesDir = ContextUtils.getDeContext(context).getFilesDir().getParentFile(); File[] paths = new File[]{ ServerConfig.getDestJarFile(), new File(internalFilesDir, "main.jar"), new File(internalFilesDir, "run_server.sh"), context.getDatabasePath("am"), context.getDatabasePath("am-shm"), context.getDatabasePath("am-wal"), }; for (File path : paths) { FileUtils.deleteSilently(path); } // Delete old cache dir (removed in v2.6.4 (394)) File oldCacheDir = context.getExternalFilesDir("cache"); Paths.get(oldCacheDir).delete(); // Disable Internet feature by default FeatureController.getInstance().modifyState(FeatureController.FEAT_INTERNET, false); } }; private static final MigrationTask MIGRATE_FROM_3_0_0_RC01_RC04_TO_3_0_0 = new MigrationTask(406, 410) { @Override public void run() { Log.d(TAG, "Running MIGRATE_FROM_3_0_0_RC01_RC04_TO_3_0_0 from %d to %d", fromVersion, toVersion); // Clear DB AppsDb.getInstance().backupDao().deleteAll(); } }; private static final MigrationTask MIGRATE_FROM_4_0_5_TO_4_0_6 = new MigrationTask(445, 446) { @Override public void run() { Log.d(TAG, "Running MIGRATE_FROM_4_0_5_TO_4_0_6 from %d to %d", fromVersion, toVersion); // DB File newAppsDb = context.getDatabasePath("apps.db"); if (!newAppsDb.exists()) { File oldAppsDb = new File(FileUtils.getCachePath(), "apps.db"); if (oldAppsDb.exists()) { oldAppsDb.renameTo(newAppsDb); } } } }; private static final Migration migration; static { migration = new Migration(); migration.addTask(MIGRATE_FROM_ALL_VERSION_TO_3_0_0); migration.addTask(MIGRATE_FROM_3_0_0_RC01_RC04_TO_3_0_0); migration.addTask(MIGRATE_FROM_4_0_5_TO_4_0_6); } @WorkerThread public static void startMigration(long fromVersion) { migration.migrate(fromVersion); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/self/SelfPermissions.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.self; import android.Manifest; import android.annotation.UserIdInt; import android.app.AppOpsManager; import android.app.AppOpsManagerHidden; import android.content.pm.PackageManager; import android.os.Build; import android.os.Environment; import android.os.Process; import android.os.RemoteException; import android.os.UserHandleHidden; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.compat.AppOpsManagerCompat; import io.github.muntashirakon.AppManager.compat.ManifestCompat; import io.github.muntashirakon.AppManager.compat.PackageManagerCompat; import io.github.muntashirakon.AppManager.compat.PermissionCompat; import io.github.muntashirakon.AppManager.rules.compontents.ComponentsBlocker; import io.github.muntashirakon.AppManager.settings.FeatureController; import io.github.muntashirakon.AppManager.settings.Ops; import io.github.muntashirakon.AppManager.users.Users; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.Utils; import io.github.muntashirakon.io.Paths; public class SelfPermissions { public static final String SHELL_PACKAGE_NAME = "com.android.shell"; public static void init() { if (!canModifyPermissions()) { return; } String[] permissions = new String[]{ Manifest.permission.DUMP, ManifestCompat.permission.GET_APP_OPS_STATS, ManifestCompat.permission.INTERACT_ACROSS_USERS, Manifest.permission.READ_LOGS, Manifest.permission.WRITE_SECURE_SETTINGS }; int userId = UserHandleHidden.myUserId(); for (String permission : permissions) { if (!checkSelfPermission(permission)) { try { PermissionCompat.grantPermission(BuildConfig.APPLICATION_ID, permission, userId); } catch (Exception ignore) { } } } // Grant usage stats permission (both permission and app op needs to be granted) if (FeatureController.isUsageAccessEnabled()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !checkSelfPermission(Manifest.permission.PACKAGE_USAGE_STATS)) { try { PermissionCompat.grantPermission(BuildConfig.APPLICATION_ID, Manifest.permission.PACKAGE_USAGE_STATS, userId); } catch (Exception ignore) { } } try { AppOpsManagerCompat appOps = new AppOpsManagerCompat(); appOps.setMode(AppOpsManagerHidden.OP_GET_USAGE_STATS, Process.myUid(), BuildConfig.APPLICATION_ID, AppOpsManager.MODE_ALLOWED); } catch (RemoteException ignore) { } } } public static boolean canBlockByIFW() { return Paths.get(ComponentsBlocker.SYSTEM_RULES_PATH).canWrite(); } public static boolean canWriteToDataData() { return Paths.get("/data/data").canWrite(); } public static boolean canModifyAppComponentStates(@UserIdInt int userId, @Nullable String packageName, boolean testOnlyApp) { if (!checkCrossUserPermission(userId, false)) { return false; } final int callingUid = Users.getSelfOrRemoteUid(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // Since Oreo, shell can only disable components of test only apps if (callingUid == Ops.SHELL_UID && !testOnlyApp) { return false; } } if (BuildConfig.APPLICATION_ID.equals(packageName)) { // We can change components for this package return true; } return checkSelfOrRemotePermission(Manifest.permission.CHANGE_COMPONENT_ENABLED_STATE, callingUid); } public static boolean canModifyAppOpMode() { int callingUid = Users.getSelfOrRemoteUid(); boolean canModify = checkSelfOrRemotePermission(ManifestCompat.permission.UPDATE_APP_OPS_STATS, callingUid); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { canModify &= checkSelfOrRemotePermission(ManifestCompat.permission.MANAGE_APP_OPS_MODES, callingUid); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { canModify &= checkSelfOrRemotePermission(ManifestCompat.permission.MANAGE_APPOPS, callingUid); } } return canModify; } public static boolean canModifyPermissions() { int callingUid = Users.getSelfOrRemoteUid(); return checkSelfOrRemotePermission(ManifestCompat.permission.GRANT_RUNTIME_PERMISSIONS, callingUid) || checkSelfOrRemotePermission(ManifestCompat.permission.REVOKE_RUNTIME_PERMISSIONS, callingUid); } public static boolean checkGetGrantRevokeRuntimePermissions() { int callingUid = Users.getSelfOrRemoteUid(); return checkSelfOrRemotePermission(ManifestCompat.permission.GET_RUNTIME_PERMISSIONS, callingUid) || checkSelfOrRemotePermission(ManifestCompat.permission.GRANT_RUNTIME_PERMISSIONS, callingUid) || checkSelfOrRemotePermission(ManifestCompat.permission.REVOKE_RUNTIME_PERMISSIONS, callingUid); } public static boolean canInstallExistingPackages() { int callingUid = Users.getSelfOrRemoteUid(); if (checkSelfOrRemotePermission(Manifest.permission.INSTALL_PACKAGES, callingUid)) { return true; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { return checkSelfOrRemotePermission(ManifestCompat.permission.INSTALL_EXISTING_PACKAGES, callingUid); } return false; } public static boolean canFreezeUnfreezePackages() { // 1. Suspend (7+): MANAGE_USERS (<= 9), SUSPEND_APPS (>= 9) // 2. Disable: CHANGE_COMPONENT_ENABLED_STATE // 2. HIDE: MANAGE_USERS int callingUid = Users.getSelfOrRemoteUid(); boolean canFreezeUnfreeze = checkSelfOrRemotePermission(Manifest.permission.CHANGE_COMPONENT_ENABLED_STATE, callingUid) || checkSelfOrRemotePermission(ManifestCompat.permission.MANAGE_USERS, callingUid); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { canFreezeUnfreeze |= checkSelfOrRemotePermission(ManifestCompat.permission.SUSPEND_APPS, callingUid); } return canFreezeUnfreeze; } public static boolean canClearAppCache() { int callingUid = Users.getSelfOrRemoteUid(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { return checkSelfOrRemotePermission(ManifestCompat.permission.INTERNAL_DELETE_CACHE_FILES, callingUid); } return checkSelfOrRemotePermission(Manifest.permission.DELETE_CACHE_FILES, callingUid); } public static boolean canKillUid() { int callingUid = Users.getSelfOrRemoteUid(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { return checkSelfOrRemotePermission(ManifestCompat.permission.KILL_UID, callingUid); } return callingUid == Ops.SYSTEM_UID; } public static boolean checkNotificationListenerAccess() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) { return false; } int callingUid = Users.getSelfOrRemoteUid(); if (checkSelfOrRemotePermission(ManifestCompat.permission.MANAGE_NOTIFICATION_LISTENERS, callingUid)) { return true; } return callingUid == Ops.ROOT_UID || callingUid == Ops.SYSTEM_UID || callingUid == Ops.PHONE_UID; } public static boolean checkUsageStatsPermission() { AppOpsManagerCompat appOps = new AppOpsManagerCompat(); int callingUid = Users.getSelfOrRemoteUid(); if (callingUid == Ops.ROOT_UID || callingUid == Ops.SYSTEM_UID) { return true; } int mode = appOps.checkOpNoThrow(AppOpsManagerHidden.OP_GET_USAGE_STATS, callingUid, getCallingPackage(callingUid)); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && mode == AppOpsManager.MODE_DEFAULT) { return checkSelfOrRemotePermission(Manifest.permission.PACKAGE_USAGE_STATS, callingUid); } return mode == AppOpsManager.MODE_ALLOWED; } public static boolean checkSelfStoragePermission() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Utils.isRoboUnitTest()) { return false; } return Environment.isExternalStorageManager(); } return checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE); } public static boolean checkStoragePermission() { int callingUid = Users.getSelfOrRemoteUid(); if (callingUid == Ops.ROOT_UID) { return true; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { String packageName = getCallingPackage(callingUid); AppOpsManagerCompat appOps = new AppOpsManagerCompat(); int opMode = appOps.checkOpNoThrow(AppOpsManagerHidden.OP_MANAGE_EXTERNAL_STORAGE, callingUid, packageName); switch (opMode) { case AppOpsManager.MODE_DEFAULT: return checkSelfOrRemotePermission(Manifest.permission.MANAGE_EXTERNAL_STORAGE, callingUid); case AppOpsManager.MODE_ALLOWED: return true; case AppOpsManager.MODE_ERRORED: case AppOpsManager.MODE_IGNORED: return false; default: throw new IllegalStateException("Unknown AppOpsManager mode " + opMode); } } return checkSelfOrRemotePermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, callingUid); } public static boolean checkCrossUserPermission(@UserIdInt int userId, boolean requireFullPermission) { int callingUid = Users.getSelfOrRemoteUid(); return checkCrossUserPermission(userId, requireFullPermission, callingUid); } public static boolean checkCrossUserPermission(@UserIdInt int userId, boolean requireFullPermission, int callingUid) { if (userId == UserHandleHidden.USER_NULL) { userId = UserHandleHidden.myUserId(); } if (userId < 0 && userId != UserHandleHidden.USER_ALL) { throw new IllegalArgumentException("Invalid userId " + userId); } if (isSystemOrRootOrShell(callingUid) || userId == UserHandleHidden.getUserId(callingUid)) { return true; } if (requireFullPermission) { return checkSelfOrRemotePermission(ManifestCompat.permission.INTERACT_ACROSS_USERS_FULL, callingUid); } return checkSelfOrRemotePermission(ManifestCompat.permission.INTERACT_ACROSS_USERS_FULL, callingUid) || checkSelfOrRemotePermission(ManifestCompat.permission.INTERACT_ACROSS_USERS, callingUid); } public static boolean isShell() { return Users.getSelfOrRemoteUid() == Ops.SHELL_UID; } public static boolean isSystem() { return Users.getSelfOrRemoteUid() == Ops.SYSTEM_UID; } public static boolean isSystemOrRoot() { int callingUid = Users.getSelfOrRemoteUid(); return callingUid == Ops.ROOT_UID || callingUid == Ops.SYSTEM_UID; } public static boolean isSystemOrRootOrShell() { return isSystemOrRootOrShell(Users.getSelfOrRemoteUid()); } private static boolean isSystemOrRootOrShell(int callingUid) { return callingUid == Ops.ROOT_UID || callingUid == Ops.SYSTEM_UID || callingUid == Ops.SHELL_UID; } public static boolean checkSelfOrRemotePermission(@NonNull String permissionName) { return checkSelfOrRemotePermission(permissionName, Users.getSelfOrRemoteUid()); } public static boolean checkSelfOrRemotePermission(@NonNull String permissionName, int uid) { if (uid == Ops.ROOT_UID) { // Root UID has all the permissions granted return true; } if (uid != Process.myUid()) { try { return PackageManagerCompat.getPackageManager().checkUidPermission(permissionName, uid) == PackageManager.PERMISSION_GRANTED; } catch (RemoteException ignore) { } } return checkSelfPermission(permissionName); } public static boolean checkSelfPermission(@NonNull String permissionName) { return ContextCompat.checkSelfPermission(ContextUtils.getContext(), permissionName) == PackageManager.PERMISSION_GRANTED; } public static void requireSelfPermission(@NonNull String permissionName) throws SecurityException { if (!checkSelfPermission(permissionName)) { throw new SecurityException("App Manager does not have the required permission " + permissionName); } } @NonNull public static String getCallingPackage(int callingUid) { if (callingUid == Ops.ROOT_UID || callingUid == Ops.SHELL_UID) { return SHELL_PACKAGE_NAME; } if (callingUid == Ops.SYSTEM_UID) { return "android"; } return BuildConfig.APPLICATION_ID; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/self/SelfUriManager.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.self; import android.net.Uri; import android.os.UserHandleHidden; import android.text.TextUtils; import androidx.annotation.Nullable; import io.github.muntashirakon.AppManager.types.UserPackagePair; import io.github.muntashirakon.AppManager.utils.PackageUtils; public class SelfUriManager { public static final String APP_MANAGER_SCHEME = "app-manager"; public static final String SETTINGS_HOST = "settings"; public static final String DETAILS_HOST = "details"; @Nullable public static UserPackagePair getUserPackagePairFromUri(@Nullable Uri detailsUri) { // Required format app-manager://details?id=&user= if (detailsUri == null || !APP_MANAGER_SCHEME.equals(detailsUri.getScheme()) || !DETAILS_HOST.equals(detailsUri.getHost())) { return null; } String pkg = detailsUri.getQueryParameter("id"); String userIdStr = detailsUri.getQueryParameter("user"); if (pkg != null && PackageUtils.validateName(pkg.trim())) { int userId; if (userIdStr != null && TextUtils.isDigitsOnly(userIdStr)) { userId = Integer.parseInt(userIdStr); } else userId = UserHandleHidden.myUserId(); return new UserPackagePair(pkg, userId); } return null; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/self/filecache/FileCache.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.self.filecache; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.io.Closeable; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import io.github.muntashirakon.AppManager.utils.FileUtils; import io.github.muntashirakon.io.IoUtils; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; public class FileCache implements Closeable { private static FileCache sInstance; @NonNull public static FileCache getGlobalFileCache() { if (sInstance == null) { sInstance = new FileCache(false); } return sInstance; } private final File mCacheDir; private final Map mFileCacheMap = new HashMap<>(); private final Set mFileCache = new HashSet<>(); private final Set mDirectoryCache = new HashSet<>(); private final boolean mSessionOnly; private boolean mClosed = false; public FileCache() { this(true); } private FileCache(boolean sessionOnly) { mSessionOnly = sessionOnly; mCacheDir = new File(FileUtils.getCachePath(), "files"); if (!mCacheDir.exists()) { if (!mCacheDir.mkdirs()) { throw new IllegalStateException("Could not create cache. Is this OS broken?"); } } } @Override public void close() { mClosed = true; if (mSessionOnly) { deleteAll(); } } @Override protected void finalize() { if (!mClosed) { close(); } } @NonNull public File getCachedFile(@NonNull Path source) throws IOException { if (!source.exists()) { // No need for cache if the path is non-existent throw new FileNotFoundException("Path " + source + " does not exist."); } File tempFile = mFileCacheMap.get(source); if (tempFile == null || !tempFile.exists()) { String extension = source.getExtension(); tempFile = File.createTempFile(source.getName() + "_", "." + (extension != null ? extension : "tmp"), mCacheDir); mFileCacheMap.put(source, tempFile); } else if (source.lastModified() > 0 && source.lastModified() < tempFile.lastModified()) { return tempFile; } IoUtils.copy(source, Paths.get(tempFile)); return tempFile; } @NonNull public File getCachedFile(@NonNull InputStream is, @Nullable String extension) throws IOException { File tempFile = File.createTempFile("file_", "." + (extension != null ? extension : "tmp"), mCacheDir); mFileCache.add(tempFile); try (OutputStream os = new FileOutputStream(tempFile)) { IoUtils.copy(is, os); } return tempFile; } @NonNull public File getCachedFile(@NonNull byte[] bytes, @Nullable String extension) throws IOException { File tempFile = File.createTempFile("file_", "." + (extension != null ? extension : "tmp"), mCacheDir); mFileCache.add(tempFile); try (OutputStream os = new FileOutputStream(tempFile)) { os.write(bytes); } return tempFile; } @NonNull public File createCachedFile(@Nullable String extension) throws IOException { File tempFile = File.createTempFile("file_", "." + (extension != null ? extension : "tmp"), mCacheDir); mFileCache.add(tempFile); return tempFile; } @NonNull public File createCachedDir(@Nullable String prefix) { if (prefix != null) { prefix = Paths.sanitize(prefix, true); } if (prefix == null) { prefix = "folder"; } String dirName = prefix; int i = 1; File newDir = new File(mCacheDir, dirName); while (newDir.exists()) { dirName = prefix + "_" + i; newDir = new File(mCacheDir, dirName); ++i; } newDir.mkdirs(); // Get first path component from dirName int idx = dirName.indexOf(File.separator); String firstComponent; if (idx == -1) { firstComponent = dirName; } else { firstComponent = dirName.substring(0, idx); } mDirectoryCache.add(new File(mCacheDir, firstComponent)); return newDir; } public boolean delete(@Nullable Path path) { File tempFile = mFileCacheMap.remove(path); return tempFile != null && tempFile.delete(); } public boolean delete(@Nullable File tempFile) { if (mFileCache.remove(tempFile)) { return tempFile != null && tempFile.delete(); } return false; } public void deleteAll() { for (File file : mFileCacheMap.values()) { FileUtils.deleteSilently(file); } for (File file : mFileCache) { FileUtils.deleteSilently(file); } for (File file : mDirectoryCache) { Paths.get(file).delete(); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/self/filecache/InternalCacheCleanerService.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.self.filecache; import android.app.AlarmManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.PendingIntentCompat; import java.util.Calendar; import io.github.muntashirakon.AppManager.types.ForegroundService; import io.github.muntashirakon.AppManager.utils.FileUtils; import io.github.muntashirakon.io.FileSystemManager; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; // IMPORTANT: This service must be run without authentication. public class InternalCacheCleanerService extends ForegroundService { public static final String TAG = InternalCacheCleanerService.class.getSimpleName(); public static void scheduleAlarm(@NonNull Context context) { Intent intent = new Intent(context, InternalCacheCleanerService.class); int flags = PendingIntent.FLAG_UPDATE_CURRENT; PendingIntent pastAlarmIntent = PendingIntentCompat.getService(context, 0, intent, flags | PendingIntent.FLAG_NO_CREATE, false); if (pastAlarmIntent != null) { // Already exists return; } PendingIntent alarmIntent = PendingIntentCompat.getService(context, 0, intent, flags, false); // Set the alarm to start at 5:00 AM Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(System.currentTimeMillis()); calendar.set(Calendar.HOUR_OF_DAY, 5); // Run everyday AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); alarmManager.setInexactRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), AlarmManager.INTERVAL_DAY, alarmIntent); } public InternalCacheCleanerService() { super(TAG); } @Override public int onStartCommand(@Nullable Intent intent, int flags, int startId) { return super.onStartCommand(intent, flags, startId); } @Override protected void onHandleIntent(@Nullable Intent intent) { clearOldFiles(); clearOldImages(); } private void clearOldFiles() { // Delete any files not accessed the last 3 days long lastAccessDate = System.currentTimeMillis() - 259_200_000; Path fileCache = Paths.getUnprivileged(FileSystemManager.getLocal().getFile(FileUtils.getCachePath(), "files")); int deleteCount = deleteFilesWithAccessDate(fileCache, lastAccessDate); Log.i(TAG, "Deleted " + deleteCount + " files and directories."); } private void clearOldImages() { // Delete any files not accessed the last 7 days long lastAccessDate = System.currentTimeMillis() - 604_800_000; Path fileCache = Paths.getUnprivileged(FileSystemManager.getLocal().getFile(FileUtils.getCachePath(), "images")); int deleteCount = deleteFilesWithAccessDate(fileCache, lastAccessDate); Log.i(TAG, "Deleted " + deleteCount + " images."); } private static int deleteFilesWithAccessDate(@NonNull Path basePath, long accessDate) { int deleteCount = 0; Path[] files = basePath.listFiles(); for (Path file : files) { long lastAccess = file.lastAccess(); if (lastAccess <= 0) { lastAccess = file.lastModified(); } if (lastAccess <= accessDate && file.delete()) { ++deleteCount; } } return deleteCount; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/self/imagecache/ImageFileCache.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.self.imagecache; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.drawable.Drawable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.utils.FileUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.io.IoUtils; class ImageFileCache { private static final long sLastModifiedDate = System.currentTimeMillis() - 604_800_000; private final File mCacheDir; public ImageFileCache() { mCacheDir = new File(FileUtils.getCachePath(), "images"); if (!mCacheDir.exists()) { if (!mCacheDir.mkdirs()) { throw new IllegalStateException("Could not create cache. Is this OS broken?"); } } } public void putImage(@NonNull String name, @NonNull InputStream inputStream) throws IOException { File iconFile = getImageFile(name); try (OutputStream os = new FileOutputStream(iconFile)) { IoUtils.copy(inputStream, os); } } public void putImage(@NonNull String name, @NonNull Drawable drawable) throws IOException { putImage(name, UIUtils.getBitmapFromDrawable(drawable)); } public void putImage(@NonNull String name, @NonNull Bitmap bitmap) throws IOException { File iconFile = getImageFile(name); try (OutputStream os = new FileOutputStream(iconFile)) { bitmap.compress(Bitmap.CompressFormat.PNG, 100, os); os.flush(); } } @Nullable public Bitmap getImage(@NonNull String name) { File iconFile = getImageFile(name); if (iconFile.lastModified() >= sLastModifiedDate) { try (FileInputStream fis = new FileInputStream(iconFile)) { return BitmapFactory.decodeStream(fis); } catch (IOException e) { return null; } } return null; } @Nullable public Drawable getImageDrawable(@NonNull String name) { File iconFile = getImageFile(name); if (iconFile.lastModified() >= sLastModifiedDate) { try (FileInputStream fis = new FileInputStream(iconFile)) { return Drawable.createFromStream(fis, name); } catch (IOException e) { return null; } } return null; } @NonNull private File getImageFile(@NonNull String name) { return new File(mCacheDir, name + ".png"); } public void clear() { File[] files = mCacheDir.listFiles(); if (files == null) return; int count = 0; for (File f : files) { if (f.lastModified() < sLastModifiedDate) { if (f.delete()) ++count; } } Log.d("Cache", "Deleted %d images.", count); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/self/imagecache/ImageLoader.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.self.imagecache; import android.content.pm.PackageItemInfo; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.widget.ImageView; import androidx.annotation.AnyThread; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.Px; import androidx.annotation.UiThread; import androidx.annotation.WorkerThread; import androidx.collection.LruCache; import androidx.core.content.ContextCompat; import java.io.Closeable; import java.io.IOException; import java.lang.ref.WeakReference; import java.util.Objects; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; public class ImageLoader implements Closeable { @AnyThread public static void displayImage(@Nullable PackageItemInfo info, @Nullable ImageView imageView) { WeakReference ivRef = new WeakReference<>(imageView); ThreadUtils.postOnBackgroundThread(() -> { ImageView iv = ivRef.get(); if (info == null || iv == null) { return; } Drawable drawable = info.loadIcon(iv.getContext().getPackageManager()); ThreadUtils.postOnMainThread(() -> { ImageView iv2 = ivRef.get(); if (iv2 != null) { iv2.setImageDrawable(drawable); } }); }); } public interface ImageFetcherInterface { @WorkerThread @NonNull ImageFetcherResult fetchImage(@NonNull String tag); } private static final ImageLoader sInstance = new ImageLoader(); @NonNull public static ImageLoader getInstance() { return sInstance; } private final LruCache mMemoryCache = new LruCache<>(300); private final ImageFileCache mImageFileCache = new ImageFileCache(); private boolean mIsClosed = false; private ImageLoader() { } @WorkerThread @Nullable public Bitmap getCachedImage(@NonNull String tag) { Bitmap image = mMemoryCache.get(tag); if (image != null) { return image; } // Load from file system return mImageFileCache.getImage(tag); } @UiThread public void displayImage(@NonNull String tag, @NonNull ImageView imageView, @NonNull ImageFetcherInterface imageFetcherInterface) { Bitmap image = mMemoryCache.get(tag); if (image != null) { imageView.setImageBitmap(image); } else { queueImage(tag, imageView, imageFetcherInterface); } } @UiThread public void displayImage(@NonNull String tag, @Nullable PackageItemInfo info, @NonNull ImageView imageView) { Bitmap image = mMemoryCache.get(tag); if (image != null) { imageView.setImageBitmap(image); } else { queueImage(tag, info, imageView); } } @AnyThread private void queueImage(@NonNull String tag, @Nullable PackageItemInfo info, @NonNull ImageView imageView) { queueImage(tag, imageView, new PackageInfoImageFetcher(info)); } @AnyThread private void queueImage(@NonNull String tag, @NonNull ImageView imageView, @NonNull ImageFetcherInterface imageFetcherInterface) { ImageLoaderQueueItem queueItem = new ImageLoaderQueueItem(tag, imageFetcherInterface, imageView); ThreadUtils.postOnBackgroundThread(new LoadQueueItem(queueItem)); } @Override public void close() { mIsClosed = true; mMemoryCache.evictAll(); mImageFileCache.clear(); } @Override protected void finalize() { if (!mIsClosed) { close(); } } private static class PackageInfoImageFetcher implements ImageFetcherInterface { @Nullable private final PackageItemInfo mInfo; public PackageInfoImageFetcher(@Nullable PackageItemInfo info) { mInfo = info; } @Override @NonNull public ImageFetcherResult fetchImage(@NonNull String tag) { PackageManager pm = ContextUtils.getContext().getPackageManager(); Drawable drawable = mInfo != null ? mInfo.loadIcon(pm) : null; return new ImageFetcherResult(tag, drawable != null ? UIUtils.getBitmapFromDrawable(drawable) : null, mInfo != null && tag.equals(mInfo.packageName), true, new DefaultImageDrawable("android_default_icon", pm.getDefaultActivityIcon())); } } public interface DefaultImage { @Nullable default String getTag() { return null; } @NonNull Bitmap getImage(); } public static class DefaultImageDrawableRes implements DefaultImage { @Nullable private final String mTag; @DrawableRes private final int mDrawableRes; @Px private final int mPadding; public DefaultImageDrawableRes(@Nullable String tag, int drawableRes) { this(tag, drawableRes, 0); } public DefaultImageDrawableRes(@Nullable String tag, int drawableRes, @Px int padding) { mTag = tag; mDrawableRes = drawableRes; mPadding = padding; } @Override @Nullable public String getTag() { return mTag; } @NonNull @Override public Bitmap getImage() { return UIUtils.getBitmapFromDrawable(Objects.requireNonNull(ContextCompat .getDrawable(ContextUtils.getContext(), mDrawableRes)), mPadding); } } public static class DefaultImageDrawable implements DefaultImage { @Nullable private final String mTag; @NonNull private final Drawable mDrawable; public DefaultImageDrawable(@Nullable String tag, @NonNull Drawable drawable) { mTag = tag; mDrawable = drawable; } @Override @Nullable public String getTag() { return mTag; } @NonNull @Override public Bitmap getImage() { return UIUtils.getBitmapFromDrawable(mDrawable); } } public static class DefaultImageString implements DefaultImage { @Nullable private final String mTag; @NonNull private final String mText; public DefaultImageString(@Nullable String tag, @NonNull String text) { mTag = tag; mText = text; } @Override @Nullable public String getTag() { return mTag; } @NonNull @Override public Bitmap getImage() { return UIUtils.generateBitmapFromText(mText, null); } } public static class ImageFetcherResult { @NonNull public final String tag; @Nullable public final Bitmap bitmap; public final boolean cacheInMemory; public final boolean persistCache; @NonNull public final DefaultImage defaultImage; public ImageFetcherResult(@NonNull String tag, @Nullable Bitmap bitmap, @NonNull DefaultImage defaultImage) { this(tag, bitmap, true, true, defaultImage); } public ImageFetcherResult(@NonNull String tag, @Nullable Bitmap bitmap, boolean cacheInMemory, boolean persistCache, @NonNull DefaultImage defaultImage) { this.tag = tag; this.bitmap = bitmap; this.cacheInMemory = cacheInMemory; this.persistCache = persistCache; this.defaultImage = defaultImage; } } @AnyThread public static class ImageLoaderQueueItem { public final String tag; public final WeakReference imageView; private final ImageFetcherInterface mImageFetcherInterface; public ImageLoaderQueueItem(@NonNull String tag, @NonNull ImageFetcherInterface imageFetcherInterface, @NonNull ImageView imageView) { this.tag = tag; this.imageView = new WeakReference<>(imageView); mImageFetcherInterface = imageFetcherInterface; } } private class LoadQueueItem implements Runnable { private final ImageLoaderQueueItem mQueueItem; LoadQueueItem(ImageLoaderQueueItem queueItem) { mQueueItem = queueItem; } @WorkerThread public void run() { if (imageViewReusedOrClosed(mQueueItem)) return; Bitmap image = mImageFileCache.getImage(mQueueItem.tag); if (image != null) { // Cache hit mMemoryCache.put(mQueueItem.tag, image); } else { // Cache miss ImageFetcherResult result = mQueueItem.mImageFetcherInterface.fetchImage(mQueueItem.tag); if (result.bitmap == null) { // No image produced, try default DefaultImage defaultImage = result.defaultImage; String tag = defaultImage.getTag(); if (tag == null) { // No tag listed, use the image directly image = defaultImage.getImage(); } else { // Listed a tag, try cache first image = mMemoryCache.get(tag); if (image == null) { image = mImageFileCache.getImage(tag); } if (image == null) { // Cache miss image = defaultImage.getImage(); mMemoryCache.put(tag, image); try { mImageFileCache.putImage(tag, image); } catch (IOException ignore) { } } } } else { image = result.bitmap; if (result.cacheInMemory) { mMemoryCache.put(mQueueItem.tag, image); } if (result.persistCache) { try { mImageFileCache.putImage(result.tag, image); } catch (IOException ignore) { } } } } if (imageViewReusedOrClosed(mQueueItem)) return; ThreadUtils.postOnMainThread(new LoadImageInImageView(image, mQueueItem)); } } // Used to display bitmap in the UI thread private class LoadImageInImageView implements Runnable { private final Bitmap mImage; private final ImageLoaderQueueItem mQueueItem; public LoadImageInImageView(@NonNull Bitmap image, ImageLoaderQueueItem queueItem) { mImage = image; mQueueItem = queueItem; } @UiThread public void run() { if (imageViewReusedOrClosed(mQueueItem)) return; ImageView iv = mQueueItem.imageView.get(); if (iv != null) { Object tag = iv.getTag(); if (tag == null || tag.equals(mQueueItem.tag)) { iv.setImageBitmap(mImage); } } } } @AnyThread private boolean imageViewReusedOrClosed(@NonNull ImageLoaderQueueItem imageLoaderQueueItem) { ImageView iv = imageLoaderQueueItem.imageView.get(); return mIsClosed || iv == null; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/self/life/BuildExpiryChecker.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.self.life; import android.content.DialogInterface; import android.content.Intent; import android.net.Uri; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.FragmentActivity; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Locale; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.R; public final class BuildExpiryChecker { @IntDef({BUILD_TYPE_DEBUG, BUILD_TYPE_ALPHA, BUILD_TYPE_BETA, BUILD_TYPE_RC, BUILD_TYPE_STABLE}) @Retention(RetentionPolicy.SOURCE) private @interface BuildType {} private static final int BUILD_TYPE_DEBUG = 0; private static final int BUILD_TYPE_ALPHA = 1; private static final int BUILD_TYPE_BETA = 2; private static final int BUILD_TYPE_RC = 3; private static final int BUILD_TYPE_STABLE = 4; private static final long[] TIME_SPAN_MILLIS = new long[]{ 2L * 30 * 24 * 60 * 60_000, // 2 months 6L * 30 * 24 * 60 * 60_000, // 6 months 6L * 30 * 24 * 60 * 60_000, // 6 months 6L * 30 * 24 * 60 * 60_000, // 6 months 18L * 30 * 24 * 60 * 60_000, // 18 months }; private static final long[] WARNING_PERIOD_MILLIS = new long[]{ 14L * 24 * 60 * 60_000, // 2 weeks 30L * 24 * 60 * 60_000, // 1 month 30L * 24 * 60 * 60_000, // 1 month 30L * 24 * 60 * 60_000, // 1 month 3L * 30 * 24 * 60 * 60_000, // 3 months }; @NonNull public static AlertDialog getBuildExpiredDialog(@NonNull FragmentActivity activity, @Nullable DialogInterface.OnClickListener continueClickListener) { MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(activity) .setTitle(R.string.app_manager_build_expired) .setMessage(R.string.app_manager_build_expired_message) .setCancelable(false) .setPositiveButton(R.string.update, (dialog, which) -> { Intent intent = new Intent(Intent.ACTION_VIEW); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setData(getUpdateUri()); activity.startActivity(intent); activity.finishAndRemoveTask(); }) .setNeutralButton(R.string.uninstall, (dialog, which) -> { Intent intent = new Intent(Intent.ACTION_DELETE); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setData(Uri.parse("package:" + BuildConfig.APPLICATION_ID)); activity.startActivity(intent); activity.finishAndRemoveTask(); }); if (getBuildType() == BUILD_TYPE_STABLE) { builder.setNeutralButton(R.string.action_continue, continueClickListener); } return builder.create(); } @Nullable public static Boolean buildExpired() { int buildType = getBuildType(); long timeSpan = getCurrentTime() - getBuildTime(); long realTimeSpan = TIME_SPAN_MILLIS[buildType]; if (timeSpan <= realTimeSpan) { // Build hasn't yet expired return false; } // Build has expired long warningPeriod = WARNING_PERIOD_MILLIS[buildType]; if (timeSpan <= realTimeSpan + warningPeriod) { // Build has expired but in warning period return null; } // Build has completely expired and should stop working return true; } private static long getCurrentTime() { // Ideally, we should fetch this from an SNTP server such as time.android.com return System.currentTimeMillis(); } private static long getBuildTime() { return BuildConfig.BUILD_TIME_MILLIS; } private static Uri getUpdateUri() { if (getBuildType() == BUILD_TYPE_DEBUG) { return Uri.parse("https://github.com/MuntashirAkon/AppManager/actions/workflows/debug_build.yml"); } // TODO: 3/12/22 For Stable builds, check F-Droid too return Uri.parse("https://github.com/MuntashirAkon/AppManager/releases"); } @BuildType private static int getBuildType() { if (BuildConfig.DEBUG) { return BUILD_TYPE_DEBUG; } String[] versionParts = BuildConfig.VERSION_NAME.split("-"); if (versionParts.length == 1) { return BUILD_TYPE_STABLE; } String lastPart = versionParts[versionParts.length - 1]; switch (lastPart.substring(0, lastPart.length() - 2).toLowerCase(Locale.ROOT)) { case "alpha": return BUILD_TYPE_ALPHA; case "beta": return BUILD_TYPE_BETA; case "rc": return BUILD_TYPE_RC; default: throw new IllegalStateException("Invalid App Manager version"); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/self/life/FundingCampaignChecker.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.self.life; public class FundingCampaignChecker { private static final long FUNDING_CAMPAIGN_START = 1703160000000L; private static final long FUNDING_CAMPAIGN_END = 1717200000000L; public static boolean campaignRunning() { long currentTime = System.currentTimeMillis(); return currentTime >= FUNDING_CAMPAIGN_START && currentTime <= FUNDING_CAMPAIGN_END; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/self/pref/TipsPrefs.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.self.pref; import androidx.annotation.NonNull; import io.github.muntashirakon.AppManager.utils.AppPref; public class TipsPrefs { private static final int TIPS_TAB_APP_OPS = 1 << 0; private static final int TIPS_TAB_USES_PERMISSIONS = 1 << 1; private static final int TIPS_TAB_PERMISSIONS = 1 << 2; private static final int TIPS_TAB_OVERLAYS = 1 << 3; private static TipsPrefs sInstance; @NonNull public static TipsPrefs getInstance() { if (sInstance != null) { return sInstance; } return sInstance = new TipsPrefs(); } private int mFlags; private TipsPrefs() { mFlags = AppPref.getInt(AppPref.PrefKey.PREF_TIPS_PREFS_INT); } public boolean displayInAppOpsTab() { return (mFlags & TIPS_TAB_APP_OPS) != 0; } public void setDisplayInAppOpsTab(boolean display) { save(TIPS_TAB_APP_OPS, display); } public boolean displayInUsesPermissionsTab() { return (mFlags & TIPS_TAB_USES_PERMISSIONS) != 0; } public void setDisplayInUsesPermissionsTab(boolean display) { save(TIPS_TAB_USES_PERMISSIONS, display); } public boolean displayInPermissionsTab() { return (mFlags & TIPS_TAB_PERMISSIONS) != 0; } public void setDisplayInPermissionsTab(boolean display) { save(TIPS_TAB_PERMISSIONS, display); } public boolean displayInOverlaysTab() { return (mFlags & TIPS_TAB_USES_PERMISSIONS) != 0; } public void setDisplayInOverlaysTab(boolean display) { save(TIPS_TAB_OVERLAYS, display); } private void save(int flag, boolean display) { if (display) mFlags |= flag; else mFlags &= ~flag; AppPref.set(AppPref.PrefKey.PREF_TIPS_PREFS_INT, mFlags); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/servermanager/AssetsUtils.java ================================================ // SPDX-License-Identifier: MIT AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.servermanager; import android.content.Context; import android.content.res.AssetFileDescriptor; import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; import org.jetbrains.annotations.NotNull; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; import java.io.InputStreamReader; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.server.common.ConfigParams; import io.github.muntashirakon.AppManager.server.common.Constants; import io.github.muntashirakon.io.IoUtils; // Copyright 2016 Zheng Li @SuppressWarnings("ResultOfMethodCallIgnored") class AssetsUtils { @WorkerThread public static void copyFile(@NonNull Context context, String fileName, File destFile, boolean force) throws IOException { try (AssetFileDescriptor openFd = context.getAssets().openFd(fileName)) { if (force) { destFile.delete(); } else { if (destFile.exists()) { if (destFile.length() != openFd.getLength()) { destFile.delete(); } else { return; } } } try (FileInputStream open = openFd.createInputStream(); FileOutputStream fos = new FileOutputStream(destFile)) { byte[] buff = new byte[IoUtils.DEFAULT_BUFFER_SIZE]; int len; while ((len = open.read(buff)) != -1) { fos.write(buff, 0, len); } fos.flush(); fos.getFD().sync(); } } } @WorkerThread static void writeServerExecScript(@NonNull Context context, @NonNull File destFile, @NonNull String classPath) throws IOException { try (AssetFileDescriptor openFd = context.getAssets().openFd(ServerConfig.SERVER_RUNNER_EXEC_NAME); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(openFd.createInputStream()))) { if (destFile.exists()) { destFile.delete(); } try (BufferedWriter bw = new BufferedWriter(new FileWriter(destFile, false))) { // Set variables StringBuilder script = new StringBuilder(); script.append("SERVER_NAME=").append(Constants.SERVER_NAME).append("\n") .append("JAR_NAME=").append(Constants.JAR_NAME).append("\n") .append("JAR_PATH=").append(classPath).append("\n") .append("ARGS=").append(getServerArgs()).append("\n"); String line; while ((line = bufferedReader.readLine()) != null) { String wl; if ("%ENV_VARS%".equals(line.trim())) { wl = script.toString(); } else wl = line; bw.write(wl); bw.newLine(); } bw.flush(); } } } @NotNull private static String getServerArgs() { StringBuilder argsBuilder = new StringBuilder(); argsBuilder.append(',').append(ConfigParams.PARAM_APP).append(':').append(BuildConfig.APPLICATION_ID); if (ServerConfig.getAllowBgRunning()) { argsBuilder.append(',').append(ConfigParams.PARAM_RUN_IN_BACKGROUND).append(':').append(1); } if (BuildConfig.DEBUG) { argsBuilder.append(',').append(ConfigParams.PARAM_DEBUG).append(':').append(1); } return argsBuilder.toString(); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/servermanager/LocalServer.java ================================================ // SPDX-License-Identifier: MIT AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.servermanager; import android.annotation.SuppressLint; import android.content.Context; import androidx.annotation.AnyThread; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import java.io.IOException; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.SocketTimeoutException; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.misc.NoOps; import io.github.muntashirakon.AppManager.server.common.Caller; import io.github.muntashirakon.AppManager.server.common.CallerResult; import io.github.muntashirakon.AppManager.server.common.Shell; import io.github.muntashirakon.AppManager.server.common.ShellCaller; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.adb.AdbPairingRequiredException; // Copyright 2016 Zheng Li public class LocalServer { @GuardedBy("lockObject") private static final Object sLock = new Object(); @SuppressLint("StaticFieldLeak") @Nullable private static LocalServer sLocalServer; @GuardedBy("lockObject") @WorkerThread @NoOps(used = true) public static LocalServer getInstance() throws IOException, AdbPairingRequiredException { // Non-null check must be done outside the synchronised block to prevent deadlock on ADB over TCP mode. if (sLocalServer != null) return sLocalServer; synchronized (sLock) { try { Log.d("IPC", "Init: Local server"); sLocalServer = new LocalServer(); } finally { sLock.notifyAll(); } } return sLocalServer; } public static void die() { synchronized (sLock) { try { if (sLocalServer != null) { sLocalServer.destroy(); } } finally { sLocalServer = null; } } } @WorkerThread @NoOps public static boolean alive(Context context) { try (ServerSocket socket = new ServerSocket()) { socket.bind(new InetSocketAddress(ServerConfig.getLocalServerHost(context), ServerConfig.getLocalServerPort()), 1); return false; } catch (IOException e) { return true; } } @NonNull private final Context mContext; @NonNull private final LocalServerManager mLocalServerManager; @WorkerThread @NoOps(used = true) private LocalServer() throws IOException, AdbPairingRequiredException { mContext = ContextUtils.getDeContext(ContextUtils.getContext()); mLocalServerManager = LocalServerManager.getInstance(mContext); // Initialise necessary files and permissions ServerConfig.init(mContext); // Start server if not already checkConnect(); } private final Object mConnectLock = new Object(); private boolean mConnectStarted = false; @GuardedBy("connectLock") @WorkerThread @NoOps(used = true) public void checkConnect() throws IOException, AdbPairingRequiredException { synchronized (mConnectLock) { if (mConnectStarted) { try { mConnectLock.wait(); } catch (InterruptedException e) { return; } } mConnectStarted = true; try { mLocalServerManager.start(); } catch (IOException | AdbPairingRequiredException e) { mConnectStarted = false; mConnectLock.notify(); throw e; } mConnectStarted = false; mConnectLock.notify(); } } public Shell.Result runCommand(String command) throws IOException { ShellCaller shellCaller = new ShellCaller(command); CallerResult callerResult = exec(shellCaller); Throwable th = callerResult.getThrowable(); if (th != null) { throw new IOException(th); } return (Shell.Result) callerResult.getReplyObj(); } @WorkerThread public CallerResult exec(Caller caller) throws IOException { try { checkConnect(); return mLocalServerManager.execNew(caller); } catch (SocketTimeoutException e) { e.printStackTrace(); closeBgServer(); // Retry try { checkConnect(); return mLocalServerManager.execNew(caller); } catch (AdbPairingRequiredException e2) { throw new IOException(e2); } } catch (AdbPairingRequiredException e) { throw new IOException(e); } } @AnyThread public boolean isRunning() { return mLocalServerManager.isRunning(); } public void destroy() { mLocalServerManager.stop(); } @WorkerThread public void closeBgServer() throws IOException { mLocalServerManager.closeBgServer(); mLocalServerManager.stop(); } @WorkerThread @NoOps(used = true) public static void restart() throws IOException, AdbPairingRequiredException { if (sLocalServer != null) { LocalServerManager manager = sLocalServer.mLocalServerManager; manager.closeBgServer(); manager.stop(); manager.start(); } else { getInstance(); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/servermanager/LocalServerManager.java ================================================ // SPDX-License-Identifier: MIT AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.servermanager; import android.annotation.SuppressLint; import android.content.Context; import android.os.SystemClock; import androidx.annotation.AnyThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.Socket; import java.util.Objects; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import io.github.muntashirakon.AppManager.adb.AdbConnectionManager; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.misc.NoOps; import io.github.muntashirakon.AppManager.runner.Runner; import io.github.muntashirakon.AppManager.server.common.BaseCaller; import io.github.muntashirakon.AppManager.server.common.Caller; import io.github.muntashirakon.AppManager.server.common.CallerResult; import io.github.muntashirakon.AppManager.server.common.Constants; import io.github.muntashirakon.AppManager.server.common.DataTransmission; import io.github.muntashirakon.AppManager.server.common.ParcelableUtil; import io.github.muntashirakon.AppManager.settings.Ops; import io.github.muntashirakon.adb.AdbPairingRequiredException; import io.github.muntashirakon.adb.AdbStream; import io.github.muntashirakon.io.IoUtils; // Copyright 2016 Zheng Li class LocalServerManager { private static final String TAG = "LocalServerManager"; @SuppressLint("StaticFieldLeak") private static LocalServerManager sLocalServerManager; @AnyThread @NoOps @NonNull static LocalServerManager getInstance(@NonNull Context context) { synchronized (LocalServerManager.class) { if (sLocalServerManager == null) { sLocalServerManager = new LocalServerManager(context); } } return sLocalServerManager; } private final Object mLock = new Object(); @NonNull private final Context mContext; @Nullable private ClientSession mSession; @AnyThread private LocalServerManager(@NonNull Context context) { mContext = context; } /** * Get current session. If no session is running, create a new one. If no server is running, * create one first. * * @return Currently running session * @throws IOException When creating session fails or server couldn't be started */ @WorkerThread @NonNull @NoOps(used = true) private ClientSession getSession() throws IOException, AdbPairingRequiredException { synchronized (mLock) { if (mSession == null || !mSession.isRunning()) { try { mSession = createSession(); } catch (Exception e) { if (!Ops.isDirectRoot() && !Ops.isAdb()) { // Do not bother attempting to create a new session throw new IOException("Could not create session", e); } } if (mSession == null) { try { startServer(); } catch (AdbPairingRequiredException e) { throw e; } catch (Exception e) { throw new IOException("Could not start server", e); } mSession = createSession(); } } return mSession; } } @AnyThread public boolean isRunning() { return mSession != null && mSession.isRunning(); } /** * Close client session */ @AnyThread void closeSession() { IoUtils.closeQuietly(mSession); mSession = null; } /** * Stop ADB and then close client session */ void stop() { IoUtils.closeQuietly(mAdbStream); IoUtils.closeQuietly(mSession); mAdbStream = null; mSession = null; } @WorkerThread @NoOps(used = true) void start() throws IOException, AdbPairingRequiredException { getSession(); } @WorkerThread @NonNull private DataTransmission getSessionDataTransmission() throws IOException { try { return getSession().getDataTransmission(); } catch (AdbPairingRequiredException e) { throw new IOException(e); } } @WorkerThread @NonNull private byte[] execPre(@NonNull byte[] params) throws IOException { try { return getSessionDataTransmission().sendAndReceiveMessage(params); } catch (IOException e) { if (e.getMessage() != null && e.getMessage().contains("pipe")) { closeSession(); return getSessionDataTransmission().sendAndReceiveMessage(params); } throw e; } } @WorkerThread CallerResult execNew(@NonNull Caller caller) throws IOException { byte[] result = execPre(ParcelableUtil.marshall(new BaseCaller(caller.wrapParameters()))); return ParcelableUtil.unmarshall(result, CallerResult.CREATOR); } @WorkerThread void closeBgServer() throws IOException { try { BaseCaller baseCaller = new BaseCaller(BaseCaller.TYPE_CLOSE); getSession().getDataTransmission().sendAndReceiveMessage(ParcelableUtil.marshall(baseCaller)); } catch (Exception e) { // Since the server is closed abruptly, this should always produce error Log.w(TAG, "closeBgServer: Error", e); } // Check if the server is still active if (LocalServer.alive(mContext)) { // Server still active, need to run killall am_local_server try { stopServer(); } catch (Exception e) { throw new IOException(e); } } } @Nullable private volatile AdbStream mAdbStream; private volatile CountDownLatch mAdbConnectionWatcher = new CountDownLatch(1); private volatile boolean mAdbServerStarted; private final Runnable mAdbOutputThread = () -> { try (BufferedReader reader = new BufferedReader(new InputStreamReader(Objects.requireNonNull(mAdbStream).openInputStream()))) { String s; while ((s = reader.readLine()) != null) { Log.d(TAG, "RESPONSE: %s", s); if (s.startsWith("Success!")) { mAdbServerStarted = true; mAdbConnectionWatcher.countDown(); break; } else if (s.startsWith("Error!")) { mAdbServerStarted = false; mAdbConnectionWatcher.countDown(); break; } } } catch (Throwable e) { Log.e(TAG, "useAdbStartServer: unable to read from shell.", e); } }; @WorkerThread private void useAdbStartServer() throws Exception { if (mAdbStream == null || Objects.requireNonNull(mAdbStream).isClosed()) { // ADB shell not running String adbHost = ServerConfig.getAdbHost(mContext); int adbPort = ServerConfig.getAdbPort(); AdbConnectionManager manager = AdbConnectionManager.getInstance(); Log.d(TAG, "useAdbStartServer: Connecting using host=%s, port=%d", adbHost, adbPort); manager.setTimeout(10, TimeUnit.SECONDS); if (!manager.isConnected() && !manager.connect(adbHost, adbPort)) { throw new IOException("Could not connect to ADB."); } Log.d(TAG, "useAdbStartServer: Opening shell..."); mAdbStream = manager.openStream("shell:"); mAdbConnectionWatcher = new CountDownLatch(1); mAdbServerStarted = false; new Thread(mAdbOutputThread).start(); } Log.d(TAG, "useAdbStartServer: Shell opened."); try (OutputStream os = Objects.requireNonNull(mAdbStream).openOutputStream()) { os.write("id\n".getBytes()); // ADB may require a fallback method String command = ServerConfig.getServerRunnerAdbCommand(); Log.d(TAG, "useAdbStartServer: %s", command); os.write((command + "\n").getBytes()); } if (!mAdbConnectionWatcher.await(1, TimeUnit.MINUTES) || !mAdbServerStarted) { throw new Exception("Server wasn't started."); } Log.d(TAG, "useAdbStartServer: Server has started."); } @WorkerThread private void useRootStartServer() throws Exception { if (!Ops.hasRoot()) { throw new Exception("Root access denied"); } String command = ServerConfig.getServerRunnerCommand(0); // + "\n" + "supolicy --live 'allow qti_init_shell zygote_exec file execute'"; Log.d(TAG, "useRootStartServer: %s", command); Runner.Result result = Runner.runCommand(command); Log.d(TAG, "useRootStartServer: %s", result.getOutput()); if (!result.isSuccessful()) { throw new Exception("Could not start server."); } SystemClock.sleep(3000); Log.e(TAG, "useRootStartServer: Server has started."); } /** * Start root or ADB server based on config */ @WorkerThread @NoOps(used = true) private void startServer() throws Exception { if (Ops.isAdb()) { useAdbStartServer(); } else if (Ops.isDirectRoot()) { useRootStartServer(); } else throw new Exception("Neither root nor ADB mode is enabled."); } /** * Stop root or ADB server based on config */ @WorkerThread @NoOps(used = true) private void stopServer() throws Exception { String command = "killall " + Constants.SERVER_NAME; if (Ops.isAdb()) { if (mAdbStream == null || Objects.requireNonNull(mAdbStream).isClosed()) { // ADB shell not running String adbHost = ServerConfig.getAdbHost(mContext); int adbPort = ServerConfig.getAdbPort(); AdbConnectionManager manager = AdbConnectionManager.getInstance(); Log.d(TAG, "stopServer (ADB): Connecting using host=%s, port=%d", adbHost, adbPort); manager.setTimeout(10, TimeUnit.SECONDS); if (!manager.isConnected() && !manager.connect(adbHost, adbPort)) { throw new IOException("Could not connect to ADB."); } Log.d(TAG, "stopServer (ADB): Opening shell..."); mAdbStream = manager.openStream("shell:"); mAdbConnectionWatcher = new CountDownLatch(1); mAdbServerStarted = false; new Thread(mAdbOutputThread).start(); } Log.d(TAG, "stopServer (ADB): Shell opened."); try (OutputStream os = Objects.requireNonNull(mAdbStream).openOutputStream()) { os.write("id\n".getBytes()); Log.d(TAG, "stopServer (ADB): %s", command); os.write((command + "\n").getBytes()); } if (!mAdbConnectionWatcher.await(1, TimeUnit.MINUTES) || !mAdbServerStarted) { throw new Exception("Server wasn't stopped."); } Log.d(TAG, "useAdbStartServer: Server has stopped."); } else if (Ops.isDirectRoot()) { if (!Ops.hasRoot()) { throw new Exception("Root access denied"); } Log.d(TAG, "stopServer (root): %s", command); Runner.Result result = Runner.runCommand(command); Log.d(TAG, "stopServer (root): %s", result.getOutput()); if (!result.isSuccessful()) { throw new Exception("Could not start server."); } SystemClock.sleep(3000); Log.e(TAG, "useRootStartServer: Server has started."); } else throw new Exception("Neither root nor ADB mode is enabled."); } /** * Create a client session * * @return New session if not running, running session otherwise * @throws IOException If session creation failed */ @WorkerThread @NonNull @NoOps(used = true) private ClientSession createSession() throws IOException { if (isRunning()) { // Non-null check has already been done return Objects.requireNonNull(mSession); } String host = ServerConfig.getLocalServerHost(mContext); int port = ServerConfig.getLocalServerPort(); Socket socket = new Socket(host, port); socket.setSoTimeout(30_000); // NOTE: (CWE-319) No need for SSL since it only runs on a random port in localhost with specific authorization. // TODO: 5/8/23 We could use an SSL server with a randomly generated certificate per session without requiring // any other authorization methods. This session is independent of the application. OutputStream os = socket.getOutputStream(); InputStream is = socket.getInputStream(); DataTransmission transfer = new DataTransmission(os, is, false); transfer.shakeHands(ServerConfig.getLocalToken(), DataTransmission.Role.Client); return new ClientSession(socket, transfer); } /** * The client session handler */ private static class ClientSession implements AutoCloseable { private volatile boolean mIsRunning; @NonNull private final Socket mSocket; @NonNull private final DataTransmission mDataTransmission; @AnyThread ClientSession(@NonNull Socket socket, @NonNull DataTransmission dataTransmission) { mSocket = socket; mDataTransmission = dataTransmission; mIsRunning = true; } /** * Close the session, stop any active transmission */ @AnyThread @Override public void close() throws IOException { if (mIsRunning) { mIsRunning = false; mDataTransmission.close(); mSocket.close(); } } /** * Whether the client session is running */ @AnyThread boolean isRunning() { return mIsRunning; } @AnyThread @NonNull DataTransmission getDataTransmission() { return mDataTransmission; } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/servermanager/ServerConfig.java ================================================ // SPDX-License-Identifier: MIT AND GPL-3.0-or-later package io.github.muntashirakon.AppManager.servermanager; import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; import android.os.Build; import android.provider.Settings; import android.text.TextUtils; import androidx.annotation.AnyThread; import androidx.annotation.IntRange; import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; import java.io.File; import java.io.IOException; import java.net.Inet4Address; import java.security.SecureRandom; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.misc.NoOps; import io.github.muntashirakon.AppManager.server.common.Constants; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.FileUtils; // Copyright 2016 Zheng Li public final class ServerConfig { public static final String TAG = ServerConfig.class.getSimpleName(); public static final int DEFAULT_ADB_PORT = 5555; static final String SERVER_RUNNER_EXEC_NAME = "run_server.sh"; private static final String LOCAL_TOKEN = "l_token"; private static final File[] SERVER_RUNNER_EXEC = new File[2]; private static final File[] SERVER_RUNNER_JAR = new File[2]; private static final SharedPreferences sPreferences = ContextUtils.getContext() .getSharedPreferences("server_config", Context.MODE_PRIVATE); private static volatile boolean sInitialised = false; @WorkerThread @NoOps public static void init(@NonNull Context context) throws IOException { if (sInitialised) { return; } // Setup paths File externalCachePath = FileUtils.getExternalCachePath(context); File externalMediaPath = FileUtils.getExternalMediaPath(context); File deStorage = ContextUtils.getDeContext(context).getCacheDir(); SERVER_RUNNER_EXEC[0] = new File(externalCachePath, SERVER_RUNNER_EXEC_NAME); SERVER_RUNNER_EXEC[1] = new File(deStorage, SERVER_RUNNER_EXEC_NAME); SERVER_RUNNER_JAR[0] = new File(externalCachePath, Constants.JAR_NAME); SERVER_RUNNER_JAR[1] = new File(deStorage, Constants.JAR_NAME); // Copy JAR boolean force = BuildConfig.DEBUG; AssetsUtils.copyFile(context, Constants.JAR_NAME, SERVER_RUNNER_JAR[0], force); AssetsUtils.copyFile(context, Constants.JAR_NAME, SERVER_RUNNER_JAR[1], force); // Write script AssetsUtils.writeServerExecScript(context, SERVER_RUNNER_EXEC[0], SERVER_RUNNER_JAR[0].getAbsolutePath()); AssetsUtils.writeServerExecScript(context, SERVER_RUNNER_EXEC[1], SERVER_RUNNER_JAR[1].getAbsolutePath()); // Update permission FileUtils.chmod711(deStorage); FileUtils.chmod644(SERVER_RUNNER_JAR[1]); FileUtils.chmod644(SERVER_RUNNER_EXEC[1]); sInitialised = true; } @AnyThread @NonNull public static File getDestJarFile() { // For compatibility only return SERVER_RUNNER_JAR[0]; } @AnyThread @NonNull public static String getServerRunnerCommand(int index) throws IndexOutOfBoundsException { Log.e(TAG, "Classpath: %s", SERVER_RUNNER_JAR[index]); Log.e(TAG, "Exec path: %s", SERVER_RUNNER_EXEC[index]); return "sh " + SERVER_RUNNER_EXEC[index] + " " + getLocalServerPort() + " " + getLocalToken(); } @AnyThread @NonNull public static String getServerRunnerAdbCommand() throws IndexOutOfBoundsException { return getServerRunnerCommand(0) + " || " + getServerRunnerCommand(1); } /** * Get existing or generate new 16-digit token for client session * * @return Existing or new token */ @AnyThread @NonNull public static String getLocalToken() { String token = sPreferences.getString(LOCAL_TOKEN, null); if (TextUtils.isEmpty(token)) { token = generateToken(); sPreferences.edit().putString(LOCAL_TOKEN, token).apply(); } return token; } @AnyThread public static boolean getAllowBgRunning() { return sPreferences.getBoolean("allow_bg_running", true); } @AnyThread @IntRange(from = 0, to = 65535) @NoOps public static int getAdbPort() { return sPreferences.getInt("adb_port", DEFAULT_ADB_PORT); } @AnyThread @NoOps public static void setAdbPort(@IntRange(from = 0, to = 65535) int port) { sPreferences.edit().putInt("adb_port", port).apply(); } @AnyThread public static int getLocalServerPort() { return Prefs.Misc.getAdbLocalServerPort(); } @WorkerThread @NonNull public static String getAdbHost(Context context) { return getHostIpAddress(context); } @WorkerThread @NonNull public static String getLocalServerHost(Context context) { String ipAddress = Inet4Address.getLoopbackAddress().getHostAddress(); if (ipAddress == null || ipAddress.equals("::1")) return "127.0.0.1"; return ipAddress; } @WorkerThread @NonNull private static String getHostIpAddress(@NonNull Context context) { if (isEmulator(context)) return "10.0.2.2"; String ipAddress = Inet4Address.getLoopbackAddress().getHostAddress(); if (ipAddress == null || ipAddress.equals("::1")) return "127.0.0.1"; return ipAddress; } // https://github.com/firebase/firebase-android-sdk/blob/7d86138304a6573cbe2c61b66b247e930fa05767/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CommonUtils.java#L402 private static final String GOLDFISH = "goldfish"; private static final String RANCHU = "ranchu"; private static final String SDK = "sdk"; private static boolean isEmulator(@NonNull Context context) { @SuppressLint("HardwareIds") String androidId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); return Build.PRODUCT.contains(SDK) || Build.HARDWARE.contains(GOLDFISH) || Build.HARDWARE.contains(RANCHU) || androidId == null; } @AnyThread @NonNull private static String generateToken() { Context context = ContextUtils.getContext(); String[] wordList = context.getResources().getStringArray(R.array.word_list); SecureRandom secureRandom = new SecureRandom(); String[] tokenItems = new String[3 + secureRandom.nextInt(3)]; for (int i = 0; i < tokenItems.length; ++i) { tokenItems[i] = wordList[secureRandom.nextInt(wordList.length)]; } return TextUtils.join("-", tokenItems); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/servermanager/ServerStatusChangeReceiver.java ================================================ // SPDX-License-Identifier: MIT package io.github.muntashirakon.AppManager.servermanager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.os.Process; import android.os.RemoteException; import android.os.SystemClock; import androidx.annotation.AnyThread; import androidx.annotation.NonNull; import java.io.IOException; import io.github.muntashirakon.AppManager.ipc.LocalServices; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.server.common.ConfigParams; import io.github.muntashirakon.AppManager.server.common.ServerActions; import io.github.muntashirakon.AppManager.settings.Ops; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.adb.AdbPairingRequiredException; // Copyright 2016 Zheng Li public class ServerStatusChangeReceiver extends BroadcastReceiver { private static final String TAG = ServerStatusChangeReceiver.class.getSimpleName(); @Override public void onReceive(Context context, @NonNull Intent intent) { String action = intent.getAction(); if (action == null) { return; } // Verify token before doing action String token = intent.getStringExtra(ConfigParams.PARAM_TOKEN); if (!ServerConfig.getLocalToken().equals(token)) { Log.d(TAG, "Mismatch token. Expected: %s, Received: %s", ServerConfig.getLocalToken(), token); return; } String uidString = intent.getStringExtra(ConfigParams.PARAM_UID); if (uidString == null) { Log.w(TAG, "No UID received from the server."); return; } Log.d(TAG, "onReceive --> %s %s", action, uidString); int uid = Integer.parseInt(uidString); switch (action) { case ServerActions.ACTION_SERVER_STARTED: // Server was started for the first time Ops.setWorkingUid(uid); startServerIfNotAlready(context); // TODO: 8/4/24 Need to broadcast this message to update UI and/or trigger development break; case ServerActions.ACTION_SERVER_STOPPED: // Server was stopped LocalServer.die(); Ops.setWorkingUid(Process.myUid()); break; case ServerActions.ACTION_SERVER_CONNECTED: // Server was connected with App Manager Ops.setWorkingUid(uid); break; case ServerActions.ACTION_SERVER_DISCONNECTED: // Exited from App Manager Ops.setWorkingUid(Process.myUid()); break; } } @AnyThread private void startServerIfNotAlready(@NonNull Context context) { ThreadUtils.postOnBackgroundThread(() -> { try { while (!LocalServer.alive(context)) { // Server isn't yet in listening mode Log.w(TAG, "Waiting for server..."); SystemClock.sleep(100); } LocalServer.getInstance(); LocalServices.bindServicesIfNotAlready(); } catch (IOException | AdbPairingRequiredException e) { Log.w(TAG, "Failed to start server", e); } catch (RemoteException e) { Log.w(TAG, "Failed to start services", e); } }); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/servermanager/WifiWaitService.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.servermanager; import android.app.Notification; import android.app.Service; import android.content.Context; import android.content.Intent; import android.net.ConnectivityManager; import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkRequest; import android.os.Build; import android.os.IBinder; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.WorkerThread; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.adb.AdbUtils; import io.github.muntashirakon.AppManager.settings.Ops; import io.github.muntashirakon.AppManager.types.ForegroundService; import io.github.muntashirakon.AppManager.utils.NotificationUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.Utils; @RequiresApi(Build.VERSION_CODES.R) public class WifiWaitService extends Service { private static final String TAG = WifiWaitService.class.getSimpleName(); public static final String CHANNEL_ID = BuildConfig.APPLICATION_ID + ".channel.WIFI_WAIT_SERVICE"; private final ConnectivityManager.NetworkCallback mNetworkCallback = new ConnectivityManager.NetworkCallback() { @Override public void onAvailable(@NonNull Network network) { Log.d(TAG, "Wi-Fi network available"); } @Override public void onLost(@NonNull Network network) { Log.d(TAG, "Network lost"); } @Override public void onCapabilitiesChanged(@NonNull Network network, @NonNull NetworkCapabilities networkCapabilities) { // Double-check Wi-Fi availability when capabilities change if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) && !mAutoconnectCompleted) { connectAdbWifi(); unregisterNetworkCallback(); } } }; private ConnectivityManager mConnectivityManager; private boolean mAutoconnectCompleted = false; private boolean mUnregisterDone = true; @Override public void onCreate() { super.onCreate(); NotificationUtils.getNewNotificationManager(this, CHANNEL_ID, "Wi-Fi Wait Service", NotificationManagerCompat.IMPORTANCE_LOW); mConnectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); } @Override public int onStartCommand(Intent intent, int flags, int startId) { Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle(getString(R.string.waiting_for_wifi)) .setSmallIcon(android.R.drawable.ic_dialog_info) .setPriority(NotificationCompat.PRIORITY_LOW) .setOngoing(true) .build(); ForegroundService.start(this, NotificationUtils.nextNotificationId(null), notification, ForegroundService.FOREGROUND_SERVICE_TYPE_DATA_SYNC | ForegroundService.FOREGROUND_SERVICE_TYPE_SPECIAL_USE); if (LocalServer.alive(getApplicationContext())) { // Already connected mAutoconnectCompleted = true; } if (!mAutoconnectCompleted) { registerNetworkCallback(); } else { stopSelf(); } return START_NOT_STICKY; // Don't restart if killed } private void registerNetworkCallback() { NetworkRequest networkRequest = new NetworkRequest.Builder() .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) .build(); try { mConnectivityManager.registerNetworkCallback(networkRequest, mNetworkCallback); mUnregisterDone = false; Log.d(TAG, "Network callback registered"); } catch (Exception e) { Log.e(TAG, "Failed to register network callback", e); stopSelf(); } } private void connectAdbWifi() { if (mAutoconnectCompleted) { return; // Prevent multiple executions } mAutoconnectCompleted = true; ThreadUtils.postOnBackgroundThread(() -> { try { doConnectAdbWifi(); } finally { stopSelf(); } }); } @WorkerThread private void doConnectAdbWifi() { Context context = getApplicationContext(); if (!Utils.isWifiActive(context)) { Log.w(TAG, "Autoconnect failed: Wi-Fi not enabled."); return; } if (!AdbUtils.enableWirelessDebugging(context)) { Log.w(TAG, "Autoconnect failed: Could not enable wireless debugging."); return; } int status = Ops.autoConnectWirelessDebugging(context); if (status == Ops.STATUS_ADB_PAIRING_REQUIRED) { Log.w(TAG, "Autoconnect failed: pairing required"); } else if (status == Ops.STATUS_WIRELESS_DEBUGGING_CHOOSER_REQUIRED) { Log.w(TAG, "Autoconnect failed: could not find a valid port"); } else if (status == Ops.STATUS_FAILURE_ADB_NEED_MORE_PERMS) { Log.w(TAG, "Autoconnect failed: not enough permissions available"); } else if (status == Ops.STATUS_SUCCESS) { Log.i(TAG, "Autoconnect success!"); } else { Log.w(TAG, "Autoconnect failed"); } } private void unregisterNetworkCallback() { if (mUnregisterDone) { return; } mUnregisterDone = true; try { mConnectivityManager.unregisterNetworkCallback(mNetworkCallback); Log.d(TAG, "Network callback unregistered"); } catch (Exception e) { Log.e(TAG, "Error unregistering callback", e); } } @Nullable @Override public IBinder onBind(Intent intent) { return null; } @Override public void onDestroy() { unregisterNetworkCallback(); super.onDestroy(); Log.d(TAG, "Service destroyed"); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/session/SessionMonitoringService.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.session; import android.app.PendingIntent; import android.app.Service; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.IBinder; import android.os.Process; import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import androidx.core.app.PendingIntentCompat; import androidx.core.app.ServiceCompat; import androidx.core.content.ContextCompat; import java.util.concurrent.Future; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.DummyActivity; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.misc.ScreenLockChecker; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.types.ForegroundService; import io.github.muntashirakon.AppManager.utils.NotificationUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; public class SessionMonitoringService extends Service { public static final String TAG = SessionMonitoringService.class.getSimpleName(); public static final String CHANNEL_ID = BuildConfig.APPLICATION_ID + ".channel.SESSION_MONITOR"; public static final String STOP_ACTION = BuildConfig.APPLICATION_ID + ".action.STOP_SESSION_MONITOR"; private boolean mIsWorking; @Nullable private Future mCheckLockResult; private ScreenLockChecker mScreenLockChecker; private boolean mScreenLockedReceiverRegistered = false; private final BroadcastReceiver mScreenLockedReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { try { if (mCheckLockResult != null) { mCheckLockResult.cancel(true); } mCheckLockResult = ThreadUtils.postOnBackgroundThread(() -> { if (mScreenLockChecker == null) { mScreenLockChecker = new ScreenLockChecker(SessionMonitoringService.this, () -> lockScreen()); } mScreenLockChecker.checkLock(); }); } catch (Throwable th) { th.printStackTrace(); } } }; public SessionMonitoringService() { } @Override public int onStartCommand(Intent intent, int flags, int startId) { if (intent != null && STOP_ACTION.equals(intent.getAction())) { lockScreen(); return START_NOT_STICKY; } if (mIsWorking) { return START_NOT_STICKY; } mIsWorking = true; boolean screenLockEnabled = Prefs.Privacy.isScreenLockEnabled(); NotificationUtils.getNewNotificationManager(this, CHANNEL_ID, "Session Monitor", NotificationManagerCompat.IMPORTANCE_LOW); Intent notificationSettingIntent = NotificationUtils.getNotificationSettingIntent(CHANNEL_ID); PendingIntent defaultIntent = PendingIntentCompat.getActivity(this, 0, notificationSettingIntent, PendingIntent.FLAG_UPDATE_CURRENT, false); Intent stopIntent = new Intent(this, SessionMonitoringService.class).setAction(STOP_ACTION); PendingIntent pendingIntent = PendingIntentCompat.getService(this, 0, stopIntent, PendingIntent.FLAG_ONE_SHOT, false); NotificationCompat.Action stopServiceAction = new NotificationCompat.Action.Builder(null, getString(screenLockEnabled ? R.string.action_lock_app : R.string.action_stop_service), pendingIntent) .setAuthenticationRequired(true) .build(); NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID) .setLocalOnly(true) .setOngoing(true) .setOnlyAlertOnce(true) .setSilent(true) .setContentTitle(getString(screenLockEnabled ? R.string.app_manager_is_unlocked : R.string.app_manager_is_running)) .setContentText(getString(R.string.tap_to_open_notification_settings)) .setSmallIcon(R.drawable.ic_default_notification) .setPriority(NotificationCompat.PRIORITY_LOW) .setContentIntent(defaultIntent) .addAction(stopServiceAction); ForegroundService.start(this, NotificationUtils.nextNotificationId(null), builder.build(), ForegroundService.FOREGROUND_SERVICE_TYPE_DATA_SYNC | ForegroundService.FOREGROUND_SERVICE_TYPE_SPECIAL_USE); if (screenLockEnabled && Prefs.Privacy.isAutoLockEnabled()) { IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_ON); filter.addAction(Intent.ACTION_SCREEN_OFF); filter.addAction(Intent.ACTION_USER_PRESENT); ContextCompat.registerReceiver(this, mScreenLockedReceiver, filter, ContextCompat.RECEIVER_EXPORTED); mScreenLockedReceiverRegistered = true; } return START_NOT_STICKY; } @Nullable @Override public IBinder onBind(Intent intent) { return null; } @Override public void onTaskRemoved(Intent rootIntent) { // https://issuetracker.google.com/issues/36967794 Intent intent = new Intent(this, DummyActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); } @Override public void onDestroy() { if (mScreenLockedReceiverRegistered) { unregisterReceiver(mScreenLockedReceiver); mScreenLockedReceiverRegistered = false; } ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE); super.onDestroy(); } public void lockScreen() { // Simply stopping the service is enough // TODO: 16/7/23 Wipe memory? Ref: https://github.com/mollyim/mollyim-android/blob/2f8fe769628f7daddc87e8acfab1c4b5d301f728/app/src/main/java/org/thoughtcrime/securesms/service/WipeMemoryService.java#L102 stopSelf(); Process.killProcess(Process.myPid()); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/settings/AboutDeviceFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.settings; import android.os.Build; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.widget.NestedScrollView; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.misc.DeviceInfo2; import io.github.muntashirakon.util.UiUtils; public class AboutDeviceFragment extends Fragment { private MainPreferencesViewModel mModel; private TextView mTextView; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); mModel = new ViewModelProvider(requireActivity()).get(MainPreferencesViewModel.class); } @Override public void onStart() { super.onStart(); requireActivity().setTitle(R.string.about_device); } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { NestedScrollView view = (NestedScrollView) inflater.inflate(io.github.muntashirakon.ui.R.layout.dialog_scrollable_text_view, container, false); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { view.setScrollIndicators(0); } ViewGroup.LayoutParams lp = view.getLayoutParams(); if (lp instanceof ViewGroup.MarginLayoutParams) { ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) lp; mlp.topMargin = 0; view.setLayoutParams(mlp); } boolean secondary = false; if (getArguments() != null) { secondary = requireArguments().getBoolean(PreferenceFragment.PREF_SECONDARY); requireArguments().remove(PreferenceFragment.PREF_KEY); requireArguments().remove(PreferenceFragment.PREF_SECONDARY); } if (secondary) { UiUtils.applyWindowInsetsAsPadding(view, false, true, false, true); } else UiUtils.applyWindowInsetsAsPaddingNoTop(view); return view; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { mTextView = view.findViewById(android.R.id.content); view.findViewById(android.R.id.checkbox).setVisibility(View.GONE); mModel.getDeviceInfo().observe(getViewLifecycleOwner(), deviceInfo -> mTextView.setText(deviceInfo.toLocalizedString(requireActivity()))); mModel.loadDeviceInfo(new DeviceInfo2(requireActivity())); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/settings/AboutPreferences.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.settings; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.ViewModelProvider; import androidx.preference.Preference; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.transition.MaterialSharedAxis; import java.util.Locale; import java.util.Objects; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.changelog.ChangelogRecyclerAdapter; import io.github.muntashirakon.AppManager.misc.HelpActivity; import io.github.muntashirakon.dialog.AlertDialogBuilder; import io.github.muntashirakon.dialog.ScrollableDialogBuilder; public class AboutPreferences extends PreferenceFragment { private MainPreferencesViewModel mModel; @Override public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { setPreferencesFromResource(R.xml.preferences_about, rootKey); getPreferenceManager().setPreferenceDataStore(new SettingsDataStore()); mModel = new ViewModelProvider(requireActivity()).get(MainPreferencesViewModel.class); Preference versionPref = Objects.requireNonNull(findPreference("version")); versionPref.setSummary(String.format(Locale.getDefault(), "%s (%d)", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)); versionPref.setOnPreferenceClickListener(preference -> { mModel.loadChangeLog(); return true; }); // User manual ((Preference) Objects.requireNonNull(findPreference("user_manual"))) .setOnPreferenceClickListener(preference -> { Intent helpIntent = new Intent(requireContext(), HelpActivity.class); startActivity(helpIntent); return true; }); // Website ((Preference) Objects.requireNonNull(findPreference("website"))) .setOnPreferenceClickListener(preference -> { Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.website_message))); startActivity(intent); return true; }); // Get help ((Preference) Objects.requireNonNull(findPreference("get_help"))) .setOnPreferenceClickListener(preference -> { Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.discussions_site))); startActivity(intent); return true; }); // Third-party libraries ((Preference) Objects.requireNonNull(findPreference("third_party_libraries"))) .setOnPreferenceClickListener(preference -> { new ScrollableDialogBuilder(requireActivity()) .setTitle(R.string.third_party) .setMessage(R.string.third_party_message) .enableAnchors() .setNegativeButton(R.string.close, null) .show(); return true; }); // Credits ((Preference) Objects.requireNonNull(findPreference("credits"))) .setOnPreferenceClickListener(preference -> { new ScrollableDialogBuilder(requireActivity()) .setTitle(R.string.credits) .setMessage(R.string.credits_message) .enableAnchors() .setNegativeButton(R.string.close, null) .show(); return true; }); } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setEnterTransition(new MaterialSharedAxis(MaterialSharedAxis.Z, true)); setReturnTransition(new MaterialSharedAxis(MaterialSharedAxis.Z, false)); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); // Observe Changelog mModel.getChangeLog().observe(getViewLifecycleOwner(), changelog -> { View v = View.inflate(requireContext(), R.layout.dialog_whats_new, null); RecyclerView recyclerView = v.findViewById(android.R.id.list); recyclerView.setLayoutManager(new LinearLayoutManager(recyclerView.getContext())); ChangelogRecyclerAdapter adapter = new ChangelogRecyclerAdapter(); recyclerView.setAdapter(adapter); adapter.setAdapterList(changelog.getChangelogItems()); new AlertDialogBuilder(requireActivity(), true) .setTitle(R.string.changelog) .setView(recyclerView) .show(); }); } @Override public int getTitle() { return R.string.about; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/settings/AdvancedPreferences.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.settings; import android.os.Build; import android.os.Bundle; import android.text.Editable; import android.text.InputType; import android.text.TextUtils; import android.view.View; import android.view.inputmethod.EditorInfo; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.core.view.inputmethod.EditorInfoCompat; import androidx.fragment.app.DialogFragment; import androidx.lifecycle.ViewModelProvider; import androidx.preference.Preference; import androidx.preference.SwitchPreferenceCompat; import com.google.android.material.chip.Chip; import com.google.android.material.chip.ChipGroup; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.textfield.TextInputEditText; import com.google.android.material.transition.MaterialSharedAxis; import java.util.ArrayList; import java.util.List; import java.util.Objects; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.settings.crypto.ImportExportKeyStoreDialogFragment; import io.github.muntashirakon.AppManager.utils.AppPref; import io.github.muntashirakon.AppManager.utils.ArrayUtils; import io.github.muntashirakon.AppManager.utils.MultithreadedExecutor; import io.github.muntashirakon.AppManager.utils.Utils; import io.github.muntashirakon.dialog.SearchableMultiChoiceDialogBuilder; import io.github.muntashirakon.dialog.TextInputDialogBuilder; import io.github.muntashirakon.util.UiUtils; public class AdvancedPreferences extends PreferenceFragment { public static final String[] APK_NAME_FORMATS = new String[]{ "%label%", "%package_name%", "%version%", "%version_code%", "%min_sdk%", "%target_sdk%", "%datetime%" }; private int mThreadCount; private MainPreferencesViewModel mModel; @Override public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { setPreferencesFromResource(R.xml.preferences_advanced, rootKey); getPreferenceManager().setPreferenceDataStore(new SettingsDataStore()); mModel = new ViewModelProvider(requireActivity()).get(MainPreferencesViewModel.class); // Selected users Preference usersPref = Objects.requireNonNull(findPreference("selected_users")); usersPref.setOnPreferenceClickListener(preference -> { mModel.loadAllUsers(); return true; }); // Saved apk name format Preference savedApkFormatPref = Objects.requireNonNull(findPreference("saved_apk_format")); savedApkFormatPref.setOnPreferenceClickListener(preference -> { View view = getLayoutInflater().inflate(R.layout.dialog_set_apk_format, null); TextInputEditText inputApkNameFormat = view.findViewById(R.id.input_apk_name_format); inputApkNameFormat.setText(AppPref.getString(AppPref.PrefKey.PREF_SAVED_APK_FORMAT_STR)); ChipGroup apkNameFormats = view.findViewById(R.id.apk_name_formats); for (String apkNameFormatStr : APK_NAME_FORMATS) { if ("%min_sdk%".equals(apkNameFormatStr) && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { // Old devices does not support min SDK continue; } addChip(apkNameFormats, apkNameFormatStr).setOnClickListener(v -> { Editable apkFormat = inputApkNameFormat.getText(); if (apkFormat != null) { apkFormat.insert(inputApkNameFormat.getSelectionStart(), ((Chip) v).getText()); } }); } AlertDialog dialog = new MaterialAlertDialogBuilder(requireActivity()) .setTitle(R.string.pref_saved_apk_name_format) .setView(view) .setPositiveButton(R.string.save, (dialog1, which) -> { Editable apkFormat = inputApkNameFormat.getText(); if (!TextUtils.isEmpty(apkFormat)) { AppPref.set(AppPref.PrefKey.PREF_SAVED_APK_FORMAT_STR, apkFormat.toString().trim()); } }) .setNegativeButton(R.string.cancel, null) .create(); dialog.setOnShowListener(dialog1 -> inputApkNameFormat.postDelayed(() -> { inputApkNameFormat.requestFocus(); inputApkNameFormat.requestFocusFromTouch(); inputApkNameFormat.setSelection(inputApkNameFormat.length()); UiUtils.showKeyboard(inputApkNameFormat); }, 200)); dialog.show(); return true; }); // Thread count Preference threadCountPref = Objects.requireNonNull(findPreference("thread_count")); mThreadCount = MultithreadedExecutor.getThreadCount(); threadCountPref.setSummary(getResources().getQuantityString(R.plurals.pref_thread_count_msg, mThreadCount, mThreadCount)); threadCountPref.setOnPreferenceClickListener(preference -> { new TextInputDialogBuilder(requireActivity(), null) .setTitle(R.string.pref_thread_count) .setHelperText(getString(R.string.pref_thread_count_hint, Utils.getTotalCores())) .setInputText(String.valueOf(mThreadCount)) .setInputInputType(InputType.TYPE_CLASS_NUMBER) .setInputImeOptions(EditorInfo.IME_ACTION_DONE | EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING) .setNegativeButton(R.string.cancel, null) .setPositiveButton(R.string.save, (dialog, which, inputText, isChecked) -> { if (inputText != null && TextUtils.isDigitsOnly(inputText)) { int c = Integer.decode(inputText.toString()); MultithreadedExecutor.setThreadCount(c); mThreadCount = MultithreadedExecutor.getThreadCount(); threadCountPref.setSummary(getResources().getQuantityString(R.plurals.pref_thread_count_msg, mThreadCount, mThreadCount)); } }) .show(); return true; }); // ADB local server port Preference adbLsPort = Objects.requireNonNull(findPreference("adb_local_server_port")); int port = Prefs.Misc.getAdbLocalServerPort(); adbLsPort.setSummary(String.valueOf(port)); adbLsPort.setOnPreferenceClickListener(pref -> { new TextInputDialogBuilder(requireActivity(), null) .setTitle(R.string.adb_local_server_port) .setInputText(String.valueOf(port)) .setInputInputType(InputType.TYPE_CLASS_NUMBER) .setInputImeOptions(EditorInfo.IME_ACTION_DONE | EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING) .setNegativeButton(R.string.cancel, null) .setPositiveButton(R.string.save, (dialog, which, inputText, isChecked) -> { if (inputText != null && TextUtils.isDigitsOnly(inputText)) { int c = Integer.decode(inputText.toString()); // TODO: 10/18/25 If the port number is different, restart the local server with this new port if possible. Prefs.Misc.setAdbLocalServerPort(c); adbLsPort.setSummary(String.valueOf(c)); } }) .show(); return true; }); // Import/export App Manager's KeyStore ((Preference) Objects.requireNonNull(findPreference("import_export_keystore"))) .setOnPreferenceClickListener(preference -> { DialogFragment fragment = new ImportExportKeyStoreDialogFragment(); fragment.show(getParentFragmentManager(), ImportExportKeyStoreDialogFragment.TAG); return true; }); // Send notifications to the connected device ((SwitchPreferenceCompat) Objects.requireNonNull(findPreference("send_notifications_to_connected_devices"))) .setChecked(Prefs.Misc.sendNotificationsToConnectedDevices()); } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setEnterTransition(new MaterialSharedAxis(MaterialSharedAxis.Z, true)); setReturnTransition(new MaterialSharedAxis(MaterialSharedAxis.Z, false)); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); mModel.selectUsers().observe(getViewLifecycleOwner(), users -> { if (users == null) return; int[] selectedUsers = Prefs.Misc.getSelectedUsers(); Integer[] userIds = new Integer[users.size()]; CharSequence[] userInfo = new CharSequence[users.size()]; List preselectedUserIds = new ArrayList<>(); for (int i = 0; i < users.size(); ++i) { userIds[i] = users.get(i).id; userInfo[i] = users.get(i).toLocalizedString(requireContext()); if (selectedUsers == null || ArrayUtils.contains(selectedUsers, userIds[i])) { preselectedUserIds.add(userIds[i]); } } new SearchableMultiChoiceDialogBuilder<>(requireActivity(), userIds, userInfo) .setTitle(R.string.pref_selected_users) .addSelections(preselectedUserIds) .setPositiveButton(R.string.save, (dialog, which, selectedUserIds) -> { if (!selectedUserIds.isEmpty()) { Prefs.Misc.setSelectedUsers(ArrayUtils.convertToIntArray(selectedUserIds)); } else Prefs.Misc.setSelectedUsers(null); Utils.relaunchApp(requireActivity()); }) .setNegativeButton(R.string.cancel, null) .setNeutralButton(R.string.use_default, (dialog, which, selectedUserIds) -> { Prefs.Misc.setSelectedUsers(null); Utils.relaunchApp(requireActivity()); }) .show(); }); } @Override public int getTitle() { return R.string.pref_cat_advanced; } @NonNull private static Chip addChip(@NonNull ChipGroup apkFormats, @NonNull CharSequence text) { Chip chip = new Chip(apkFormats.getContext()); chip.setText(text); apkFormats.addView(chip); return chip; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/settings/ApkSigningPreferences.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.settings; import android.os.Bundle; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.ViewModelProvider; import androidx.preference.Preference; import androidx.preference.SwitchPreferenceCompat; import com.google.android.material.transition.MaterialSharedAxis; import java.util.Objects; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.apk.signing.SigSchemes; import io.github.muntashirakon.AppManager.apk.signing.Signer; import io.github.muntashirakon.AppManager.settings.crypto.RSACryptoSelectionDialogFragment; import io.github.muntashirakon.AppManager.utils.DigestUtils; import io.github.muntashirakon.dialog.SearchableFlagsDialogBuilder; public class ApkSigningPreferences extends PreferenceFragment { public static final String TAG = "ApkSigningPreferences"; private SettingsActivity mActivity; private Preference mCustomSigPref; private MainPreferencesViewModel mModel; @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.preferences_signature, rootKey); getPreferenceManager().setPreferenceDataStore(new SettingsDataStore()); mModel = new ViewModelProvider(requireActivity()).get(MainPreferencesViewModel.class); mActivity = (SettingsActivity) requireActivity(); // Set signature schemes Preference sigSchemes = Objects.requireNonNull(findPreference("signature_schemes")); final SigSchemes sigSchemeFlags = Prefs.Signing.getSigSchemes(); sigSchemes.setOnPreferenceClickListener(preference -> { new SearchableFlagsDialogBuilder<>(mActivity, sigSchemeFlags.getAllItems(), R.array.sig_schemes, sigSchemeFlags.getFlags()) .setTitle(R.string.app_signing_signature_schemes) .setPositiveButton(R.string.save, (dialog, which, selections) -> { int flags = 0; for (int flag : selections) { flags |= flag; } sigSchemeFlags.setFlags(flags); Prefs.Signing.setSigSchemes(flags); }) .setNegativeButton(R.string.cancel, null) .setNeutralButton(R.string.reset_to_default, (dialog, which, selections) -> { sigSchemeFlags.setFlags(SigSchemes.DEFAULT_SCHEMES); Prefs.Signing.setSigSchemes(SigSchemes.DEFAULT_SCHEMES); }) .show(); return true; }); mCustomSigPref = Objects.requireNonNull(findPreference("signing_keys")); mCustomSigPref.setOnPreferenceClickListener(preference -> { RSACryptoSelectionDialogFragment fragment = RSACryptoSelectionDialogFragment.getInstance(Signer.SIGNING_KEY_ALIAS); fragment.setOnKeyPairUpdatedListener((keyPair, certificateBytes) -> { if (keyPair != null && certificateBytes != null) { String hash = DigestUtils.getHexDigest(DigestUtils.SHA_256, certificateBytes); try { keyPair.destroy(); } catch (Exception ignore) { } mCustomSigPref.setSummary(hash); } else { mCustomSigPref.setSummary(R.string.key_not_set); } }); fragment.show(getParentFragmentManager(), RSACryptoSelectionDialogFragment.TAG); return true; }); ((SwitchPreferenceCompat) Objects.requireNonNull(findPreference("zip_align"))) .setChecked(Prefs.Signing.zipAlign()); } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setEnterTransition(new MaterialSharedAxis(MaterialSharedAxis.Z, true)); setReturnTransition(new MaterialSharedAxis(MaterialSharedAxis.Z, false)); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); mModel.getSigningKeySha256HashLiveData().observe(getViewLifecycleOwner(), hash -> { if (hash != null) { mCustomSigPref.setSummary(hash); } else { mCustomSigPref.setSummary(R.string.key_not_set); } }); mModel.loadSigningKeySha256Hash(); } @Override public int getTitle() { return R.string.apk_signing; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/settings/AppearancePreferences.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.settings; import android.os.Bundle; import android.view.View; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatDelegate; import androidx.preference.Preference; import androidx.preference.SwitchPreferenceCompat; import com.google.android.material.transition.MaterialSharedAxis; import java.util.Arrays; import java.util.List; import java.util.Objects; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.utils.appearance.AppearanceUtils; import io.github.muntashirakon.AppManager.utils.appearance.TypefaceUtil; import io.github.muntashirakon.dialog.SearchableFlagsDialogBuilder; import io.github.muntashirakon.dialog.SearchableSingleChoiceDialogBuilder; public class AppearancePreferences extends PreferenceFragment { private static final List THEME_CONST = Arrays.asList( AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM, AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY, AppCompatDelegate.MODE_NIGHT_NO, AppCompatDelegate.MODE_NIGHT_YES); private static final List LAYOUT_ORIENTATION_CONST = Arrays.asList( View.LAYOUT_DIRECTION_LOCALE, View.LAYOUT_DIRECTION_LTR, View.LAYOUT_DIRECTION_RTL); private int mCurrentTheme; private int mCurrentLayoutDirection; @Override public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { setPreferencesFromResource(R.xml.preferences_appearance, rootKey); getPreferenceManager().setPreferenceDataStore(new SettingsDataStore()); // App theme final String[] themes = getResources().getStringArray(R.array.themes); mCurrentTheme = Prefs.Appearance.getNightMode(); Preference appTheme = Objects.requireNonNull(findPreference("app_theme")); appTheme.setSummary(themes[THEME_CONST.indexOf(mCurrentTheme)]); appTheme.setOnPreferenceClickListener(preference -> { new SearchableSingleChoiceDialogBuilder<>(requireActivity(), THEME_CONST, themes) .setTitle(R.string.select_theme) .setSelection(mCurrentTheme) .setPositiveButton(R.string.apply, (dialog, which, selectedTheme) -> { if (selectedTheme != null && selectedTheme != mCurrentTheme) { mCurrentTheme = selectedTheme; Prefs.Appearance.setNightMode(mCurrentTheme); AppCompatDelegate.setDefaultNightMode(mCurrentTheme); } }) .setNegativeButton(R.string.cancel, null) .show(); return true; }); // Black theme/custom theme SwitchPreferenceCompat fullBlackTheme = Objects.requireNonNull(findPreference("app_theme_pure_black")); fullBlackTheme.setChecked(Prefs.Appearance.isPureBlackTheme()); fullBlackTheme.setOnPreferenceChangeListener((preference, newValue) -> { boolean enabled = (boolean) newValue; Prefs.Appearance.setPureBlackTheme(enabled); AppearanceUtils.applyConfigurationChangesToActivities(); return true; }); // Black theme/custom theme SwitchPreferenceCompat useSystemFontPref = Objects.requireNonNull(findPreference("use_system_font")); useSystemFontPref.setChecked(Prefs.Appearance.useSystemFont()); useSystemFontPref.setOnPreferenceChangeListener((preference, newValue) -> { if (((boolean) newValue)) { // Enable system font TypefaceUtil.replaceFontsWithSystem(requireContext()); } else { // Disable system font TypefaceUtil.restoreFonts(); } AppearanceUtils.applyConfigurationChangesToActivities(); return true; }); // Layout orientation final String[] layoutOrientations = getResources().getStringArray(R.array.layout_orientations); mCurrentLayoutDirection = Prefs.Appearance.getLayoutDirection(); Preference layoutOrientation = Objects.requireNonNull(findPreference("layout_orientation")); layoutOrientation.setSummary(layoutOrientations[LAYOUT_ORIENTATION_CONST.indexOf(mCurrentLayoutDirection)]); layoutOrientation.setOnPreferenceClickListener(preference -> { new SearchableSingleChoiceDialogBuilder<>(requireActivity(), LAYOUT_ORIENTATION_CONST, layoutOrientations) .setTitle(R.string.pref_layout_direction) .setSelection(mCurrentLayoutDirection) .setPositiveButton(R.string.apply, (dialog, which, selectedLayoutOrientation) -> { mCurrentLayoutDirection = Objects.requireNonNull(selectedLayoutOrientation); Prefs.Appearance.setLayoutDirection(mCurrentLayoutDirection); AppearanceUtils.applyConfigurationChangesToActivities(); }) .setNegativeButton(R.string.cancel, null) .show(); return true; }); // Enable/disable features FeatureController fc = FeatureController.getInstance(); ((Preference) Objects.requireNonNull(findPreference("enabled_features"))) .setOnPreferenceClickListener(preference -> { new SearchableFlagsDialogBuilder<>(requireActivity(), FeatureController.featureFlags, FeatureController.getFormattedFlagNames(requireActivity()), fc.getFlags()) .setTitle(R.string.enable_disable_features) .setOnMultiChoiceClickListener((dialog, which, item, isChecked) -> fc.modifyState(FeatureController.featureFlags.get(which), isChecked)) .setNegativeButton(R.string.close, null) .show(); return true; }); } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setEnterTransition(new MaterialSharedAxis(MaterialSharedAxis.Z, true)); setReturnTransition(new MaterialSharedAxis(MaterialSharedAxis.Z, false)); } @Override public int getTitle() { return R.string.pref_cat_appearance; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/settings/BackupRestorePreferences.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.settings; import static io.github.muntashirakon.AppManager.utils.UIUtils.getSecondaryText; import static io.github.muntashirakon.AppManager.utils.UIUtils.getSmallerText; import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.text.SpannableStringBuilder; import android.view.View; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.annotation.UiThread; import androidx.appcompat.app.AlertDialog; import androidx.collection.ArrayMap; import androidx.core.content.ContextCompat; import androidx.fragment.app.DialogFragment; import androidx.lifecycle.ViewModelProvider; import androidx.preference.Preference; import androidx.preference.SwitchPreferenceCompat; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.transition.MaterialSharedAxis; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.backup.BackupFlags; import io.github.muntashirakon.AppManager.backup.BackupUtils; import io.github.muntashirakon.AppManager.backup.CryptoUtils; import io.github.muntashirakon.AppManager.backup.convert.ImportType; import io.github.muntashirakon.AppManager.batchops.BatchOpsManager; import io.github.muntashirakon.AppManager.batchops.BatchOpsService; import io.github.muntashirakon.AppManager.batchops.BatchQueueItem; import io.github.muntashirakon.AppManager.batchops.struct.BatchBackupImportOptions; import io.github.muntashirakon.AppManager.crypto.RSACrypto; import io.github.muntashirakon.AppManager.settings.crypto.AESCryptoSelectionDialogFragment; import io.github.muntashirakon.AppManager.settings.crypto.ECCCryptoSelectionDialogFragment; import io.github.muntashirakon.AppManager.settings.crypto.OpenPgpKeySelectionDialogFragment; import io.github.muntashirakon.AppManager.settings.crypto.RSACryptoSelectionDialogFragment; import io.github.muntashirakon.dialog.DialogTitleBuilder; import io.github.muntashirakon.dialog.SearchableItemsDialogBuilder; import io.github.muntashirakon.dialog.SearchableMultiChoiceDialogBuilder; import io.github.muntashirakon.dialog.SearchableSingleChoiceDialogBuilder; import io.github.muntashirakon.io.Paths; public class BackupRestorePreferences extends PreferenceFragment { private static final String[] ENCRYPTION = new String[]{ CryptoUtils.MODE_NO_ENCRYPTION, CryptoUtils.MODE_OPEN_PGP, CryptoUtils.MODE_AES, CryptoUtils.MODE_RSA, CryptoUtils.MODE_ECC }; @StringRes private static final Integer[] ENCRYPTION_NAMES = new Integer[]{ R.string.none, R.string.open_pgp_provider, R.string.aes, R.string.rsa, R.string.ecc, }; private SettingsActivity mActivity; private String mCurrentCompressionMethod; private Uri mBackupVolume; @ImportType private int mImportType; private boolean mDeleteBackupsAfterImport; private MainPreferencesViewModel mModel; private final ActivityResultLauncher mSafSelectBackupVolume = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { try { if (result.getResultCode() != Activity.RESULT_OK) return; Intent data = result.getData(); if (data == null) return; Uri treeUri = data.getData(); if (treeUri == null) return; int takeFlags = data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); requireContext().getContentResolver().takePersistableUriPermission(treeUri, takeFlags); } finally { // Display backup volumes again mModel.loadStorageVolumes(); } }); private final ActivityResultLauncher mSafSelectImportDirectory = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { if (result.getResultCode() != Activity.RESULT_OK) return; Intent data = result.getData(); if (data == null) return; Uri treeUri = data.getData(); if (treeUri == null) return; int takeFlags = data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); requireContext().getContentResolver().takePersistableUriPermission(treeUri, takeFlags); startImportOperation(mImportType, treeUri, mDeleteBackupsAfterImport); }); @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.preferences_backup_restore, rootKey); getPreferenceManager().setPreferenceDataStore(new SettingsDataStore()); mModel = new ViewModelProvider(requireActivity()).get(MainPreferencesViewModel.class); mActivity = (SettingsActivity) requireActivity(); // Backup compression method mCurrentCompressionMethod = Prefs.BackupRestore.getCompressionMethod(); Preference compressionMethod = Objects.requireNonNull(findPreference("backup_compression_method")); compressionMethod.setSummary(BackupUtils.getReadableTarType(mCurrentCompressionMethod)); compressionMethod.setOnPreferenceClickListener(preference -> { new SearchableSingleChoiceDialogBuilder<>(mActivity, BackupUtils.TAR_TYPES, BackupUtils.TAR_TYPES_READABLE) .setTitle(R.string.pref_compression_method) .setSelection(mCurrentCompressionMethod) .setPositiveButton(R.string.save, (dialog, which, selectedTarType) -> { if (selectedTarType != null) { mCurrentCompressionMethod = selectedTarType; Prefs.BackupRestore.setCompressionMethod(mCurrentCompressionMethod); compressionMethod.setSummary(BackupUtils.getReadableTarType(mCurrentCompressionMethod)); } }) .setNegativeButton(R.string.cancel, null) .show(); return true; }); // Backup flags BackupFlags flags = BackupFlags.fromPref(); ((Preference) Objects.requireNonNull(findPreference("backup_flags"))).setOnPreferenceClickListener(preference -> { List supportedBackupFlags = BackupFlags.getSupportedBackupFlagsAsArray(); new SearchableMultiChoiceDialogBuilder<>(requireActivity(), supportedBackupFlags, BackupFlags.getFormattedFlagNames(requireContext(), supportedBackupFlags)) .setTitle(R.string.backup_options) .addSelections(flags.flagsToCheckedIndexes(supportedBackupFlags)) .hideSearchBar(true) .showSelectAll(false) .setPositiveButton(R.string.save, (dialog, which, selectedItems) -> { int flagsInt = 0; for (int flag : selectedItems) { flagsInt |= flag; } flags.setFlags(flagsInt); Prefs.BackupRestore.setBackupFlags(flags.getFlags()); }) .setNegativeButton(R.string.cancel, null) .show(); return true; }); // Keystore toggle SwitchPreferenceCompat backupKeyStore = Objects.requireNonNull(findPreference("backup_android_keystore")); backupKeyStore.setChecked(Prefs.BackupRestore.backupAppsWithKeyStore()); // Encryption ((Preference) Objects.requireNonNull(findPreference("encryption"))).setOnPreferenceClickListener(preference -> { CharSequence[] encryptionNamesText = new CharSequence[ENCRYPTION_NAMES.length]; for (int i = 0; i < ENCRYPTION_NAMES.length; ++i) { encryptionNamesText[i] = getString(ENCRYPTION_NAMES[i]); } new SearchableSingleChoiceDialogBuilder<>(mActivity, ENCRYPTION, encryptionNamesText) .setTitle(R.string.encryption) .setSelection(Prefs.Encryption.getEncryptionMode()) .setOnSingleChoiceClickListener((dialog, which, encryptionMode, isChecked) -> { if (!isChecked) return; switch (encryptionMode) { case CryptoUtils.MODE_NO_ENCRYPTION: Prefs.Encryption.setEncryptionMode(encryptionMode); break; case CryptoUtils.MODE_AES: { DialogFragment fragment = new AESCryptoSelectionDialogFragment(); fragment.show(getParentFragmentManager(), AESCryptoSelectionDialogFragment.TAG); break; } case CryptoUtils.MODE_RSA: { RSACryptoSelectionDialogFragment fragment = RSACryptoSelectionDialogFragment.getInstance(RSACrypto.RSA_KEY_ALIAS); fragment.setOnKeyPairUpdatedListener((keyPair, certificateBytes) -> { if (keyPair != null) { Prefs.Encryption.setEncryptionMode(CryptoUtils.MODE_RSA); } }); fragment.show(getParentFragmentManager(), RSACryptoSelectionDialogFragment.TAG); break; } case CryptoUtils.MODE_ECC: { ECCCryptoSelectionDialogFragment fragment = new ECCCryptoSelectionDialogFragment(); fragment.setOnKeyPairUpdatedListener((keyPair, certificateBytes) -> { if (keyPair != null) { Prefs.Encryption.setEncryptionMode(CryptoUtils.MODE_ECC); } }); fragment.show(getParentFragmentManager(), RSACryptoSelectionDialogFragment.TAG); break; } case CryptoUtils.MODE_OPEN_PGP: { Prefs.Encryption.setEncryptionMode(encryptionMode); DialogFragment fragment = new OpenPgpKeySelectionDialogFragment(); fragment.show(getParentFragmentManager(), OpenPgpKeySelectionDialogFragment.TAG); } } }) .setPositiveButton(R.string.ok, null) .show(); return true; }); // Backup volume mBackupVolume = Prefs.Storage.getVolumePath(); ((Preference) Objects.requireNonNull(findPreference("backup_volume"))) .setOnPreferenceClickListener(preference -> { mModel.loadStorageVolumes(); return true; }); // Import backups ((Preference) Objects.requireNonNull(findPreference("import_backups"))) .setOnPreferenceClickListener(preference -> { new SearchableItemsDialogBuilder<>(mActivity, R.array.import_backup_options) .setTitle(new DialogTitleBuilder(mActivity) .setTitle(R.string.pref_import_backups) .setSubtitle(R.string.pref_import_backups_hint) .build()) .setOnItemClickListener((dialog, which, item) -> { mImportType = which; String path; switch (mImportType) { case ImportType.OAndBackup: path = "oandbackups"; break; case ImportType.TitaniumBackup: path = "TitaniumBackup"; break; case ImportType.SwiftBackup: path = "SwiftBackup"; break; default: path = ""; } new MaterialAlertDialogBuilder(mActivity) .setTitle(R.string.pref_import_backups) .setMessage(R.string.import_backups_warning_delete_backups_after_import) .setPositiveButton(R.string.no, (dialog1, which1) -> { mDeleteBackupsAfterImport = false; mSafSelectImportDirectory.launch(getSafIntent(path)); }) .setNegativeButton(R.string.yes, (dialog1, which1) -> { mDeleteBackupsAfterImport = true; mSafSelectImportDirectory.launch(getSafIntent(path)); }) .setNeutralButton(R.string.cancel, null) .show(); }) .setNegativeButton(R.string.close, null) .show(); return true; }); } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setEnterTransition(new MaterialSharedAxis(MaterialSharedAxis.Z, true)); setReturnTransition(new MaterialSharedAxis(MaterialSharedAxis.Z, false)); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); mModel.getStorageVolumesLiveData().observe(getViewLifecycleOwner(), this::displayVolumeSelectionDialog); } @Override public int getTitle() { return R.string.backup_restore; } @UiThread private void startImportOperation(@ImportType int backupType, Uri uri, boolean removeImported) { // Start batch ops service BatchOpsManager.Result input = new BatchOpsManager.Result(Collections.emptyList()); BatchBackupImportOptions options = new BatchBackupImportOptions(backupType, uri, removeImported); BatchQueueItem item = BatchQueueItem.getBatchOpQueue(BatchOpsManager.OP_IMPORT_BACKUPS, input.getFailedPackages(), input.getAssociatedUsers(), options); Intent intent = BatchOpsService.getServiceIntent(mActivity, item); ContextCompat.startForegroundService(mActivity, intent); } private void displayVolumeSelectionDialog(@NonNull ArrayMap storageLocations) { // TODO: 13/8/22 Move to a separate BottomSheet dialog fragment AtomicReference alertDialog = new AtomicReference<>(null); DialogTitleBuilder titleBuilder = new DialogTitleBuilder(mActivity) .setTitle(R.string.backup_volume) .setSubtitle(R.string.backup_volume_dialog_description) .setStartIcon(R.drawable.ic_zip_disk) .setEndIcon(R.drawable.ic_add, v -> new MaterialAlertDialogBuilder(mActivity) .setTitle(R.string.notice) .setMessage(R.string.notice_saf) .setPositiveButton(R.string.go, (dialog1, which1) -> { if (alertDialog.get() != null) { alertDialog.get().dismiss(); } mSafSelectBackupVolume.launch(getSafIntent("AppManager")); }) .setNeutralButton(R.string.cancel, null) .show()); if (storageLocations.isEmpty()) { alertDialog.set(new MaterialAlertDialogBuilder(mActivity) .setCustomTitle(titleBuilder.build()) .setMessage(R.string.no_volumes_found) .setNegativeButton(R.string.ok, null) .show()); return; } Uri[] backupVolumes = new Uri[storageLocations.size()]; CharSequence[] backupVolumesStr = new CharSequence[storageLocations.size()]; for (int i = 0; i < storageLocations.size(); ++i) { backupVolumes[i] = storageLocations.valueAt(i); backupVolumesStr[i] = new SpannableStringBuilder(storageLocations.keyAt(i)).append("\n") .append(getSecondaryText(mActivity, getSmallerText(backupVolumes[i].getPath()))); } alertDialog.set(new SearchableSingleChoiceDialogBuilder<>(mActivity, backupVolumes, backupVolumesStr) .setTitle(titleBuilder.build()) .setSelection(mBackupVolume) .setNegativeButton(R.string.cancel, null) .setPositiveButton(R.string.save, (dialog, which, selectedBackupVolume) -> { mBackupVolume = selectedBackupVolume; Uri lastBackupVolume = Prefs.Storage.getVolumePath(); if (!lastBackupVolume.equals(mBackupVolume)) { Prefs.Storage.setVolumePath(mBackupVolume.toString()); mModel.reloadApps(); } }) .show()); } private Intent getSafIntent(String path) { return new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) .putExtra("android.provider.extra.SHOW_ADVANCED", true) .putExtra("android.provider.extra.INITIAL_URI", Paths.getPrimaryPath(path).getUri()); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/settings/ChangeLanguageFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.settings; import android.annotation.SuppressLint; import android.os.Build; import android.os.Bundle; import android.text.TextUtils; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.CheckedTextView; import android.widget.FrameLayout; import androidx.annotation.LayoutRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.collection.ArraySet; import androidx.core.widget.TextViewCompat; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.LinearLayoutManager; import com.google.android.material.color.MaterialColors; import com.google.android.material.resources.MaterialAttributes; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.utils.LangUtils; import io.github.muntashirakon.AppManager.utils.appearance.AppearanceUtils; import io.github.muntashirakon.util.AdapterUtils; import io.github.muntashirakon.util.UiUtils; import io.github.muntashirakon.widget.RecyclerView; import io.github.muntashirakon.widget.SearchView; public class ChangeLanguageFragment extends Fragment { private SearchView mSearchView; private RecyclerView mRecyclerView; private FrameLayout mViewContainer; @Nullable private SearchableRecyclerViewAdapter mAdapter; private String mCurrentLang; private boolean mIsTextSelectable; @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(io.github.muntashirakon.ui.R.layout.dialog_searchable_single_choice, container, false); mRecyclerView = view.findViewById(android.R.id.list); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { mRecyclerView.setScrollIndicators(0); } mRecyclerView.setLayoutManager(new LinearLayoutManager(view.getContext(), LinearLayoutManager.VERTICAL, false)); mViewContainer = view.findViewById(io.github.muntashirakon.ui.R.id.container); mSearchView = view.findViewById(io.github.muntashirakon.ui.R.id.action_search); mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { return false; } @Override public boolean onQueryTextChange(String newText) { if (mAdapter != null) mAdapter.setFilteredItems(newText); return true; } }); view.setFitsSystemWindows(true); boolean secondary = false; if (getArguments() != null) { secondary = requireArguments().getBoolean(PreferenceFragment.PREF_SECONDARY); requireArguments().remove(PreferenceFragment.PREF_KEY); requireArguments().remove(PreferenceFragment.PREF_SECONDARY); } if (secondary) { UiUtils.applyWindowInsetsAsPadding(view, false, true, false, true); } else UiUtils.applyWindowInsetsAsPaddingNoTop(view); return view; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); mCurrentLang = Prefs.Appearance.getLanguage(); Map locales = LangUtils.getAppLanguages(requireActivity()); final CharSequence[] languageNames = getLanguagesL(locales); final String[] languages = new String[languageNames.length]; int i = 0; int localeIndex = 0; for (Map.Entry localeEntry : locales.entrySet()) { languages[i] = localeEntry.getKey(); if (languages[i].equals(mCurrentLang)) { localeIndex = i; } ++i; } @SuppressLint({"RestrictedApi", "PrivateResource"}) int layoutId = MaterialAttributes.resolveInteger(requireContext(), androidx.appcompat.R.attr.singleChoiceItemLayout, com.google.android.material.R.layout.mtrl_alert_select_dialog_singlechoice); mAdapter = new SearchableRecyclerViewAdapter<>(Arrays.asList(languageNames), Arrays.asList(languages), layoutId); mAdapter.setSelectedIndex(localeIndex, false); mRecyclerView.setAdapter(mAdapter); } @Override public void onStart() { super.onStart(); requireActivity().setTitle(R.string.choose_language); } @NonNull private CharSequence[] getLanguagesL(@NonNull Map locales) { CharSequence[] localesL = new CharSequence[locales.size()]; Locale locale; int i = 0; for (Map.Entry localeEntry : locales.entrySet()) { locale = localeEntry.getValue(); if (LangUtils.LANG_AUTO.equals(localeEntry.getKey())) { localesL[i] = getString(R.string.auto); } else localesL[i] = locale.getDisplayName(locale); ++i; } return localesL; } private void triggerSingleChoiceClickListener(int index, boolean isChecked) { if (mAdapter == null) { return; } String selectedItem = mAdapter.mItems.get(index); if (selectedItem != null && isChecked) { mCurrentLang = selectedItem; Prefs.Appearance.setLanguage(mCurrentLang); AppearanceUtils.applyConfigurationChangesToActivities(); } } class SearchableRecyclerViewAdapter extends RecyclerView.Adapter.ViewHolder> { @NonNull private final List mItemNames; @NonNull private final List mItems; @NonNull private final ArrayList mFilteredItems = new ArrayList<>(); private int mSelectedItem = -1; private final Set mDisabledItems = new ArraySet<>(); @LayoutRes private final int mLayoutId; SearchableRecyclerViewAdapter(@NonNull List itemNames, @NonNull List items, int layoutId) { mItemNames = itemNames; mItems = items; mLayoutId = layoutId; synchronized (mFilteredItems) { for (int i = 0; i < items.size(); ++i) { mFilteredItems.add(i); } } } void setFilteredItems(String constraint) { constraint = TextUtils.isEmpty(constraint) ? null : constraint.toLowerCase(Locale.ROOT); Locale locale = Locale.getDefault(); synchronized (mFilteredItems) { int previousCount = mFilteredItems.size(); mFilteredItems.clear(); for (int i = 0; i < mItems.size(); ++i) { if (constraint == null || mItemNames.get(i).toString().toLowerCase(locale).contains(constraint) || mItems.get(i).toString().toLowerCase(Locale.ROOT).contains(constraint)) { mFilteredItems.add(i); } } AdapterUtils.notifyDataSetChanged(this, previousCount, mFilteredItems.size()); } } @Nullable T getSelection() { if (mSelectedItem >= 0) { return mItems.get(mSelectedItem); } return null; } void setSelection(@Nullable T selectedItem, boolean triggerListener) { if (selectedItem != null) { int index = mItems.indexOf(selectedItem); if (index != -1) { setSelectedIndex(index, triggerListener); } } } void setSelectedIndex(int selectedIndex, boolean triggerListener) { if (selectedIndex == mSelectedItem) { // Do nothing return; } updateSelection(false, triggerListener); mSelectedItem = selectedIndex; updateSelection(true, triggerListener); mRecyclerView.setSelection(selectedIndex); } void addDisabledItems(@Nullable List disabledItems) { if (disabledItems != null) { for (T item : disabledItems) { int index = mItems.indexOf(item); if (index != -1) { synchronized (mDisabledItems) { mDisabledItems.add(index); } } } } } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(mLayoutId, parent, false); return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { Integer index; synchronized (mFilteredItems) { index = mFilteredItems.get(position); } final AtomicBoolean selected = new AtomicBoolean(mSelectedItem == index); holder.item.setText(mItemNames.get(index)); holder.item.setTextIsSelectable(mIsTextSelectable); synchronized (mDisabledItems) { holder.item.setEnabled(!mDisabledItems.contains(index)); } holder.item.setChecked(selected.get()); holder.item.setOnClickListener(v -> { if (selected.get()) { // Already selected, do nothing return; } // Unselect the previous and select this one updateSelection(false, true); mSelectedItem = index; // Update selection manually selected.set(!selected.get()); holder.item.setChecked(selected.get()); triggerSingleChoiceClickListener(index, selected.get()); }); } @Override public int getItemCount() { synchronized (mFilteredItems) { return mFilteredItems.size(); } } private void updateSelection(boolean selected, boolean triggerListener) { if (mSelectedItem < 0) { return; } int position; synchronized (mFilteredItems) { position = mFilteredItems.indexOf(mSelectedItem); } if (position >= 0) { notifyItemChanged(position, AdapterUtils.STUB); } if (triggerListener) { triggerSingleChoiceClickListener(mSelectedItem, selected); } } class ViewHolder extends RecyclerView.ViewHolder { CheckedTextView item; @SuppressLint("RestrictedApi") public ViewHolder(@NonNull View itemView) { super(itemView); item = itemView.findViewById(android.R.id.text1); int textAppearanceBodyLarge = MaterialAttributes.resolveInteger(item.getContext(), com.google.android.material.R.attr.textAppearanceBodyLarge, 0); TextViewCompat.setTextAppearance(item, textAppearanceBodyLarge); item.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16); item.setTextColor(MaterialColors.getColor(item.getContext(), com.google.android.material.R.attr.colorOnSurfaceVariant, -1)); } } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/settings/FeatureController.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.settings; import android.Manifest; import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageManager; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.collection.SparseArrayCompat; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Objects; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.apk.installer.PackageInstallerActivity; import io.github.muntashirakon.AppManager.details.AppDetailsActivity; import io.github.muntashirakon.AppManager.details.manifest.ManifestViewerActivity; import io.github.muntashirakon.AppManager.editor.CodeEditorActivity; import io.github.muntashirakon.AppManager.intercept.ActivityInterceptor; import io.github.muntashirakon.AppManager.logcat.LogViewerActivity; import io.github.muntashirakon.AppManager.scanner.ScannerActivity; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.terminal.TermActivity; import io.github.muntashirakon.AppManager.utils.AppPref; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.viewer.ExplorerActivity; public class FeatureController { @IntDef(flag = true, value = { FEAT_INTERCEPTOR, FEAT_MANIFEST, FEAT_SCANNER, FEAT_INSTALLER, FEAT_USAGE_ACCESS, FEAT_LOG_VIEWER, FEAT_INTERNET, FEAT_APP_EXPLORER, FEAT_APP_INFO, FEAT_CODE_EDITOR, FEAT_VIRUS_TOTAL, FEAT_TERMINAL, }) public @interface FeatureFlags { } private static final int FEAT_INTERCEPTOR = 1; private static final int FEAT_MANIFEST = 1 << 1; private static final int FEAT_SCANNER = 1 << 2; public static final int FEAT_INSTALLER = 1 << 3; public static final int FEAT_USAGE_ACCESS = 1 << 4; public static final int FEAT_LOG_VIEWER = 1 << 5; public static final int FEAT_INTERNET = 1 << 6; private static final int FEAT_APP_EXPLORER = 1 << 7; private static final int FEAT_APP_INFO = 1 << 8; private static final int FEAT_CODE_EDITOR = 1 << 9; public static final int FEAT_VIRUS_TOTAL = 1 << 10; public static final int FEAT_TERMINAL = 1 << 11; @NonNull public static FeatureController getInstance() { return new FeatureController(); } @FeatureFlags public static final List featureFlags = new ArrayList<>(); private static final LinkedHashMap sFeatureFlagsMap = new LinkedHashMap() { { featureFlags.add(FEAT_APP_EXPLORER); put(FEAT_APP_EXPLORER, R.string.app_explorer); featureFlags.add(FEAT_APP_INFO); put(FEAT_APP_INFO, R.string.app_info); featureFlags.add(FEAT_CODE_EDITOR); put(FEAT_CODE_EDITOR, R.string.title_code_editor); featureFlags.add(FEAT_INTERCEPTOR); put(FEAT_INTERCEPTOR, R.string.interceptor); featureFlags.add(FEAT_LOG_VIEWER); put(FEAT_LOG_VIEWER, R.string.log_viewer); featureFlags.add(FEAT_MANIFEST); put(FEAT_MANIFEST, R.string.manifest_viewer); featureFlags.add(FEAT_INSTALLER); put(FEAT_INSTALLER, R.string.package_installer); featureFlags.add(FEAT_SCANNER); put(FEAT_SCANNER, R.string.scanner); featureFlags.add(FEAT_TERMINAL); put(FEAT_TERMINAL, R.string.title_terminal_emulator); featureFlags.add(FEAT_USAGE_ACCESS); put(FEAT_USAGE_ACCESS, R.string.usage_access); featureFlags.add(FEAT_VIRUS_TOTAL); put(FEAT_VIRUS_TOTAL, R.string.virus_total); } }; @NonNull public static CharSequence[] getFormattedFlagNames(@NonNull Context context) { CharSequence[] flagNames = new CharSequence[featureFlags.size()]; for (int i = 0; i < flagNames.length; ++i) { flagNames[i] = context.getText(Objects.requireNonNull(sFeatureFlagsMap.get(featureFlags.get(i)))); } return flagNames; } private static final SparseArrayCompat sComponentCache = new SparseArrayCompat<>(4); private final String mPackageName = BuildConfig.APPLICATION_ID; private final PackageManager mPm; private int mFlags; private FeatureController() { mPm = ContextUtils.getContext().getPackageManager(); mFlags = AppPref.getInt(AppPref.PrefKey.PREF_ENABLED_FEATURES_INT); } public int getFlags() { return mFlags; } public static boolean isInterceptorEnabled() { return getInstance().isEnabled(FEAT_INTERCEPTOR); } public static boolean isManifestEnabled() { return getInstance().isEnabled(FEAT_MANIFEST); } public static boolean isScannerEnabled() { return getInstance().isEnabled(FEAT_SCANNER); } public static boolean isInstallerEnabled() { return getInstance().isEnabled(FEAT_INSTALLER); } public static boolean isUsageAccessEnabled() { return getInstance().isEnabled(FEAT_USAGE_ACCESS); } public static boolean isLogViewerEnabled() { return getInstance().isEnabled(FEAT_LOG_VIEWER); } public static boolean isInternetEnabled() { return getInstance().isEnabled(FEAT_INTERNET); } public static boolean isVirusTotalEnabled() { return getInstance().isEnabled(FEAT_VIRUS_TOTAL); } public static boolean isCodeEditorEnabled() { return getInstance().isEnabled(FEAT_CODE_EDITOR); } public static boolean isTerminalEnabled() { return getInstance().isEnabled(FEAT_TERMINAL); } private boolean isEnabled(@FeatureFlags int key) { ComponentName cn; switch (key) { case FEAT_INSTALLER: cn = getComponentName(key, PackageInstallerActivity.class); break; case FEAT_INTERCEPTOR: cn = getComponentName(key, ActivityInterceptor.class); break; case FEAT_MANIFEST: cn = getComponentName(key, ManifestViewerActivity.class); break; case FEAT_SCANNER: cn = getComponentName(key, ScannerActivity.class); break; case FEAT_USAGE_ACCESS: // Only depends on flag return (mFlags & key) != 0; case FEAT_VIRUS_TOTAL: return (mFlags & key) != 0 && isEnabled(FEAT_INTERNET); case FEAT_INTERNET: return (mFlags & key) != 0 && SelfPermissions.checkSelfPermission(Manifest.permission.INTERNET); case FEAT_LOG_VIEWER: cn = getComponentName(key, LogViewerActivity.class); break; case FEAT_APP_EXPLORER: cn = getComponentName(key, ExplorerActivity.class); break; case FEAT_APP_INFO: cn = getComponentName(key, AppDetailsActivity.ALIAS_APP_INFO); break; case FEAT_CODE_EDITOR: cn = getComponentName(key, CodeEditorActivity.ALIAS_EDITOR); break; case FEAT_TERMINAL: cn = getComponentName(key, TermActivity.class); break; default: throw new IllegalArgumentException(); } return isComponentEnabled(cn) && (mFlags & key) != 0; } public void modifyState(@FeatureFlags int key, boolean enabled) { switch (key) { case FEAT_INSTALLER: modifyState(key, PackageInstallerActivity.class, enabled); break; case FEAT_INTERCEPTOR: modifyState(key, ActivityInterceptor.class, enabled); break; case FEAT_MANIFEST: modifyState(key, ManifestViewerActivity.class, enabled); break; case FEAT_SCANNER: modifyState(key, ScannerActivity.class, enabled); break; case FEAT_USAGE_ACCESS: case FEAT_INTERNET: case FEAT_VIRUS_TOTAL: // Only depends on flag break; case FEAT_LOG_VIEWER: modifyState(key, LogViewerActivity.class, enabled); break; case FEAT_APP_EXPLORER: modifyState(key, ExplorerActivity.class, enabled); break; case FEAT_APP_INFO: modifyState(key, AppDetailsActivity.ALIAS_APP_INFO, enabled); break; case FEAT_CODE_EDITOR: modifyState(key, CodeEditorActivity.ALIAS_EDITOR, enabled); break; case FEAT_TERMINAL: modifyState(key, TermActivity.class, enabled); break; } // Modify flags mFlags = enabled ? (mFlags | key) : (mFlags & ~key); // Save to pref AppPref.set(AppPref.PrefKey.PREF_ENABLED_FEATURES_INT, mFlags); } private void modifyState(@FeatureFlags int key, @Nullable Class clazz, boolean enabled) { ComponentName cn = getComponentName(key, clazz); if (cn == null) return; int state = enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : PackageManager.COMPONENT_ENABLED_STATE_DISABLED; mPm.setComponentEnabledSetting(cn, state, PackageManager.DONT_KILL_APP); } private void modifyState(@FeatureFlags int key, @Nullable String name, boolean enabled) { ComponentName cn = getComponentName(key, name); if (cn == null) return; int state = enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : PackageManager.COMPONENT_ENABLED_STATE_DISABLED; mPm.setComponentEnabledSetting(cn, state, PackageManager.DONT_KILL_APP); } @Nullable private ComponentName getComponentName(@FeatureFlags int key, @Nullable Class clazz) { if (clazz == null) return null; ComponentName cn = sComponentCache.get(key); if (cn == null) { cn = new ComponentName(mPackageName, clazz.getName()); sComponentCache.put(key, cn); } return cn; } @Nullable private ComponentName getComponentName(@FeatureFlags int key, @Nullable String name) { if (name == null) return null; ComponentName cn = sComponentCache.get(key); if (cn == null) { cn = new ComponentName(mPackageName, name); sComponentCache.put(key, cn); } return cn; } private boolean isComponentEnabled(@Nullable ComponentName componentName) { if (componentName == null) return true; int status = mPm.getComponentEnabledSetting(componentName); return status == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT || status == PackageManager.COMPONENT_ENABLED_STATE_ENABLED; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/settings/FileManagerPreferences.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.settings; import android.content.ComponentName; import android.content.ContentResolver; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; import android.text.InputType; import android.text.TextUtils; import android.view.inputmethod.EditorInfo; import androidx.annotation.Nullable; import androidx.core.view.inputmethod.EditorInfoCompat; import androidx.preference.Preference; import androidx.preference.SwitchPreferenceCompat; import com.google.android.material.transition.MaterialSharedAxis; import java.io.File; import java.util.Objects; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.fm.FmActivity; import io.github.muntashirakon.AppManager.fm.FmUtils; import io.github.muntashirakon.dialog.TextInputDialogBuilder; public class FileManagerPreferences extends PreferenceFragment { @Override public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { setPreferencesFromResource(R.xml.preferences_file_manager, rootKey); getPreferenceManager().setPreferenceDataStore(new SettingsDataStore()); // Display in launcher SwitchPreferenceCompat displayInLauncherPref = Objects.requireNonNull(findPreference("fm_display_in_launcher")); displayInLauncherPref.setChecked(Prefs.FileManager.displayInLauncher()); displayInLauncherPref.setOnPreferenceChangeListener((preference, newValue) -> { boolean isChecked = (boolean) newValue; int newState = isChecked ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : PackageManager.COMPONENT_ENABLED_STATE_DISABLED; ComponentName componentName = new ComponentName(BuildConfig.APPLICATION_ID, FmActivity.LAUNCHER_ALIAS); requireContext().getPackageManager().setComponentEnabledSetting(componentName, newState, PackageManager.DONT_KILL_APP); return true; }); // Remember last opened path SwitchPreferenceCompat filesRememberLastPathPref = Objects.requireNonNull(findPreference("fm_remember_last_path")); filesRememberLastPathPref.setChecked(Prefs.FileManager.isRememberLastOpenedPath()); // Set home Preference setHomePrefs = Objects.requireNonNull(findPreference("fm_home")); setHomePrefs.setSummary(FmUtils.getDisplayablePath(Prefs.FileManager.getHome())); setHomePrefs.setOnPreferenceClickListener(preference -> { new TextInputDialogBuilder(requireContext(), null) .setTitle(R.string.pref_set_home) .setInputText(FmUtils.getDisplayablePath(Prefs.FileManager.getHome())) .setInputInputType(InputType.TYPE_CLASS_TEXT) .setInputImeOptions(EditorInfo.IME_ACTION_DONE | EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING) .setNegativeButton(R.string.cancel, null) .setPositiveButton(R.string.ok, (dialog, which, inputText, isChecked) -> { if (TextUtils.isEmpty(inputText)) { return; } String newHome = inputText.toString(); Uri uri; if (newHome.startsWith(File.separator)) { uri = new Uri.Builder().scheme(ContentResolver.SCHEME_FILE).path(newHome).build(); } else uri = Uri.parse(newHome); Prefs.FileManager.setHome(uri); setHomePrefs.setSummary(FmUtils.getDisplayablePath(uri)); }) .show(); return true; }); } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setEnterTransition(new MaterialSharedAxis(MaterialSharedAxis.Z, true)); setReturnTransition(new MaterialSharedAxis(MaterialSharedAxis.Z, false)); } @Override public int getTitle() { return R.string.files; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/settings/ImportExportRulesPreferences.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.settings; import android.os.Bundle; import android.os.UserHandleHidden; import android.text.SpannableStringBuilder; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.preference.Preference; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.transition.MaterialSharedAxis; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.concurrent.Future; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.db.entity.App; import io.github.muntashirakon.AppManager.db.utils.AppDb; import io.github.muntashirakon.AppManager.oneclickops.ItemCount; import io.github.muntashirakon.AppManager.rules.RulesTypeSelectionDialogFragment; import io.github.muntashirakon.AppManager.rules.compontents.ExternalComponentsImporter; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.users.Users; import io.github.muntashirakon.AppManager.utils.DateUtils; import io.github.muntashirakon.AppManager.utils.PackageUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.dialog.SearchableItemsDialogBuilder; import io.github.muntashirakon.dialog.SearchableMultiChoiceDialogBuilder; import io.github.muntashirakon.view.ProgressIndicatorCompat; public class ImportExportRulesPreferences extends PreferenceFragment { private static final String MIME_JSON = "application/json"; private static final String MIME_TSV = "text/tab-separated-values"; private static final String MIME_XML = "text/xml"; private final int mUserHandle = UserHandleHidden.myUserId(); private SettingsActivity mActivity; @Nullable private Future importExistingFuture; private final ActivityResultLauncher mExportRules = registerForActivityResult( new ActivityResultContracts.CreateDocument(MIME_TSV), uri -> { if (uri == null) { // Back button pressed. return; } RulesTypeSelectionDialogFragment dialogFragment = new RulesTypeSelectionDialogFragment(); Bundle args = new Bundle(); args.putInt(RulesTypeSelectionDialogFragment.ARG_MODE, RulesTypeSelectionDialogFragment.MODE_EXPORT); args.putParcelable(RulesTypeSelectionDialogFragment.ARG_URI, uri); args.putStringArrayList(RulesTypeSelectionDialogFragment.ARG_PKG, null); args.putIntArray(RulesTypeSelectionDialogFragment.ARG_USERS, Users.getUsersIds()); dialogFragment.setArguments(args); dialogFragment.show(getParentFragmentManager(), RulesTypeSelectionDialogFragment.TAG); }); private final ActivityResultLauncher mImportRules = registerForActivityResult( new ActivityResultContracts.GetContent(), uri -> { if (uri == null) { // Back button pressed. return; } RulesTypeSelectionDialogFragment dialogFragment = new RulesTypeSelectionDialogFragment(); Bundle args = new Bundle(); args.putInt(RulesTypeSelectionDialogFragment.ARG_MODE, RulesTypeSelectionDialogFragment.MODE_IMPORT); args.putParcelable(RulesTypeSelectionDialogFragment.ARG_URI, uri); args.putStringArrayList(RulesTypeSelectionDialogFragment.ARG_PKG, null); args.putIntArray(RulesTypeSelectionDialogFragment.ARG_USERS, Users.getUsersIds()); dialogFragment.setArguments(args); dialogFragment.show(getParentFragmentManager(), RulesTypeSelectionDialogFragment.TAG); }); private final ActivityResultLauncher mImportFromWatt = registerForActivityResult( new ActivityResultContracts.GetMultipleContents(), uris -> { if (uris == null || uris.isEmpty()) { // Back button pressed. return; } ThreadUtils.postOnBackgroundThread(() -> { List failedFiles = ExternalComponentsImporter.applyFromWatt(uris, Users.getUsersIds()); ThreadUtils.postOnMainThread(() -> displayImportExternalRulesFailedPackagesDialog(failedFiles)); }); }); private final ActivityResultLauncher mImportFromBlocker = registerForActivityResult( new ActivityResultContracts.GetMultipleContents(), uris -> { if (uris == null || uris.isEmpty()) { // Back button pressed. return; } ThreadUtils.postOnBackgroundThread(() -> { List failedFiles = ExternalComponentsImporter.applyFromBlocker(uris, Users.getUsersIds()); ThreadUtils.postOnMainThread(() -> displayImportExternalRulesFailedPackagesDialog(failedFiles)); }); }); @Override public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { addPreferencesFromResource(R.xml.preferences_rules_import_export); getPreferenceManager().setPreferenceDataStore(new SettingsDataStore()); mActivity = (SettingsActivity) requireActivity(); ((Preference) Objects.requireNonNull(findPreference("export"))) .setOnPreferenceClickListener(preference -> { final String fileName = "app_manager_rules_export-" + DateUtils.formatDateTime(mActivity, System.currentTimeMillis()) + ".am.tsv"; mExportRules.launch(fileName); return true; }); ((Preference) Objects.requireNonNull(findPreference("import"))) .setOnPreferenceClickListener(preference -> { mImportRules.launch(MIME_TSV); return true; }); ((Preference) Objects.requireNonNull(findPreference("import_existing"))) .setOnPreferenceClickListener(preference -> { new MaterialAlertDialogBuilder(requireActivity()) .setTitle(R.string.pref_import_existing) .setMessage(R.string.apply_to_system_apps_question) .setPositiveButton(R.string.no, (dialog, which) -> importExistingRules(false)) .setNegativeButton(R.string.yes, ((dialog, which) -> importExistingRules(true))) .show(); return true; }); ((Preference) Objects.requireNonNull(findPreference("import_watt"))) .setOnPreferenceClickListener(preference -> { mImportFromWatt.launch(MIME_XML); return true; }); ((Preference) Objects.requireNonNull(findPreference("import_blocker"))) .setOnPreferenceClickListener(preference -> { mImportFromBlocker.launch(MIME_JSON); return true; }); } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setEnterTransition(new MaterialSharedAxis(MaterialSharedAxis.Z, true)); setReturnTransition(new MaterialSharedAxis(MaterialSharedAxis.Z, false)); } @Override public void onDestroy() { if (importExistingFuture != null) { importExistingFuture.cancel(true); } super.onDestroy(); } @Override public int getTitle() { return R.string.pref_import_export_blocking_rules; } private void importExistingRules(final boolean systemApps) { if (!SelfPermissions.canModifyAppComponentStates(UserHandleHidden.myUserId(), null, true)) { UIUtils.displayShortToast(R.string.only_works_in_root_or_adb_mode); return; } ProgressIndicatorCompat.setVisibility(mActivity.progressIndicator, true); importExistingFuture = ThreadUtils.postOnBackgroundThread(() -> { final List itemCounts = new ArrayList<>(); ItemCount itemCount; for (App app : new AppDb().getAllInstalledApplications()) { if (ThreadUtils.isInterrupted()) return; if (!systemApps && app.isSystemApp()) continue; itemCount = new ItemCount(); itemCount.packageName = app.packageName; itemCount.packageLabel = app.packageLabel; itemCount.count = PackageUtils.getUserDisabledComponentsForPackage(app.packageName, mUserHandle).size(); if (itemCount.count > 0) itemCounts.add(itemCount); } ThreadUtils.postOnMainThread(() -> displayImportExistingRulesPackageSelectionDialog(itemCounts)); }); } private void displayImportExistingRulesPackageSelectionDialog(@NonNull List itemCounts) { if (itemCounts.isEmpty()) { ProgressIndicatorCompat.setVisibility(mActivity.progressIndicator, false); UIUtils.displayShortToast(R.string.no_matching_package_found); return; } final List packages = new ArrayList<>(); final CharSequence[] packagesWithItemCounts = new CharSequence[itemCounts.size()]; ItemCount itemCount; for (int i = 0; i < itemCounts.size(); ++i) { itemCount = itemCounts.get(i); packages.add(itemCount.packageName); packagesWithItemCounts[i] = new SpannableStringBuilder(itemCount.packageLabel).append("\n") .append(UIUtils.getSmallerText(UIUtils.getSecondaryText(mActivity, getResources() .getQuantityString(R.plurals.no_of_components, itemCount.count, itemCount.count)))); } ProgressIndicatorCompat.setVisibility(mActivity.progressIndicator, false); new SearchableMultiChoiceDialogBuilder<>(requireActivity(), packages, packagesWithItemCounts) .setTitle(R.string.filtered_packages) .setPositiveButton(R.string.apply, (dialog, which, selectedPackages) -> { ProgressIndicatorCompat.setVisibility(mActivity.progressIndicator, true); ThreadUtils.postOnBackgroundThread(() -> { List failedPackages = ExternalComponentsImporter .applyFromExistingBlockList(selectedPackages, mUserHandle); ThreadUtils.postOnMainThread(() -> displayImportExistingRulesFailedPackagesDialog(failedPackages)); }); }) .setNegativeButton(R.string.cancel, null) .show(); } private void displayImportExistingRulesFailedPackagesDialog(@NonNull List failedPackages) { if (isDetached()) { if (failedPackages.isEmpty()) { UIUtils.displayShortToast(R.string.the_import_was_successful); } else { UIUtils.displayShortToast(R.string.failed); } return; } ProgressIndicatorCompat.setVisibility(mActivity.progressIndicator, false); if (failedPackages.isEmpty()) { UIUtils.displayShortToast(R.string.the_import_was_successful); return; } new SearchableItemsDialogBuilder<>(requireActivity(), failedPackages) .setTitle(R.string.failed_packages) .setNegativeButton(R.string.ok, null) .show(); } private void displayImportExternalRulesFailedPackagesDialog(@NonNull List failedFiles) { if (isDetached()) { if (failedFiles.isEmpty()) { UIUtils.displayShortToast(R.string.the_import_was_successful); } else { UIUtils.displayLongToastPl(R.plurals.failed_to_import_files, failedFiles.size(), failedFiles.size()); } return; } ProgressIndicatorCompat.setVisibility(mActivity.progressIndicator, false); if (failedFiles.isEmpty()) { UIUtils.displayShortToast(R.string.the_import_was_successful); return; } new SearchableItemsDialogBuilder<>(requireActivity(), failedFiles) .setTitle(getResources().getQuantityString(R.plurals.failed_to_import_files, failedFiles.size(), failedFiles.size())) .setNegativeButton(R.string.close, null) .show(); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/settings/InstallerPreferences.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.settings; import static io.github.muntashirakon.AppManager.utils.UIUtils.getSecondaryText; import static io.github.muntashirakon.AppManager.utils.UIUtils.getSmallerText; import android.Manifest; import android.annotation.SuppressLint; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.UserHandleHidden; import android.text.InputType; import android.text.SpannableStringBuilder; import android.view.View; import android.view.inputmethod.EditorInfo; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.util.Pair; import androidx.core.view.inputmethod.EditorInfoCompat; import androidx.lifecycle.ViewModelProvider; import androidx.preference.Preference; import androidx.preference.PreferenceCategory; import androidx.preference.SwitchPreferenceCompat; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.transition.MaterialSharedAxis; import java.util.ArrayList; import java.util.List; import java.util.Objects; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.apk.signing.Signer; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.utils.PackageUtils; import io.github.muntashirakon.AppManager.utils.Utils; import io.github.muntashirakon.dialog.ScrollableDialogBuilder; import io.github.muntashirakon.dialog.SearchableSingleChoiceDialogBuilder; import io.github.muntashirakon.dialog.TextInputDialogBuilder; import io.github.muntashirakon.preference.TopSwitchPreference; public class InstallerPreferences extends PreferenceFragment { public static final Integer[] INSTALL_LOCATIONS = new Integer[] { PackageInfo.INSTALL_LOCATION_AUTO, PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY, PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL }; public static final int[] INSTALL_LOCATION_NAMES = new int[]{ R.string.auto, // PackageInfo.INSTALL_LOCATION_AUTO R.string.install_location_internal_only, // PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY R.string.install_location_prefer_external, // PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL }; @SuppressLint("InlinedApi") public static final Integer[] PKG_SOURCES = new Integer[] { PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED, PackageInstaller.PACKAGE_SOURCE_OTHER, PackageInstaller.PACKAGE_SOURCE_STORE, PackageInstaller.PACKAGE_SOURCE_LOCAL_FILE, PackageInstaller.PACKAGE_SOURCE_DOWNLOADED_FILE, }; public static final int[] PKG_SOURCES_NAMES = new int[]{ R.string._undefined, // PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED R.string.package_source_other, // PackageInstaller.PACKAGE_SOURCE_OTHER R.string.package_source_store, // PackageInstaller.PACKAGE_SOURCE_STORE R.string.package_source_local_file, // PackageInstaller.PACKAGE_SOURCE_LOCAL_FILE R.string.package_source_downloaded_file, // PackageInstaller.PACKAGE_SOURCE_DOWNLOADED_FILE }; private SettingsActivity mActivity; private PackageManager mPm; private String mInstallerApp; private Preference mInstallerAppPref; private MainPreferencesViewModel mModel; @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.preferences_installer, rootKey); getPreferenceManager().setPreferenceDataStore(new SettingsDataStore()); mModel = new ViewModelProvider(requireActivity()).get(MainPreferencesViewModel.class); mActivity = (SettingsActivity) requireActivity(); mPm = mActivity.getPackageManager(); boolean isInstallerEnabled = FeatureController.isInstallerEnabled(); PreferenceCategory catGeneral = requirePreference("cat_general"); PreferenceCategory catAdvanced = requirePreference("cat_advanced"); TopSwitchPreference useInstaller = requirePreference("use_installer"); useInstaller.setChecked(isInstallerEnabled); useInstaller.setOnPreferenceChangeListener((preference, newValue) -> { boolean isEnabled = (boolean) newValue; enablePrefs(isEnabled, catGeneral, catAdvanced); FeatureController.getInstance().modifyState(FeatureController.FEAT_INSTALLER, isEnabled); return true; }); enablePrefs(isInstallerEnabled, catGeneral, catAdvanced); // Set installation locations Preference installLocationPref = Objects.requireNonNull(findPreference("installer_install_location")); installLocationPref.setSummary(INSTALL_LOCATION_NAMES[Prefs.Installer.getInstallLocation()]); installLocationPref.setOnPreferenceClickListener(preference -> { CharSequence[] installLocationTexts = new CharSequence[INSTALL_LOCATION_NAMES.length]; for (int i = 0; i < INSTALL_LOCATION_NAMES.length; ++i) { installLocationTexts[i] = getString(INSTALL_LOCATION_NAMES[i]); } int defaultChoice = Prefs.Installer.getInstallLocation(); new SearchableSingleChoiceDialogBuilder<>(requireActivity(), INSTALL_LOCATIONS, installLocationTexts) .setTitle(R.string.install_location) .setSelection(defaultChoice) .setPositiveButton(R.string.save, (dialog, which, newInstallLocation) -> { Objects.requireNonNull(newInstallLocation); Prefs.Installer.setInstallLocation(newInstallLocation); installLocationPref.setSummary(INSTALL_LOCATION_NAMES[newInstallLocation]); }) .setNegativeButton(R.string.cancel, null) .show(); return true; }); // Set installer app mInstallerAppPref = Objects.requireNonNull(findPreference("installer_installer_app")); mInstallerAppPref.setEnabled(SelfPermissions.checkSelfOrRemotePermission(Manifest.permission.INSTALL_PACKAGES)); mInstallerApp = Prefs.Installer.getInstallerPackageName(); mInstallerAppPref.setSummary(PackageUtils.getPackageLabel(mPm, mInstallerApp)); mInstallerAppPref.setOnPreferenceClickListener(preference -> { new MaterialAlertDialogBuilder(requireActivity()) .setTitle(R.string.installer_app) .setMessage(R.string.installer_app_message) .setPositiveButton(R.string.choose, (dialog1, which1) -> { mActivity.progressIndicator.show(); mModel.loadPackageNameLabelPair(); }) .setNegativeButton(R.string.specify_custom_name, (dialog, which) -> new TextInputDialogBuilder(requireActivity(), R.string.installer_app) .setTitle(R.string.installer_app) .setInputText(mInstallerApp) .setInputInputType(InputType.TYPE_CLASS_TEXT) .setInputImeOptions(EditorInfo.IME_ACTION_DONE | EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING) .setPositiveButton(R.string.ok, (dialog1, which1, inputText, isChecked) -> { if (inputText == null) return; mInstallerApp = inputText.toString().trim(); Prefs.Installer.setInstallerPackageName(mInstallerApp); mInstallerAppPref.setSummary(PackageUtils.getPackageLabel(mPm, mInstallerApp)); }) .setNegativeButton(R.string.cancel, null) .show()) .setNeutralButton(R.string.reset_to_default, (dialog, which) -> { Prefs.Installer.setInstallerPackageName(mInstallerApp = BuildConfig.APPLICATION_ID); mInstallerAppPref.setSummary(PackageUtils.getPackageLabel(mPm, mInstallerApp)); }) .show(); return true; }); // Disable verification SwitchPreferenceCompat disableVerification = Objects.requireNonNull(findPreference("installer_disable_verification")); disableVerification.setEnabled(SelfPermissions.isSystemOrRootOrShell()); disableVerification.setChecked(Prefs.Installer.isDisableApkVerification()); // Update ownership SwitchPreferenceCompat updateOwnership = Objects.requireNonNull(findPreference("installer_update_ownership")); updateOwnership.setVisible(Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE); updateOwnership.setChecked(Prefs.Installer.requestUpdateOwnership()); // Package source Preference pkgSource = Objects.requireNonNull(findPreference("installer_default_pkg_source")); pkgSource.setVisible(Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU); pkgSource.setSummary(PKG_SOURCES_NAMES[Prefs.Installer.getPackageSource()]); pkgSource.setOnPreferenceClickListener(preference -> { CharSequence[] pkgSourceTexts = new CharSequence[PKG_SOURCES_NAMES.length]; for (int i = 0; i < PKG_SOURCES_NAMES.length; ++i) { pkgSourceTexts[i] = getString(PKG_SOURCES_NAMES[i]); } int defaultChoice = Prefs.Installer.getPackageSource(); new SearchableSingleChoiceDialogBuilder<>(requireActivity(), PKG_SOURCES, pkgSourceTexts) .setTitle(R.string.pref_default_package_source) .setSelection(defaultChoice) .setPositiveButton(R.string.save, (dialog, which, newPkgSource) -> { Objects.requireNonNull(newPkgSource); Prefs.Installer.setPackageSource(newPkgSource); pkgSource.setSummary(PKG_SOURCES_NAMES[newPkgSource]); }) .setNegativeButton(R.string.cancel, null) .show(); return true; }); // Set origin SwitchPreferenceCompat setOrigin = Objects.requireNonNull(findPreference("installer_set_origin")); setOrigin.setChecked(Prefs.Installer.isSetOriginatingPackage()); // Sign apk before installing SwitchPreferenceCompat signApk = Objects.requireNonNull(findPreference("installer_sign_apk")); signApk.setChecked(Prefs.Installer.canSignApk()); signApk.setOnPreferenceChangeListener((preference, enabled) -> { if ((boolean) enabled && !Signer.canSign()) { new ScrollableDialogBuilder(requireActivity()) .setTitle(R.string.pref_sign_apk_no_signing_key) .setMessage(R.string.pref_sign_apk_error_signing_key_not_added) .enableAnchors() .setPositiveButton(R.string.add, (dialog, which, isChecked) -> { Intent intent = new Intent(Intent.ACTION_VIEW) .setData(Uri.parse("app-manager://settings/apk_signing_prefs/signing_keys")); startActivity(intent); }) .setNegativeButton(R.string.cancel, null) .show(); return false; } return true; }); SwitchPreferenceCompat forceDexOpt = Objects.requireNonNull(findPreference("installer_force_dex_opt")); forceDexOpt.setVisible(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N); forceDexOpt.setChecked(Prefs.Installer.forceDexOpt()); // Display changes ((SwitchPreferenceCompat) Objects.requireNonNull(findPreference("installer_display_changes"))) .setChecked(Prefs.Installer.displayChanges()); // Block trackers SwitchPreferenceCompat blockTrackersPref = Objects.requireNonNull(findPreference("installer_block_trackers")); blockTrackersPref.setVisible(SelfPermissions.canModifyAppComponentStates(UserHandleHidden.myUserId(), null, true)); blockTrackersPref.setChecked(Prefs.Installer.blockTrackers()); // Running installer in the background SwitchPreferenceCompat backgroundPref = Objects.requireNonNull(findPreference("installer_always_on_background")); backgroundPref.setVisible(Utils.canDisplayNotification(requireContext())); backgroundPref.setChecked(Prefs.Installer.installInBackground()); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); // Observe installer app selection mModel.getPackageNameLabelPairLiveData().observe(getViewLifecycleOwner(), this::displayInstallerAppSelectionDialog); } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setEnterTransition(new MaterialSharedAxis(MaterialSharedAxis.Z, true)); setReturnTransition(new MaterialSharedAxis(MaterialSharedAxis.Z, false)); } @Override public int getTitle() { return R.string.installer; } public void displayInstallerAppSelectionDialog(@NonNull List> appInfo) { ArrayList items = new ArrayList<>(appInfo.size()); ArrayList itemNames = new ArrayList<>(appInfo.size()); for (Pair pair : appInfo) { items.add(pair.first); itemNames.add(new SpannableStringBuilder(pair.second) .append("\n") .append(getSecondaryText(requireContext(), getSmallerText(pair.first)))); } mActivity.progressIndicator.hide(); new SearchableSingleChoiceDialogBuilder<>(requireActivity(), items, itemNames) .setTitle(R.string.installer_app) .setSelection(mInstallerApp) .setPositiveButton(R.string.save, (dialog, which, selectedInstallerApp) -> { if (selectedInstallerApp != null) { mInstallerApp = selectedInstallerApp; Prefs.Installer.setInstallerPackageName(mInstallerApp); mInstallerAppPref.setSummary(PackageUtils.getPackageLabel(mPm, mInstallerApp)); } }) .setNegativeButton(R.string.cancel, null) .show(); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/settings/LogViewerPreferences.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.settings; import android.app.Activity; import android.content.Intent; import android.graphics.Typeface; import android.os.Bundle; import android.text.InputType; import android.util.Log; import android.view.inputmethod.EditorInfo; import androidx.annotation.Nullable; import androidx.core.view.inputmethod.EditorInfoCompat; import androidx.fragment.app.FragmentActivity; import androidx.preference.Preference; import androidx.preference.PreferenceCategory; import androidx.preference.SwitchPreferenceCompat; import com.google.android.material.transition.MaterialSharedAxis; import java.util.Arrays; import java.util.List; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.logcat.helper.LogcatHelper; import io.github.muntashirakon.AppManager.logcat.helper.PreferenceHelper; import io.github.muntashirakon.AppManager.logcat.struct.LogLine; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.dialog.SearchableMultiChoiceDialogBuilder; import io.github.muntashirakon.dialog.SearchableSingleChoiceDialogBuilder; import io.github.muntashirakon.dialog.TextInputDialogBuilder; import io.github.muntashirakon.preference.TopSwitchPreference; public class LogViewerPreferences extends PreferenceFragment { public static final List LOG_LEVEL_VALUES = Arrays.asList(Log.VERBOSE, Log.DEBUG, Log.INFO, Log.WARN, Log.ERROR, LogLine.LOG_FATAL); public static final List LOG_BUFFER_NAMES = Arrays.asList(LogcatHelper.BUFFER_MAIN, LogcatHelper.BUFFER_RADIO, LogcatHelper.BUFFER_EVENTS, LogcatHelper.BUFFER_SYSTEM, LogcatHelper.BUFFER_CRASH); public static final List LOG_BUFFERS = Arrays.asList(LogcatHelper.LOG_ID_MAIN, LogcatHelper.LOG_ID_RADIO, LogcatHelper.LOG_ID_EVENTS, LogcatHelper.LOG_ID_SYSTEM, LogcatHelper.LOG_ID_CRASH); private static final int MAX_LOG_WRITE_PERIOD = 1000; private static final int MIN_LOG_WRITE_PERIOD = 1; private static final int MAX_DISPLAY_LIMIT = 100000; private static final int MIN_DISPLAY_LIMIT = 1000; @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { addPreferencesFromResource(R.xml.preferences_log_viewer); getPreferenceManager().setPreferenceDataStore(new SettingsDataStore()); FragmentActivity activity = requireActivity(); boolean isLogViewerEnabled = FeatureController.isLogViewerEnabled(); PreferenceCategory catAppearance = requirePreference("cat_appearance"); PreferenceCategory catConf = requirePreference("cat_conf"); PreferenceCategory catAdvanced = requirePreference("cat_advanced"); TopSwitchPreference useLogViewer = requirePreference("use_log_viewer"); useLogViewer.setChecked(isLogViewerEnabled); useLogViewer.setOnPreferenceChangeListener((preference, newValue) -> { boolean isEnabled = (boolean) newValue; enablePrefs(isEnabled, catAppearance, catAdvanced, catConf); FeatureController.getInstance().modifyState(FeatureController.FEAT_LOG_VIEWER, isEnabled); return true; }); enablePrefs(isLogViewerEnabled, catAppearance, catAdvanced, catConf); SwitchPreferenceCompat expandByDefault = requirePreference("log_viewer_expand_by_default"); expandByDefault.setChecked(Prefs.LogViewer.expandByDefault()); SwitchPreferenceCompat showPidTidTimestamp = requirePreference("log_viewer_show_pid_tid_timestamp"); showPidTidTimestamp.setChecked(Prefs.LogViewer.showPidTidTimestamp()); SwitchPreferenceCompat omitSensitiveInfo = requirePreference("log_viewer_omit_sensitive_info"); omitSensitiveInfo.setChecked(Prefs.LogViewer.omitSensitiveInfo()); omitSensitiveInfo.setOnPreferenceChangeListener((preference, newValue) -> { LogLine.omitSensitiveInfo = (boolean) newValue; return true; }); Preference filterPattern = requirePreference("log_viewer_filter_pattern"); filterPattern.setOnPreferenceClickListener(preference -> { new TextInputDialogBuilder(activity, null) .setTitle(R.string.pref_filter_pattern_title) .setInputText(Prefs.LogViewer.getFilterPattern()) .setInputTypeface(Typeface.MONOSPACE) .setInputImeOptions(EditorInfo.IME_ACTION_DONE | EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING) .setPositiveButton(R.string.save, (dialog, which, inputText, isChecked) -> { if (inputText == null) return; Prefs.LogViewer.setFilterPattern(inputText.toString().trim()); UIUtils.displayLongToast(R.string.restart_log_viewer_to_see_changes); }) .setNegativeButton(R.string.cancel, null) .setNeutralButton(R.string.reset_to_default, (dialog, which, inputText, isChecked) -> { Prefs.LogViewer.setFilterPattern(activity.getString(R.string.pref_filter_pattern_default)); UIUtils.displayLongToast(R.string.restart_log_viewer_to_see_changes); }) .show(); return true; }); Preference displayLimit = requirePreference("log_viewer_display_limit"); displayLimit.setSummary(getString(R.string.pref_display_limit_summary, Prefs.LogViewer.getDisplayLimit())); displayLimit.setOnPreferenceClickListener(preference -> { new TextInputDialogBuilder(activity, null) .setTitle(R.string.pref_display_limit_title) .setHelperText(getString(R.string.pref_display_limit_hint, MIN_DISPLAY_LIMIT, MAX_DISPLAY_LIMIT)) .setInputText(String.valueOf(Prefs.LogViewer.getDisplayLimit())) .setInputInputType(InputType.TYPE_CLASS_NUMBER) .setInputImeOptions(EditorInfo.IME_ACTION_DONE | EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING) .setPositiveButton(R.string.save, (dialog, which, inputText, isChecked) -> { if (inputText == null) return; try { int displayLimitInt = Integer.parseInt(inputText.toString().trim()); if (displayLimitInt >= MIN_DISPLAY_LIMIT && displayLimitInt <= MAX_DISPLAY_LIMIT) { Prefs.LogViewer.setDisplayLimit(displayLimitInt); UIUtils.displayLongToast(R.string.restart_log_viewer_to_see_changes); displayLimit.setSummary(getString(R.string.pref_display_limit_summary, displayLimitInt)); } } catch (NumberFormatException ignore) { } }) .setNegativeButton(R.string.cancel, null) .setNeutralButton(R.string.reset_to_default, (dialog, which, inputText, isChecked) -> { Prefs.LogViewer.setDisplayLimit(LogcatHelper.DEFAULT_DISPLAY_LIMIT); UIUtils.displayLongToast(R.string.restart_log_viewer_to_see_changes); displayLimit.setSummary(getString(R.string.pref_display_limit_summary, Prefs.LogViewer.getDisplayLimit())); }) .show(); return true; }); Preference writePeriod = requirePreference("log_viewer_write_period"); writePeriod.setSummary(getString(R.string.pref_log_write_period_summary, Prefs.LogViewer.getLogWritingInterval())); writePeriod.setOnPreferenceClickListener(preference -> { new TextInputDialogBuilder(activity, null) .setTitle(R.string.pref_log_write_period_title) .setHelperText(getString(R.string.pref_log_line_period_error)) .setInputText(String.valueOf(Prefs.LogViewer.getLogWritingInterval())) .setInputInputType(InputType.TYPE_CLASS_NUMBER) .setInputImeOptions(EditorInfo.IME_ACTION_DONE | EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING) .setPositiveButton(R.string.save, (dialog, which, inputText, isChecked) -> { if (inputText == null) return; try { int writePeriodInt = Integer.parseInt(inputText.toString().trim()); if (writePeriodInt >= MIN_LOG_WRITE_PERIOD && writePeriodInt <= MAX_LOG_WRITE_PERIOD) { Prefs.LogViewer.setLogWritingInterval(writePeriodInt); UIUtils.displayLongToast(R.string.restart_log_viewer_to_see_changes); writePeriod.setSummary(getString(R.string.pref_log_write_period_summary, writePeriodInt)); } } catch (NumberFormatException ignore) { } }) .setNegativeButton(R.string.cancel, null) .setNeutralButton(R.string.reset_to_default, (dialog, which, inputText, isChecked) -> { Prefs.LogViewer.setLogWritingInterval(LogcatHelper.DEFAULT_LOG_WRITE_INTERVAL); UIUtils.displayLongToast(R.string.restart_log_viewer_to_see_changes); writePeriod.setSummary(getString(R.string.pref_log_write_period_summary, Prefs.LogViewer.getLogWritingInterval())); }) .show(); return true; }); Preference logLevel = requirePreference("log_viewer_default_log_level"); logLevel.setOnPreferenceClickListener(preference -> { CharSequence[] logLevelsLocalised = getResources().getStringArray(R.array.log_levels); new SearchableSingleChoiceDialogBuilder<>(activity, LOG_LEVEL_VALUES, logLevelsLocalised) .setTitle(R.string.pref_default_log_level_title) .setSelection(Prefs.LogViewer.getLogLevel()) .setPositiveButton(R.string.save, (dialog, which, newLogLevel) -> { if (newLogLevel != null) { Prefs.LogViewer.setLogLevel(newLogLevel); } }) .setNegativeButton(R.string.cancel, null) .setNeutralButton(R.string.reset_to_default, (dialog, which, newLogLevel) -> Prefs.LogViewer.setLogLevel(Log.VERBOSE)) .show(); return true; }); Preference logBuffers = requirePreference("log_viewer_buffer"); logBuffers.setOnPreferenceClickListener(preference -> { new SearchableMultiChoiceDialogBuilder<>(activity, LOG_BUFFERS, LOG_BUFFER_NAMES) .setTitle(R.string.pref_buffer_title) .addSelections(PreferenceHelper.getBuffers()) .setPositiveButton(R.string.save, (dialog, which, selectedItems) -> { if (selectedItems.isEmpty()) return; int bufferFlags = 0; for (int flag : selectedItems) { bufferFlags |= flag; } int previousFlags = Prefs.LogViewer.getBuffers(); Prefs.LogViewer.setBuffers(bufferFlags); if (previousFlags != bufferFlags) { sendBufferChanged(activity); } }) .setNegativeButton(R.string.cancel, null) .setNeutralButton(R.string.reset_to_default, (dialog, which, selectedItems) -> { int previousFlags = Prefs.LogViewer.getBuffers(); Prefs.LogViewer.setBuffers(LogcatHelper.LOG_ID_DEFAULT); if (previousFlags != LogcatHelper.LOG_ID_DEFAULT) { sendBufferChanged(activity); } }) .show(); return true; }); } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setEnterTransition(new MaterialSharedAxis(MaterialSharedAxis.Z, true)); setReturnTransition(new MaterialSharedAxis(MaterialSharedAxis.Z, false)); } public static void sendBufferChanged(FragmentActivity activity) { Intent intent = new Intent().putExtra("bufferChanged", true); activity.setResult(Activity.RESULT_FIRST_USER, intent); } @Override public int getTitle() { return R.string.log_viewer; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/settings/MainPreferences.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.settings; import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.FragmentActivity; import androidx.lifecycle.ViewModelProvider; import androidx.preference.Preference; import java.util.Arrays; import java.util.List; import java.util.Locale; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.self.life.BuildExpiryChecker; import io.github.muntashirakon.AppManager.self.life.FundingCampaignChecker; import io.github.muntashirakon.AppManager.utils.LangUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.preference.InfoAlertPreference; import io.github.muntashirakon.preference.WarningAlertPreference; public class MainPreferences extends PreferenceFragment { @NonNull public static MainPreferences getInstance(@Nullable String key, boolean dualPane) { MainPreferences preferences = new MainPreferences(); Bundle args = new Bundle(); args.putString(PREF_KEY, key); args.putBoolean(PREF_SECONDARY, dualPane); preferences.setArguments(args); return preferences; } private static final List MODE_NAMES = Arrays.asList( Ops.MODE_AUTO, Ops.MODE_ROOT, Ops.MODE_ADB_OVER_TCP, Ops.MODE_ADB_WIFI, Ops.MODE_NO_ROOT); private FragmentActivity mActivity; private Preference mModePref; private Preference mLocalePref; private String[] mModes; @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.preferences_main, rootKey); getPreferenceManager().setPreferenceDataStore(new SettingsDataStore()); MainPreferencesViewModel model = new ViewModelProvider(requireActivity()).get(MainPreferencesViewModel.class); mActivity = requireActivity(); // Expiry notice WarningAlertPreference buildExpiringNotice = requirePreference("app_manager_expiring_notice"); buildExpiringNotice.setVisible(!Boolean.FALSE.equals(BuildExpiryChecker.buildExpired())); // Funding campaign notice InfoAlertPreference fundingCampaignNotice = requirePreference("funding_campaign_notice"); fundingCampaignNotice.setVisible(FundingCampaignChecker.campaignRunning()); // Custom locale mLocalePref = requirePreference("custom_locale"); // Mode of operation mModePref = requirePreference("mode_of_operations"); mModes = getResources().getStringArray(R.array.modes); model.getOperationCompletedLiveData().observe(requireActivity(), completed -> { if (requireActivity() instanceof SettingsActivity) { ((SettingsActivity) requireActivity()).progressIndicator.hide(); } UIUtils.displayShortToast(R.string.the_operation_was_successful); }); } @Override public void onStart() { super.onStart(); if (mModePref != null) { mModePref.setSummary(getString(R.string.mode_of_op_with_inferred_mode_of_op, mModes[MODE_NAMES.indexOf(Ops.getMode())], Ops.getInferredMode(mActivity))); } if (mLocalePref != null) { mLocalePref.setSummary(getLanguageName()); } } @Override public int getTitle() { return R.string.settings; } public CharSequence getLanguageName() { String langTag = Prefs.Appearance.getLanguage(); if (LangUtils.LANG_AUTO.equals(langTag)) { return getString(R.string.auto); } Locale locale = Locale.forLanguageTag(langTag); return locale.getDisplayName(locale); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/settings/MainPreferencesViewModel.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.settings; import android.app.Application; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.net.Uri; import android.os.Build; import android.os.PowerManager; import android.os.UserHandleHidden; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.collection.ArrayMap; import androidx.core.util.Pair; import androidx.documentfile.provider.DocumentFileUtils; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.security.cert.Certificate; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.apk.signing.Signer; import io.github.muntashirakon.AppManager.changelog.Changelog; import io.github.muntashirakon.AppManager.changelog.ChangelogParser; import io.github.muntashirakon.AppManager.crypto.ks.KeyPair; import io.github.muntashirakon.AppManager.crypto.ks.KeyStoreManager; import io.github.muntashirakon.AppManager.db.entity.App; import io.github.muntashirakon.AppManager.db.utils.AppDb; import io.github.muntashirakon.AppManager.misc.DeviceInfo2; import io.github.muntashirakon.AppManager.rules.compontents.ComponentUtils; import io.github.muntashirakon.AppManager.rules.compontents.ComponentsBlocker; import io.github.muntashirakon.AppManager.servermanager.ServerConfig; import io.github.muntashirakon.AppManager.users.UserInfo; import io.github.muntashirakon.AppManager.users.Users; import io.github.muntashirakon.AppManager.utils.CpuUtils; import io.github.muntashirakon.AppManager.utils.DigestUtils; import io.github.muntashirakon.AppManager.utils.StorageUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.lifecycle.SingleLiveEvent; public class MainPreferencesViewModel extends AndroidViewModel implements Ops.AdbConnectionInterface { private final Object mRulesLock = new Object(); private final MutableLiveData> mSelectUsers = new SingleLiveEvent<>(); private final MutableLiveData mChangeLog = new SingleLiveEvent<>(); private final MutableLiveData mDeviceInfo = new SingleLiveEvent<>(); private final MutableLiveData mCustomCommand0 = new SingleLiveEvent<>(); private final MutableLiveData mCustomCommand1 = new SingleLiveEvent<>(); private final MutableLiveData mModeOfOpsStatus = new SingleLiveEvent<>(); private final MutableLiveData mOperationCompletedLiveData = new SingleLiveEvent<>(); private final MutableLiveData> mStorageVolumesLiveData = new SingleLiveEvent<>(); private final MutableLiveData mSigningKeySha256HashLiveData = new SingleLiveEvent<>(); private final MutableLiveData>> mPackageNameLabelPairLiveData = new SingleLiveEvent<>(); private final ExecutorService mExecutor = Executors.newFixedThreadPool(1); public MainPreferencesViewModel(@NonNull Application application) { super(application); } public LiveData> selectUsers() { return mSelectUsers; } public void loadAllUsers() { ThreadUtils.postOnBackgroundThread(() -> mSelectUsers.postValue(Users.getAllUsers())); } public LiveData getChangeLog() { return mChangeLog; } public void loadChangeLog() { ThreadUtils.postOnBackgroundThread(() -> { try { Changelog changelog = new ChangelogParser(getApplication(), R.raw.changelog).parse(); mChangeLog.postValue(changelog); } catch (IOException | XmlPullParserException e) { e.printStackTrace(); } }); } public LiveData getDeviceInfo() { return mDeviceInfo; } public void loadDeviceInfo(@NonNull DeviceInfo2 di) { ThreadUtils.postOnBackgroundThread(() -> { di.loadInfo(); mDeviceInfo.postValue(di); }); } public void reloadApps() { ThreadUtils.postOnBackgroundThread(() -> { PowerManager.WakeLock wakeLock = CpuUtils.getPartialWakeLock("appDbUpdater"); try { wakeLock.acquire(); AppDb appDb = new AppDb(); appDb.deleteAllApplications(); appDb.deleteAllBackups(); appDb.loadInstalledOrBackedUpApplications(getApplication()); } finally { CpuUtils.releaseWakeLock(wakeLock); } }); } public MutableLiveData getCustomCommand0() { return mCustomCommand0; } public MutableLiveData getCustomCommand1() { return mCustomCommand1; } public void loadCustomCommands() { mExecutor.submit(() -> { try { ServerConfig.init(getApplication()); mCustomCommand0.postValue(ServerConfig.getServerRunnerCommand(0)); mCustomCommand1.postValue(ServerConfig.getServerRunnerCommand(1)); } catch (Throwable e) { e.printStackTrace(); mCustomCommand0.postValue(null); mCustomCommand1.postValue(null); } }); } public LiveData getModeOfOpsStatus() { return mModeOfOpsStatus; } public void setModeOfOps() { mExecutor.submit(() -> { int status = Ops.init(getApplication(), true); mModeOfOpsStatus.postValue(status); }); } public LiveData getOperationCompletedLiveData() { return mOperationCompletedLiveData; } public void applyAllRules() { ThreadUtils.postOnBackgroundThread(() -> { synchronized (mRulesLock) { // TODO: 13/8/22 Synchronise in ComponentsBlocker instead of here ComponentsBlocker.applyAllRules(getApplication(), UserHandleHidden.myUserId()); } }); } public void removeAllRules() { ThreadUtils.postOnBackgroundThread(() -> { int[] userHandles = Users.getUsersIds(); List packages = ComponentUtils.getAllPackagesWithRules(getApplication()); for (int userHandle : userHandles) { for (String packageName : packages) { ComponentUtils.removeAllRules(packageName, userHandle); } } mOperationCompletedLiveData.postValue(true); }); } public LiveData> getStorageVolumesLiveData() { return mStorageVolumesLiveData; } public void loadStorageVolumes() { ThreadUtils.postOnBackgroundThread(() -> { ArrayMap locations = StorageUtils.getAllStorageLocations(getApplication()); ArrayMap newLocations = new ArrayMap<>(locations.size()); PackageManager pm = getApplication().getPackageManager(); for (int i = 0; i < locations.size(); ++i) { Uri uri = locations.valueAt(i); String authority = uri.getAuthority(); if (authority != null) { ResolveInfo resolveInfo = DocumentFileUtils.getUriSource(getApplication(), uri); String readableName = resolveInfo != null ? resolveInfo.loadLabel(pm).toString() : locations.keyAt(i); newLocations.put(readableName, locations.valueAt(i)); } else newLocations.put(locations.keyAt(i), locations.valueAt(i)); } mStorageVolumesLiveData.postValue(newLocations); }); } public LiveData getSigningKeySha256HashLiveData() { return mSigningKeySha256HashLiveData; } public void loadSigningKeySha256Hash() { mExecutor.submit(() -> { String hash = null; try { KeyStoreManager keyStoreManager = KeyStoreManager.getInstance(); if (keyStoreManager.containsKey(Signer.SIGNING_KEY_ALIAS)) { KeyPair keyPair = keyStoreManager.getKeyPair(Signer.SIGNING_KEY_ALIAS); if (keyPair != null) { Certificate certificate = keyPair.getCertificate(); hash = DigestUtils.getHexDigest(DigestUtils.SHA_256, certificate.getEncoded()); try { keyPair.destroy(); } catch (Exception ignore) { } } } } catch (Exception e) { e.printStackTrace(); } mSigningKeySha256HashLiveData.postValue(hash); }); } public LiveData>> getPackageNameLabelPairLiveData() { return mPackageNameLabelPairLiveData; } public void loadPackageNameLabelPair() { mExecutor.submit(() -> { List appList = new AppDb().getAllApplications(); Map packageNameLabelMap = new HashMap<>(appList.size()); for (App app : appList) { packageNameLabelMap.put(app.packageName, app.packageLabel); } List> appInfo = new ArrayList<>(); for (String packageName : packageNameLabelMap.keySet()) { appInfo.add(new Pair<>(packageName, packageNameLabelMap.get(packageName))); } Collections.sort(appInfo, (o1, o2) -> o1.second.toString().compareTo(o2.second.toString())); mPackageNameLabelPairLiveData.postValue(appInfo); }); } @RequiresApi(Build.VERSION_CODES.R) public void autoConnectWirelessDebugging() { mExecutor.submit(() -> { int status = Ops.autoConnectWirelessDebugging(getApplication()); mModeOfOpsStatus.postValue(status); }); } @Override public void connectAdb(int port) { mExecutor.submit(() -> { int status = Ops.connectAdb(getApplication(), port, Ops.STATUS_FAILURE); mModeOfOpsStatus.postValue(status); }); } @Override @RequiresApi(Build.VERSION_CODES.R) public void pairAdb() { mExecutor.submit(() -> { int status = Ops.pairAdb(getApplication()); mModeOfOpsStatus.postValue(status); }); } @Override public void onStatusReceived(int status) { mModeOfOpsStatus.postValue(status); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/settings/ModeOfOpsPreference.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.settings; import android.content.res.ColorStateList; import android.os.Build; import android.os.Bundle; import android.os.Process; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.core.widget.TextViewCompat; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import com.google.android.material.button.MaterialButton; import com.google.android.material.color.MaterialColors; import com.google.android.material.textfield.TextInputLayout; import com.google.android.material.textview.MaterialTextView; import java.util.Arrays; import java.util.Collections; import java.util.List; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.ipc.LocalServices; import io.github.muntashirakon.AppManager.servermanager.LocalServer; import io.github.muntashirakon.AppManager.servermanager.ServerConfig; import io.github.muntashirakon.AppManager.users.Users; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.AppManager.utils.Utils; import io.github.muntashirakon.dialog.SearchableSingleChoiceDialogBuilder; import io.github.muntashirakon.util.UiUtils; import io.github.muntashirakon.view.TextInputLayoutCompat; import io.github.muntashirakon.widget.TextInputTextView; public class ModeOfOpsPreference extends Fragment { private static final List MODE_NAMES = Arrays.asList( Ops.MODE_AUTO, Ops.MODE_ROOT, Ops.MODE_ADB_OVER_TCP, Ops.MODE_ADB_WIFI, Ops.MODE_NO_ROOT); private MaterialTextView mInferredModeView; private MaterialTextView mRemoteServerStatusView; private MaterialTextView mRemoteServicesStatusView; private MaterialTextView mModeOfOpsView; private MainPreferencesViewModel mModel; private AlertDialog mModeOfOpsAlertDialog; private String[] mModes; @Ops.Mode private String mCurrentMode; private boolean mConnecting; @Nullable private ColorStateList mColorActive; @Nullable private ColorStateList mColorInactive; @Nullable private ColorStateList mColorError; @DrawableRes private int mIconActive; @DrawableRes private int mIconInactive; @DrawableRes private int mIconProgress; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); mModel = new ViewModelProvider(requireActivity()).get(MainPreferencesViewModel.class); } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_mode_of_ops, container, false); boolean secondary = false; if (getArguments() != null) { secondary = requireArguments().getBoolean(PreferenceFragment.PREF_SECONDARY); requireArguments().remove(PreferenceFragment.PREF_KEY); requireArguments().remove(PreferenceFragment.PREF_SECONDARY); } if (secondary) { UiUtils.applyWindowInsetsAsPadding(view, false, true, false, true); } else UiUtils.applyWindowInsetsAsPaddingNoTop(view); return view; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { mColorActive = MaterialColors.getColorStateListOrNull(view.getContext(), com.google.android.material.R.attr.colorOnPrimaryContainer); mColorInactive = MaterialColors.getColorStateListOrNull(view.getContext(), com.google.android.material.R.attr.colorOutline); mColorError = MaterialColors.getColorStateListOrNull(view.getContext(), com.google.android.material.R.attr.colorOnErrorContainer); mIconActive = R.drawable.ic_check_circle; mIconInactive = io.github.muntashirakon.ui.R.drawable.ic_caution; mIconProgress = R.drawable.ic_sync; mModeOfOpsAlertDialog = UIUtils.getProgressDialog(requireActivity(), getString(R.string.loading), true); mModes = getResources().getStringArray(R.array.modes); mCurrentMode = Ops.getMode(); mInferredModeView = view.findViewById(R.id.inferred_mode); mRemoteServerStatusView = view.findViewById(R.id.remote_server_status); mRemoteServicesStatusView = view.findViewById(R.id.remote_services_status); mModeOfOpsView = view.findViewById(R.id.op_name); MaterialButton changeModeView = view.findViewById(R.id.action_settings); List disabledItems; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R || Utils.isTv(requireContext())) { disabledItems = Collections.singletonList(Ops.MODE_ADB_WIFI); } else disabledItems = null; changeModeView.setOnClickListener(v -> new SearchableSingleChoiceDialogBuilder<>(requireActivity(), MODE_NAMES, mModes) .setTitle(R.string.pref_mode_of_operations) .setSelection(mCurrentMode) .addDisabledItems(disabledItems) .setPositiveButton(R.string.apply, (dialog, which, selectedItem) -> { if (selectedItem != null) { mCurrentMode = selectedItem; if (Ops.MODE_ADB_OVER_TCP.equals(mCurrentMode)) { ServerConfig.setAdbPort(ServerConfig.DEFAULT_ADB_PORT); } Ops.setMode(mCurrentMode); mModeOfOpsAlertDialog.show(); mConnecting = true; updateViews(); mModel.setModeOfOps(); } }) .setNegativeButton(R.string.cancel, null) .show()); TextInputTextView customCommand0 = view.findViewById(android.R.id.text1); TextInputLayout customCommand0Layout = TextInputLayoutCompat.fromTextInputEditText(customCommand0); customCommand0Layout.setEndIconOnClickListener(v -> { CharSequence command = customCommand0.getText(); if (!TextUtils.isEmpty(command)) { Utils.copyToClipboard(requireContext(), "command", command); } }); TextInputTextView customCommand1 = view.findViewById(android.R.id.text2); TextInputLayout customCommand1Layout = TextInputLayoutCompat.fromTextInputEditText(customCommand1); customCommand1Layout.setEndIconOnClickListener(v -> { CharSequence command = customCommand1.getText(); if (!TextUtils.isEmpty(command)) { Utils.copyToClipboard(requireContext(), "command", command); } }); mModel.loadCustomCommands(); updateViews(); // Mode of ops mModel.getModeOfOpsStatus().observe(getViewLifecycleOwner(), status -> { switch (status) { case Ops.STATUS_AUTO_CONNECT_WIRELESS_DEBUGGING: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { updateViews(); mModel.autoConnectWirelessDebugging(); return; } // fall-through case Ops.STATUS_WIRELESS_DEBUGGING_CHOOSER_REQUIRED: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { mModeOfOpsAlertDialog.dismiss(); updateViews(); Ops.connectWirelessDebugging(requireActivity(), mModel); return; } // fall-through case Ops.STATUS_ADB_CONNECT_REQUIRED: mModeOfOpsAlertDialog.dismiss(); updateViews(); Ops.connectAdbInput(requireActivity(), mModel); return; case Ops.STATUS_ADB_PAIRING_REQUIRED: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { mModeOfOpsAlertDialog.dismiss(); updateViews(); Ops.pairAdbInput(requireActivity(), mModel); return; } // fall-through case Ops.STATUS_FAILURE_ADB_NEED_MORE_PERMS: Ops.displayIncompleteUsbDebuggingMessage(requireActivity()); case Ops.STATUS_SUCCESS: case Ops.STATUS_FAILURE: mConnecting = false; mModeOfOpsAlertDialog.dismiss(); mCurrentMode = Ops.getMode(); updateViews(); } }); mModel.getCustomCommand0().observe(getViewLifecycleOwner(), customCommand0::setText); mModel.getCustomCommand1().observe(getViewLifecycleOwner(), customCommand1::setText); } @Override public void onStart() { super.onStart(); requireActivity().setTitle(R.string.pref_mode_of_operations); } private void updateViews() { boolean serverActive = LocalServer.alive(requireContext()); boolean serverRequired = requireRemoteServer(mCurrentMode); boolean servicesActive = LocalServices.alive(); boolean servicesRequired = requireRemoteServices(mCurrentMode); // Mode if (mConnecting) { mInferredModeView.setText(R.string.status_connecting); mInferredModeView.setTextColor(mColorActive); TextViewCompat.setCompoundDrawableTintList(mModeOfOpsView, mColorActive); mModeOfOpsView.setTextColor(mColorActive); mModeOfOpsView.setCompoundDrawablesRelativeWithIntrinsicBounds(mIconProgress, 0, 0, 0); mModeOfOpsView.setText(getString(R.string.status_connecting_via_mode, mModes[MODE_NAMES.indexOf(mCurrentMode)])); } else { int uid = Users.getSelfOrRemoteUid(); boolean goodMode = !badInferredMode(mCurrentMode, uid); mInferredModeView.setText(Ops.getInferredMode(requireContext())); if (goodMode) { mInferredModeView.setTextColor(mColorActive); TextViewCompat.setCompoundDrawableTintList(mModeOfOpsView, mColorActive); mModeOfOpsView.setTextColor(mColorActive); mModeOfOpsView.setCompoundDrawablesRelativeWithIntrinsicBounds(mIconActive, 0, 0, 0); CharSequence mode; if (serverActive && uid != Process.myUid()) { mode = "remote service"; } else mode = mModes[MODE_NAMES.indexOf(mCurrentMode)]; mModeOfOpsView.setText(getString(R.string.status_connected_via_mode, mode)); } else { mInferredModeView.setTextColor(mColorError); TextViewCompat.setCompoundDrawableTintList(mModeOfOpsView, mColorError); mModeOfOpsView.setTextColor(mColorError); mModeOfOpsView.setCompoundDrawablesRelativeWithIntrinsicBounds(mIconInactive, 0, 0, 0); mModeOfOpsView.setText(getString(R.string.status_not_connected_via_mode, mModes[MODE_NAMES.indexOf(mCurrentMode)])); } } // Server if (serverRequired) { mRemoteServerStatusView.setTextColor(serverActive ? mColorActive : mColorError); TextViewCompat.setCompoundDrawableTintList(mRemoteServerStatusView, serverActive ? mColorActive : mColorError); } else { mRemoteServerStatusView.setTextColor(mColorInactive); TextViewCompat.setCompoundDrawableTintList(mRemoteServerStatusView, mColorInactive); } mRemoteServerStatusView.setCompoundDrawablesRelativeWithIntrinsicBounds(serverActive ? mIconActive : mIconInactive, 0, 0, 0); mRemoteServerStatusView.setText(serverActive ? R.string.status_remote_server_active : R.string.status_remote_server_inactive); // Services if (servicesRequired) { mRemoteServicesStatusView.setTextColor(servicesActive ? mColorActive : mColorError); TextViewCompat.setCompoundDrawableTintList(mRemoteServicesStatusView, servicesActive ? mColorActive : mColorError); } else { mRemoteServicesStatusView.setTextColor(mColorInactive); TextViewCompat.setCompoundDrawableTintList(mRemoteServicesStatusView, mColorInactive); } mRemoteServicesStatusView.setCompoundDrawablesRelativeWithIntrinsicBounds(servicesActive ? mIconActive : mIconInactive, 0, 0, 0); mRemoteServicesStatusView.setText(servicesActive ? R.string.status_remote_services_active : R.string.status_remote_services_inactive); } private static boolean requireRemoteServer(@NonNull String mode) { return Ops.MODE_ADB_OVER_TCP.equals(mode) || Ops.MODE_ADB_WIFI.equals(mode); } private static boolean requireRemoteServices(@NonNull String mode) { return !Ops.MODE_AUTO.equals(mode) && !Ops.MODE_NO_ROOT.equals(mode); } private static boolean badInferredMode(@NonNull String mode, int uid) { switch (mode) { case Ops.MODE_ROOT: return uid != Ops.ROOT_UID; case Ops.MODE_ADB_OVER_TCP: case Ops.MODE_ADB_WIFI: return uid > Ops.SHELL_UID; default: return false; } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/settings/Ops.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.settings; import android.Manifest; import android.content.Context; import android.content.Intent; import android.os.Build; import android.os.Process; import android.os.RemoteException; import android.provider.Settings; import android.text.TextUtils; import androidx.annotation.AnyThread; import androidx.annotation.GuardedBy; import androidx.annotation.IntDef; import androidx.annotation.MainThread; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.annotation.StringDef; import androidx.annotation.UiThread; import androidx.annotation.WorkerThread; import androidx.core.content.ContextCompat; import androidx.fragment.app.FragmentActivity; import androidx.lifecycle.Observer; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Locale; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.adb.AdbConnectionManager; import io.github.muntashirakon.AppManager.adb.AdbPairingService; import io.github.muntashirakon.AppManager.adb.AdbUtils; import io.github.muntashirakon.AppManager.compat.ManifestCompat; import io.github.muntashirakon.AppManager.ipc.LocalServices; import io.github.muntashirakon.AppManager.logcat.helper.ServiceHelper; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.misc.NoOps; import io.github.muntashirakon.AppManager.runner.RunnerUtils; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.servermanager.LocalServer; import io.github.muntashirakon.AppManager.servermanager.ServerConfig; import io.github.muntashirakon.AppManager.session.SessionMonitoringService; import io.github.muntashirakon.AppManager.users.Owners; import io.github.muntashirakon.AppManager.users.Users; import io.github.muntashirakon.AppManager.utils.AppPref; import io.github.muntashirakon.AppManager.utils.ExUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.AppManager.utils.Utils; import io.github.muntashirakon.adb.AdbPairingRequiredException; import io.github.muntashirakon.dialog.DialogTitleBuilder; import io.github.muntashirakon.dialog.ScrollableDialogBuilder; import io.github.muntashirakon.dialog.TextInputDialogBuilder; /** * Controls mode of operation and other related functions */ public class Ops { public static final String TAG = Ops.class.getSimpleName(); @StringDef({MODE_AUTO, MODE_ROOT, MODE_ADB_OVER_TCP, MODE_ADB_WIFI, MODE_NO_ROOT}) @Retention(RetentionPolicy.SOURCE) public @interface Mode { } public static final String MODE_AUTO = "auto"; public static final String MODE_ROOT = "root"; public static final String MODE_ADB_OVER_TCP = "adb_tcp"; public static final String MODE_ADB_WIFI = "adb_wifi"; public static final String MODE_NO_ROOT = "no-root"; @IntDef({ STATUS_SUCCESS, STATUS_FAILURE, STATUS_AUTO_CONNECT_WIRELESS_DEBUGGING, STATUS_WIRELESS_DEBUGGING_CHOOSER_REQUIRED, STATUS_ADB_PAIRING_REQUIRED, STATUS_ADB_CONNECT_REQUIRED, STATUS_FAILURE_ADB_NEED_MORE_PERMS, }) @Retention(RetentionPolicy.SOURCE) public @interface Status { } public static final int STATUS_SUCCESS = 0; public static final int STATUS_FAILURE = 1; public static final int STATUS_AUTO_CONNECT_WIRELESS_DEBUGGING = 2; public static final int STATUS_WIRELESS_DEBUGGING_CHOOSER_REQUIRED = 3; public static final int STATUS_ADB_PAIRING_REQUIRED = 4; public static final int STATUS_ADB_CONNECT_REQUIRED = 5; public static final int STATUS_FAILURE_ADB_NEED_MORE_PERMS = 6; public static int ROOT_UID = 0; public static int SHELL_UID = 2000; public static int PHONE_UID = Process.PHONE_UID; public static int SYSTEM_UID = Process.SYSTEM_UID; private static volatile int sWorkingUid = Process.myUid(); private static volatile boolean sDirectRoot = false; // AM has root AND that root is being used private static boolean sIsAdb = false; // UID = 2000 private static boolean sIsSystem = false; // UID = 1000 private static boolean sIsRoot = false; // UID = 0 // Security private static final Object sSecurityLock = new Object(); @GuardedBy("sSecurityLock") private static boolean sIsAuthenticated = false; private Ops() { } @AnyThread public static int getWorkingUid() { return sWorkingUid; } @AnyThread public static void setWorkingUid(int newUid) { sWorkingUid = newUid; } @AnyThread public static int getWorkingUidOrRoot() { int uid = getWorkingUid(); if (uid != ROOT_UID && sDirectRoot) { return ROOT_UID; } return uid; } @AnyThread public static boolean isWorkingUidRoot() { return getWorkingUid() == ROOT_UID; } /** * Whether App Manager is currently using direct root (e.g. root granted to the app) to perform operations. The * result returned by this method may not reflect the actual state due to other factors. */ @AnyThread public static boolean isDirectRoot() { return sDirectRoot; } /** * Whether App Manager is running in system mode */ @AnyThread public static boolean isSystem() { return sIsSystem; } /** * Whether App Manager is running in ADB mode */ @AnyThread public static boolean isAdb() { return sIsAdb; } /** * Whether the current App Manager session is authenticated by the user. It does two things: *

    *
  1. If security is enabled, it marks that the user has got passed the security challenge. *
  2. It checks if a mode of operation is set before proceeding further. *
*/ @GuardedBy("sSecurityLock") @AnyThread public static boolean isAuthenticated() { synchronized (sSecurityLock) { return sIsAuthenticated; } } @GuardedBy("sSecurityLock") @MainThread public static void setAuthenticated(@NonNull Context context, boolean authenticated) { synchronized (sSecurityLock) { sIsAuthenticated = authenticated; if (Prefs.Privacy.isPersistentSessionAllowed()) { Intent service = new Intent(context, SessionMonitoringService.class); if (authenticated) { ContextCompat.startForegroundService(context, service); } else { context.stopService(service); } } } } @NonNull public static CharSequence getInferredMode(@NonNull Context context) { int uid = Users.getSelfOrRemoteUid(); if (uid == ROOT_UID) { return context.getString(R.string.root); } if (uid == SHELL_UID) { return "ADB"; } if (uid != Process.myUid()) { String uidStr = Owners.getUidOwnerMap(false).get(uid); if (!TextUtils.isEmpty(uidStr)) { return uidStr.substring(0, 1).toUpperCase(Locale.ROOT) + (uidStr.length() > 1 ? uidStr.substring(1) : ""); } } return context.getString(R.string.no_root); } @NoOps public static String getMode() { String mode = AppPref.getString(AppPref.PrefKey.PREF_MODE_OF_OPS_STR); // Backward compatibility for v2.6.0 if (mode.equals("adb")) { mode = MODE_ADB_OVER_TCP; } if ((MODE_ADB_OVER_TCP.equals(mode) || MODE_ADB_WIFI.equals(mode)) && !SelfPermissions.checkSelfPermission(Manifest.permission.INTERNET)) { // ADB enabled but the INTERNET permission is not granted, replace current with auto. return MODE_AUTO; } return mode; } @NoOps public static void setMode(@NonNull String newMode) { AppPref.set(AppPref.PrefKey.PREF_MODE_OF_OPS_STR, newMode); } @WorkerThread @NoOps // Although we've used Ops checks, its overall usage does not affect anything @Status public static int init(@NonNull Context context, boolean force) { String mode = getMode(); sDirectRoot = hasRoot(); if (MODE_AUTO.equals(mode)) { autoDetectRootSystemOrAdbAndPersist(context); return sIsAdb ? STATUS_SUCCESS : initPermissionsWithSuccess(); } if (MODE_NO_ROOT.equals(mode)) { sDirectRoot = false; sIsAdb = sIsSystem = sIsRoot = false; // Also, stop existing services if any if (LocalServices.alive()) { LocalServices.stopServices(); } if (LocalServer.alive(context)) { // We don't care about its results ThreadUtils.postOnBackgroundThread(() -> ExUtils.exceptionAsIgnored(() -> LocalServer.getInstance().closeBgServer())); } return STATUS_SUCCESS; } if (!force && isAMServiceUpAndRunning(context, mode)) { // An instance of AMService is already running return sIsAdb ? STATUS_SUCCESS : initPermissionsWithSuccess(); } try { switch (mode) { case MODE_ROOT: if (!sDirectRoot) { throw new Exception("Root is unavailable."); } // Disable server first ExUtils.exceptionAsIgnored(() -> { if (LocalServer.alive(context)) { LocalServer.getInstance().closeBgServer(); } }); sIsSystem = sIsAdb = false; sIsRoot = true; LocalServices.bindServicesIfNotAlready(); return initPermissionsWithSuccess(); case MODE_ADB_WIFI: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (!Utils.isWifiActive(context.getApplicationContext())) { throw new Exception("Wifi not enabled."); } if (AdbUtils.enableWirelessDebugging(context)) { // Wireless debugging enabled, try auto-connect return STATUS_AUTO_CONNECT_WIRELESS_DEBUGGING; } else { // Wireless debugging is turned off or there's no permission return STATUS_WIRELESS_DEBUGGING_CHOOSER_REQUIRED; } } // else fallback to ADB over TCP case MODE_ADB_OVER_TCP: sIsRoot = sIsSystem = false; sIsAdb = true; ServerConfig.setAdbPort(findAdbPort(context, 10, AdbUtils.getAdbPortOrDefault())); LocalServer.restart(); LocalServices.bindServicesIfNotAlready(); return checkRootOrIncompleteUsbDebuggingInAdb(); } } catch (Throwable e) { Log.e(TAG, e); // Fallback to no-root mode for this session, this does not modify the user preference sIsAdb = sIsSystem = sIsRoot = false; ThreadUtils.postOnMainThread(() -> UIUtils.displayLongToast(R.string.failed_to_use_the_current_mode_of_operation)); } return STATUS_FAILURE; } /** * Whether App Manager has been granted root permission. * * @return {@code true} iff root is granted. */ @AnyThread @NoOps public static boolean hasRoot() { return RunnerUtils.isRootGiven(); } @WorkerThread @NoOps // Although we've used Ops checks, its overall usage does not affect anything private static void autoDetectRootSystemOrAdbAndPersist(@NonNull Context context) { sIsRoot = sDirectRoot; if (sDirectRoot) { // Root permission was granted setMode(MODE_ROOT); // Disable remote server ExUtils.exceptionAsIgnored(() -> { if (LocalServer.alive(context)) { LocalServer.getInstance().closeBgServer(); } }); // Disable ADB and force root sIsSystem = sIsAdb = false; if (LocalServices.alive()) { if (Users.getSelfOrRemoteUid() == ROOT_UID) { // Service is already running in root mode return; } // Service is running in ADB/other mode, but we need root LocalServices.stopServices(); } try { // Service is confirmed dead LocalServices.bindServices(); if (LocalServices.alive() && Users.getSelfOrRemoteUid() == ROOT_UID) { // Service is running in root return; } } catch (RemoteException e) { Log.e(TAG, e); } // Root is granted but Binder communication cannot be initiated Log.e(TAG, "Root granted but could not use root to initiate a connection. Trying ADB..."); if (AdbUtils.startAdb(AdbUtils.getAdbPortOrDefault())) { Log.i(TAG, "Started ADB over TCP via root."); } else { Log.w(TAG, "Could not start ADB over TCP via root."); } sIsRoot = false; // Fall-through, in case we can use other options } // Root was not working/granted, but check for AM service just in case if (LocalServices.alive()) { setMode(MODE_ADB_OVER_TCP); int uid = Users.getSelfOrRemoteUid(); if (uid == ROOT_UID) { sIsSystem = sIsAdb = false; sIsRoot = true; return; } if (uid == SYSTEM_UID) { sIsRoot = sIsAdb = false; sIsSystem = true; return; } if (uid == SHELL_UID) { sIsRoot = sIsSystem = false; sIsAdb = true; ThreadUtils.postOnMainThread(() -> UIUtils.displayShortToast(R.string.working_on_adb_mode)); return; } } // Root not granted if (!SelfPermissions.checkSelfPermission(Manifest.permission.INTERNET)) { // INTERNET permission is not granted setMode(MODE_NO_ROOT); // Skip checking for ADB sIsAdb = false; return; } // Check for ADB if (!AdbUtils.isAdbdRunning()) { // ADB not running. In auto mode, we do not attempt to enable it either setMode(MODE_NO_ROOT); sIsAdb = sIsSystem = sIsRoot = false; return; } sIsAdb = true; // First enable ADB if not already try { ServerConfig.setAdbPort(findAdbPort(context, 7, ServerConfig.getAdbPort())); LocalServer.restart(); LocalServices.bindServicesIfNotAlready(); } catch (Throwable e) { Log.e(TAG, e); } sIsAdb = LocalServices.alive(); if (sIsAdb) { // No need to return anything here because we're in auto-mode. // Any message produced by the method below is just a helpful message. checkRootOrIncompleteUsbDebuggingInAdb(); } setMode(getWorkingUid() != Process.myUid() ? MODE_ADB_OVER_TCP : MODE_NO_ROOT); } @UiThread @RequiresApi(Build.VERSION_CODES.R) @NoOps // Although we've used Ops checks, its overall usage does not affect anything public static void connectWirelessDebugging(@NonNull FragmentActivity activity, @NonNull AdbConnectionInterface callback) { DialogTitleBuilder builder = new DialogTitleBuilder(activity) .setTitle(R.string.wireless_debugging) .setEndIcon(R.drawable.ic_open_in_new, v -> { Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); activity.startActivity(intent); }) .setEndIconContentDescription(R.string.open_developer_options_page); new MaterialAlertDialogBuilder(activity) .setCustomTitle(builder.build()) .setMessage(R.string.choose_what_to_do) .setCancelable(false) .setPositiveButton(R.string.adb_connect, (dialog1, which1) -> callback.onStatusReceived(STATUS_ADB_CONNECT_REQUIRED)) .setNeutralButton(R.string.adb_pair, (dialog1, which1) -> callback.onStatusReceived(STATUS_ADB_PAIRING_REQUIRED)) .setNegativeButton(R.string.cancel, (dialog, which) -> callback.connectAdb(-1)) .show(); } @WorkerThread @NoOps // Although we've used Ops checks, its overall usage does not affect anything @Status public static int autoConnectWirelessDebugging(@NonNull Context context) { boolean lastAdb = sIsAdb; boolean lastSystem = sIsSystem; boolean lastRoot = sIsRoot; sIsAdb = true; sIsSystem = sIsRoot = false; try { ServerConfig.setAdbPort(findAdbPort(context, 5, ServerConfig.getAdbPort())); LocalServer.restart(); LocalServices.bindServicesIfNotAlready(); return checkRootOrIncompleteUsbDebuggingInAdb(); } catch (RemoteException | IOException | AdbPairingRequiredException e) { Log.e(TAG, "Could not auto-connect to adbd", e); // Go back to the last mode sIsAdb = lastAdb; sIsSystem = lastSystem; sIsRoot = lastRoot; if (e instanceof AdbPairingRequiredException) { // Only pairing is required return STATUS_ADB_PAIRING_REQUIRED; } else return STATUS_WIRELESS_DEBUGGING_CHOOSER_REQUIRED; } } @WorkerThread @NoOps // Although we've used Ops checks, its overall usage does not affect anything @Status public static int connectAdb(@NonNull Context context, int port, @Status int returnCodeOnFailure) { if (port < 0) return returnCodeOnFailure; boolean lastAdb = sIsAdb; boolean lastSystem = sIsSystem; boolean lastRoot = sIsRoot; sIsAdb = true; sIsSystem = sIsRoot = false; try { ServerConfig.setAdbPort(port); LocalServer.restart(); LocalServices.bindServicesIfNotAlready(); return checkRootOrIncompleteUsbDebuggingInAdb(); } catch (RemoteException | IOException | AdbPairingRequiredException e) { Log.e(TAG, "Could not connect to adbd using port " + port, e); // Go back to the last mode sIsAdb = lastAdb; sIsSystem = lastSystem; sIsRoot = lastRoot; return returnCodeOnFailure; } } @UiThread @NoOps public static void connectAdbInput(@NonNull FragmentActivity activity, @NonNull AdbConnectionInterface callback) { new TextInputDialogBuilder(activity, R.string.port_number) .setTitle(R.string.wireless_debugging) .setInputText(String.valueOf(ServerConfig.getAdbPort())) .setHelperText(R.string.adb_connect_port_number_description) .setPositiveButton(R.string.ok, (dialog2, which2, inputText, isChecked) -> { if (TextUtils.isEmpty(inputText)) { UIUtils.displayShortToast(R.string.port_number_empty); callback.connectAdb(-1); return; } try { callback.connectAdb(Integer.decode(inputText.toString().trim())); } catch (NumberFormatException e) { UIUtils.displayShortToast(R.string.port_number_invalid); callback.connectAdb(-1); } }) .setNegativeButton(R.string.cancel, (dialog, which, inputText, isChecked) -> callback.connectAdb(-1)) .setCancelable(false) .show(); } @RequiresApi(Build.VERSION_CODES.R) @UiThread @NoOps public static void pairAdbInput(@NonNull FragmentActivity activity, @NonNull AdbConnectionInterface callback) { new MaterialAlertDialogBuilder(activity) .setTitle(R.string.wireless_debugging) .setMessage(R.string.adb_pairing_instruction) .setCancelable(false) .setNeutralButton(R.string.action_manual, (dialog, which) -> { Intent adbPairingServiceIntent = new Intent(activity, AdbPairingService.class) .setAction(AdbPairingService.ACTION_START_PAIRING); ContextCompat.startForegroundService(activity, adbPairingServiceIntent); callback.pairAdb(); }) .setNegativeButton(R.string.cancel, (dialog, which) -> callback.onStatusReceived(STATUS_FAILURE)) .setPositiveButton(R.string.go, (dialog, which) -> { Intent developerOptionsIntent = new Intent(Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); Intent adbPairingServiceIntent = new Intent(activity, AdbPairingService.class) .setAction(AdbPairingService.ACTION_START_PAIRING); activity.startActivity(developerOptionsIntent); ContextCompat.startForegroundService(activity, adbPairingServiceIntent); callback.pairAdb(); }) .show(); } @WorkerThread @NoOps @RequiresApi(Build.VERSION_CODES.R) @Status public static int pairAdb(@NonNull Context context) { try { AdbConnectionManager conn = AdbConnectionManager.getInstance(); int status = pairAdbInternal(context, conn); if (status == STATUS_ADB_CONNECT_REQUIRED) { return connectAdb(context, findAdbPort(context, 7, ServerConfig.getAdbPort()), STATUS_ADB_CONNECT_REQUIRED); } } catch (Exception e) { ThreadUtils.postOnMainThread(() -> UIUtils.displayShortToast(R.string.failed)); Log.e(TAG, e); // Failed, fall-through } return STATUS_FAILURE; } @WorkerThread @NoOps @RequiresApi(Build.VERSION_CODES.R) @Status private static int pairAdbInternal(@NonNull Context context, @NonNull AdbConnectionManager conn) { AtomicReference observerObserver = new AtomicReference<>(new CountDownLatch(1)); AtomicReference pairingError = new AtomicReference<>(); Observer observer = e -> { pairingError.set(e); observerObserver.get().countDown(); }; ThreadUtils.postOnMainThread(() -> conn.getPairingObserver().observeForever(observer)); while (true) { boolean success; try { success = observerObserver.get().await(1, TimeUnit.HOURS); } catch (InterruptedException ignore) { success = false; } if (success) { if (pairingError.get() != null) { if (ServiceHelper.checkIfServiceIsRunning(context, AdbPairingService.class)) { observerObserver.set(new CountDownLatch(1)); continue; } success = false; } } ThreadUtils.postOnMainThread(() -> conn.getPairingObserver().removeObserver(observer)); if (success) { return STATUS_ADB_CONNECT_REQUIRED; } else { context.stopService(new Intent(context, AdbPairingService.class)); return STATUS_FAILURE; } } } @UiThread public static void displayIncompleteUsbDebuggingMessage(@NonNull FragmentActivity activity) { new ScrollableDialogBuilder(activity) .setTitle(R.string.adb_incomplete_usb_debugging_title) .setMessage(R.string.adb_incomplete_usb_debugging_message) .enableAnchors() .setNegativeButton(R.string.close, null) .setPositiveButton(R.string.open, (dialog, which, isChecked) -> { Intent intent = new Intent(Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); try { activity.startActivity(intent); } catch (Throwable ignore) { } }) .show(); } private static int initPermissionsWithSuccess() { SelfPermissions.init(); return STATUS_SUCCESS; } /** * @return {@code true} iff AMService is up and running */ @WorkerThread @NoOps // Although we've used Ops checks, its overall usage does not affect anything private static boolean isAMServiceUpAndRunning(@NonNull Context context, @Mode @NonNull String mode) { boolean lastAdb = sIsAdb; boolean lastSystem = sIsSystem; boolean lastRoot = sIsRoot; // At this point, we have already checked MODE_AUTO, and MODE_NO_ROOT has lower priority. sIsRoot = MODE_ROOT.equals(mode); sIsAdb = !sIsRoot; // Because the rests are ADB sIsSystem = false; if (LocalServer.alive(context)) { // Remote server is running, but local server may not be running try { LocalServer.getInstance(); LocalServices.bindServicesIfNotAlready(); } catch (RemoteException | IOException | AdbPairingRequiredException e) { Log.e(TAG, e); // fall-through, because the remote service may still be alive } } if (LocalServices.alive()) { // AM service is running int uid = Users.getSelfOrRemoteUid(); if (sIsRoot && uid == ROOT_UID) { // AM service is running as root return true; } if (uid == SYSTEM_UID) { // AM service is running as system sIsSystem = true; sIsRoot = sIsAdb = false; return true; } if (sIsAdb) { // AM service is running as ADB return checkRootOrIncompleteUsbDebuggingInAdb() == STATUS_SUCCESS; } // All checks are failed, stop services LocalServices.stopServices(); } // Checks are failed, revert everything sIsAdb = lastAdb; sIsSystem = lastSystem; sIsRoot = lastRoot; return false; } @NoOps // Although we've used Ops checks, its overall usage does not affect anything private static int checkRootOrIncompleteUsbDebuggingInAdb() { // ADB already granted and AM service is running int uid = Users.getSelfOrRemoteUid(); if (uid == ROOT_UID) { // AM service is being run as root sIsRoot = true; sIsSystem = sIsAdb = false; ThreadUtils.postOnMainThread(() -> UIUtils.displayLongToast(R.string.warning_working_on_root_mode)); } else if (uid == SYSTEM_UID) { // AM service is being run as system sIsSystem = true; sIsRoot = sIsAdb = false; ThreadUtils.postOnMainThread(() -> UIUtils.displayLongToast(R.string.warning_working_on_system_mode)); } else if (uid == SHELL_UID) { // ADB mode if (!SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.GRANT_RUNTIME_PERMISSIONS)) { // USB debugging is incomplete, revert back to no-root sIsAdb = sIsSystem = sIsRoot = false; return STATUS_FAILURE_ADB_NEED_MORE_PERMS; } ThreadUtils.postOnMainThread(() -> UIUtils.displayShortToast(R.string.working_on_adb_mode)); } else { // No-root mode sIsAdb = sIsSystem = sIsRoot = false; return STATUS_FAILURE; } return initPermissionsWithSuccess(); } @WorkerThread @RequiresApi(Build.VERSION_CODES.R) @NoOps private static int findAdbPort(@NonNull Context context, long timeoutInSeconds) throws IOException, InterruptedException { return AdbUtils.getLatestAdbDaemon(context, timeoutInSeconds, TimeUnit.SECONDS).second; } @WorkerThread @NoOps private static int findAdbPort(@NonNull Context context, long timeoutInSeconds, int defaultPort) throws IOException { if (!AdbUtils.isAdbdRunning()) { throw new IOException("ADB daemon not running."); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { // Find ADB port only in Android 11 (R) or later try { return findAdbPort(context, timeoutInSeconds); } catch (IOException | InterruptedException e) { Log.w(TAG, "Could not find ADB port", e); } } return defaultPort; } @AnyThread public interface AdbConnectionInterface { // TODO: 8/4/24 Remove the first two methods since the third method can be used instead of them void connectAdb(int port); @RequiresApi(Build.VERSION_CODES.R) void pairAdb(); void onStatusReceived(@Status int status); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/settings/PreferenceFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.settings; import android.annotation.SuppressLint; import android.os.Bundle; import android.view.View; import androidx.annotation.CallSuper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; import androidx.recyclerview.widget.RecyclerView; import java.util.Objects; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.util.UiUtils; public abstract class PreferenceFragment extends PreferenceFragmentCompat { public static final String PREF_KEY = "key"; public static final String PREF_SECONDARY = "secondary"; @Nullable private String mPrefKey; @CallSuper @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); boolean secondary = false; if (getArguments() != null) { mPrefKey = requireArguments().getString(PREF_KEY); secondary = requireArguments().getBoolean(PREF_SECONDARY); requireArguments().remove(PREF_KEY); requireArguments().remove(PREF_SECONDARY); } // https://github.com/androidx/androidx/blob/androidx-main/preference/preference/res/layout/preference_recyclerview.xml RecyclerView recyclerView = view.findViewById(R.id.recycler_view); recyclerView.setFitsSystemWindows(true); recyclerView.setClipToPadding(false); if (secondary) { if (this instanceof MainPreferences) { UiUtils.applyWindowInsetsAsPadding(recyclerView, false, true, true, false); } else { UiUtils.applyWindowInsetsAsPadding(recyclerView, false, true, false, true); } } else UiUtils.applyWindowInsetsAsPaddingNoTop(recyclerView); } @CallSuper @Override public void onStart() { requireActivity().setTitle(getTitle()); super.onStart(); updateUi(); } @StringRes public abstract int getTitle(); public void setPrefKey(@Nullable String prefKey) { mPrefKey = prefKey; updateUi(); } public T requirePreference(CharSequence key) { return Objects.requireNonNull(findPreference(key)); } protected void enablePrefs(boolean enable, Preference ...prefs) { if (prefs == null) { return; } for (Preference pref : prefs) { pref.setEnabled(enable); } } @SuppressLint("RestrictedApi") private void updateUi() { if (mPrefKey != null) { Preference prefToNavigate = findPreference(mPrefKey); if (prefToNavigate != null) { scrollToPreference(prefToNavigate); if (prefToNavigate.getFragment() != null) { prefToNavigate.performClick(); } } } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/settings/Prefs.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.settings; import static io.github.muntashirakon.AppManager.backup.BackupUtils.TAR_TYPES; import android.Manifest; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StyleRes; import androidx.core.util.Pair; import org.json.JSONException; import org.json.JSONObject; import java.io.File; import java.io.FileNotFoundException; import java.util.Objects; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.apk.signing.SigSchemes; import io.github.muntashirakon.AppManager.apk.signing.Signer; import io.github.muntashirakon.AppManager.backup.BackupFlags; import io.github.muntashirakon.AppManager.backup.CryptoUtils; import io.github.muntashirakon.AppManager.compat.ManifestCompat; import io.github.muntashirakon.AppManager.details.AppDetailsFragment; import io.github.muntashirakon.AppManager.fm.FmActivity; import io.github.muntashirakon.AppManager.fm.FmListOptions; import io.github.muntashirakon.AppManager.logcat.helper.LogcatHelper; import io.github.muntashirakon.AppManager.main.MainListOptions; import io.github.muntashirakon.AppManager.rules.struct.ComponentRule; import io.github.muntashirakon.AppManager.runningapps.RunningAppsActivity; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.utils.AppPref; import io.github.muntashirakon.AppManager.utils.ArrayUtils; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.FileUtils; import io.github.muntashirakon.AppManager.utils.FreezeUtils; import io.github.muntashirakon.AppManager.utils.TarUtils; import io.github.muntashirakon.io.Path; import io.github.muntashirakon.io.Paths; // Why this class? // // This class is just an abstract over the AppPref to make life a bit easier. In the future, however, it might be // possible to deliver the changes to the settings using lifecycle where required. For example, in the log viewer page, // changes to the settings are not immediately reflected unless the settings page is opened from the page itself. public final class Prefs { public static final class AppDetailsPage { public static boolean displayDefaultAppOps() { return AppPref.getBoolean(AppPref.PrefKey.PREF_APP_OP_SHOW_DEFAULT_BOOL); } public static void setDisplayDefaultAppOps(boolean display) { AppPref.set(AppPref.PrefKey.PREF_APP_OP_SHOW_DEFAULT_BOOL, display); } @AppDetailsFragment.SortOrder public static int getAppOpsSortOrder() { return AppPref.getInt(AppPref.PrefKey.PREF_APP_OP_SORT_ORDER_INT); } public static void setAppOpsSortOrder(@AppDetailsFragment.SortOrder int sortOrder) { AppPref.set(AppPref.PrefKey.PREF_APP_OP_SORT_ORDER_INT, sortOrder); } @AppDetailsFragment.SortOrder public static int getComponentsSortOrder() { return AppPref.getInt(AppPref.PrefKey.PREF_COMPONENTS_SORT_ORDER_INT); } public static void setComponentsSortOrder(@AppDetailsFragment.SortOrder int sortOrder) { AppPref.set(AppPref.PrefKey.PREF_COMPONENTS_SORT_ORDER_INT, sortOrder); } @AppDetailsFragment.SortOrder public static int getPermissionsSortOrder() { return AppPref.getInt(AppPref.PrefKey.PREF_PERMISSIONS_SORT_ORDER_INT); } public static void setPermissionsSortOrder(@AppDetailsFragment.SortOrder int sortOrder) { AppPref.set(AppPref.PrefKey.PREF_PERMISSIONS_SORT_ORDER_INT, sortOrder); } @AppDetailsFragment.SortOrder public static int getOverlaysSortOrder() { return AppPref.getInt(AppPref.PrefKey.PREF_OVERLAYS_SORT_ORDER_INT); } public static void setOverlaysSortOrder(@AppDetailsFragment.SortOrder int sortOrder) { AppPref.set(AppPref.PrefKey.PREF_OVERLAYS_SORT_ORDER_INT, sortOrder); } } public static final class Appearance { @NonNull public static String getLanguage() { return AppPref.getString(AppPref.PrefKey.PREF_CUSTOM_LOCALE_STR); } @NonNull public static String getLanguage(@NonNull Context context) { // Required when application isn't initialised properly AppPref appPref = AppPref.getNewInstance(context); return (String) appPref.getValue(AppPref.PrefKey.PREF_CUSTOM_LOCALE_STR); } public static void setLanguage(@NonNull String language) { AppPref.set(AppPref.PrefKey.PREF_CUSTOM_LOCALE_STR, language); } public static int getLayoutDirection() { return AppPref.getInt(AppPref.PrefKey.PREF_LAYOUT_ORIENTATION_INT); } public static void setLayoutDirection(int layoutDirection) { AppPref.set(AppPref.PrefKey.PREF_LAYOUT_ORIENTATION_INT, layoutDirection); } @StyleRes public static int getAppTheme() { switch (AppPref.getInt(AppPref.PrefKey.PREF_APP_THEME_CUSTOM_INT)) { case 1: // Full black theme return io.github.muntashirakon.ui.R.style.AppTheme_Black; default: // Normal theme return io.github.muntashirakon.ui.R.style.AppTheme; } } @StyleRes public static int getTransparentAppTheme() { switch (AppPref.getInt(AppPref.PrefKey.PREF_APP_THEME_CUSTOM_INT)) { case 1: // Full black theme return io.github.muntashirakon.ui.R.style.AppTheme_TransparentBackground_Black; default: // Normal theme return io.github.muntashirakon.ui.R.style.AppTheme_TransparentBackground; } } public static boolean isPureBlackTheme() { return AppPref.getInt(AppPref.PrefKey.PREF_APP_THEME_CUSTOM_INT) == 1; } public static void setPureBlackTheme(boolean enabled) { AppPref.set(AppPref.PrefKey.PREF_APP_THEME_CUSTOM_INT, enabled ? 1 : 0); } public static int getNightMode() { return AppPref.getInt(AppPref.PrefKey.PREF_APP_THEME_INT); } public static void setNightMode(int nightMode) { AppPref.set(AppPref.PrefKey.PREF_APP_THEME_INT, nightMode); } public static boolean useSystemFont() { return AppPref.getBoolean(AppPref.PrefKey.PREF_USE_SYSTEM_FONT_BOOL); } } public static final class BackupRestore { public static boolean backupAppsWithKeyStore() { return AppPref.getBoolean(AppPref.PrefKey.PREF_BACKUP_ANDROID_KEYSTORE_BOOL); } @NonNull @TarUtils.TarType public static String getCompressionMethod() { String tarType = AppPref.getString(AppPref.PrefKey.PREF_BACKUP_COMPRESSION_METHOD_STR); // Verify tar type if (ArrayUtils.indexOf(TAR_TYPES, tarType) == -1) { // Unknown tar type, set default tarType = TarUtils.TAR_GZIP; } return tarType; } public static void setCompressionMethod(@NonNull @TarUtils.TarType String tarType) { AppPref.set(AppPref.PrefKey.PREF_BACKUP_COMPRESSION_METHOD_STR, tarType); } @BackupFlags.BackupFlag public static int getBackupFlags() { return AppPref.getInt(AppPref.PrefKey.PREF_BACKUP_FLAGS_INT); } public static void setBackupFlags(@BackupFlags.BackupFlag int flags) { AppPref.set(AppPref.PrefKey.PREF_BACKUP_FLAGS_INT, flags); } public static boolean backupDirectoryExists() { Uri uri = Storage.getVolumePath(); Path path; if (uri.getScheme().equals(ContentResolver.SCHEME_FILE)) { // Append AppManager only if storage permissions are granted String newPath = uri.getPath(); if (SelfPermissions.checkStoragePermission()) { newPath += File.separator + "AppManager"; } path = Paths.get(newPath); } else path = Paths.get(uri); return path.exists(); } } public static final class Blocking { public static boolean globalBlockingEnabled() { return AppPref.getBoolean(AppPref.PrefKey.PREF_GLOBAL_BLOCKING_ENABLED_BOOL); } @ComponentRule.ComponentStatus public static String getDefaultBlockingMethod() { String selectedStatus = AppPref.getString(AppPref.PrefKey.PREF_DEFAULT_BLOCKING_METHOD_STR); if (!SelfPermissions.canBlockByIFW()) { if (selectedStatus.equals(ComponentRule.COMPONENT_TO_BE_BLOCKED_IFW_DISABLE) || selectedStatus.equals(ComponentRule.COMPONENT_TO_BE_BLOCKED_IFW)) { // Lower the status return ComponentRule.COMPONENT_TO_BE_DISABLED; } } return selectedStatus; } public static void setDefaultBlockingMethod(@NonNull @ComponentRule.ComponentStatus String blockingMethod) { AppPref.set(AppPref.PrefKey.PREF_DEFAULT_BLOCKING_METHOD_STR, blockingMethod); } @FreezeUtils.FreezeMethod public static int getDefaultFreezingMethod() { int freezeType = AppPref.getInt(AppPref.PrefKey.PREF_FREEZE_TYPE_INT); if (freezeType == FreezeUtils.FREEZE_HIDE) { // Requires MANAGE_USERS permission if (!SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.MANAGE_USERS)) { return FreezeUtils.FREEZE_DISABLE; } } else if (freezeType == FreezeUtils.FREEZE_SUSPEND || freezeType == FreezeUtils.FREEZE_ADV_SUSPEND) { // 7+ only. Requires MANAGE_USERS permission until P. Requires SUSPEND_APPS permission after that. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && !SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.SUSPEND_APPS) || (Build.VERSION.SDK_INT < Build.VERSION_CODES.P && !SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.MANAGE_USERS))) { return FreezeUtils.FREEZE_DISABLE; } } return freezeType; } public static void setDefaultFreezingMethod(@FreezeUtils.FreezeMethod int freezeType) { AppPref.set(AppPref.PrefKey.PREF_FREEZE_TYPE_INT, freezeType); } } public static final class Encryption { @NonNull @CryptoUtils.Mode public static String getEncryptionMode() { return AppPref.getString(AppPref.PrefKey.PREF_ENCRYPTION_STR); } public static void setEncryptionMode(@NonNull @CryptoUtils.Mode String mode) { AppPref.set(AppPref.PrefKey.PREF_ENCRYPTION_STR, mode); } @NonNull public static String getOpenPgpProvider() { return AppPref.getString(AppPref.PrefKey.PREF_OPEN_PGP_PACKAGE_STR); } public static void setOpenPgpProvider(@NonNull String providerPackage) { AppPref.set(AppPref.PrefKey.PREF_OPEN_PGP_PACKAGE_STR, providerPackage); } @NonNull public static String getOpenPgpKeyIds() { return AppPref.getString(AppPref.PrefKey.PREF_OPEN_PGP_USER_ID_STR); } public static void setOpenPgpKeyIds(@NonNull String keyIds) { AppPref.set(AppPref.PrefKey.PREF_OPEN_PGP_USER_ID_STR, keyIds); } } public static final class FileManager { public static boolean displayInLauncher() { ComponentName componentName = new ComponentName(BuildConfig.APPLICATION_ID, FmActivity.LAUNCHER_ALIAS); int state = ContextUtils.getContext().getPackageManager().getComponentEnabledSetting(componentName); return state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED; } public static Uri getHome() { return Uri.parse(AppPref.getString(AppPref.PrefKey.PREF_FM_HOME_STR)); } public static void setHome(@NonNull Uri uri) { AppPref.set(AppPref.PrefKey.PREF_FM_HOME_STR, uri.toString()); } public static boolean isRememberLastOpenedPath() { return AppPref.getBoolean(AppPref.PrefKey.PREF_FM_REMEMBER_LAST_PATH_BOOL); } @Nullable public static Pair> getLastOpenedPath() { String jsonString = AppPref.getString(AppPref.PrefKey.PREF_FM_LAST_PATH_STR); try { JSONObject object = new JSONObject(jsonString); if (object.has("path") && object.has("pos")) { boolean vfs = object.has("vfs") && object.getBoolean("vfs"); FmActivity.Options options = new FmActivity.Options(Uri.parse(object.getString("path")), vfs, false, false); if (!Paths.getStrict(options.uri).exists()) { // Do not bother if path does not exist return null; } Uri initUri; if (vfs && object.has("init")) { initUri = Uri.parse(object.getString("init")); } else initUri = null; Pair uriPositionPair = new Pair<>(initUri, object.getInt("pos")); return new Pair<>(options, uriPositionPair); } } catch (JSONException | FileNotFoundException e) { e.printStackTrace(); } return null; } public static void setLastOpenedPath(@NonNull FmActivity.Options options, @NonNull Uri initUri, int position) { try { if (options.isVfs()) { // Ignore VFS for now return; } JSONObject object = new JSONObject(); object.put("pos", position); if (options.isVfs()) { object.put("vfs", true); object.put("path", options.uri.toString()); object.put("init", initUri.toString()); } else { object.put("path", initUri.toString()); } AppPref.set(AppPref.PrefKey.PREF_FM_LAST_PATH_STR, object.toString()); } catch (JSONException e) { e.printStackTrace(); } } @FmListOptions.Options public static int getOptions() { return AppPref.getInt(AppPref.PrefKey.PREF_FM_OPTIONS_INT); } public static void setOptions(@FmListOptions.Options int options) { AppPref.set(AppPref.PrefKey.PREF_FM_OPTIONS_INT, options); } @FmListOptions.SortOrder public static int getSortOrder() { return AppPref.getInt(AppPref.PrefKey.PREF_FM_SORT_ORDER_INT); } public static void setSortOrder(@FmListOptions.SortOrder int sortOrder) { AppPref.set(AppPref.PrefKey.PREF_FM_SORT_ORDER_INT, sortOrder); } public static boolean isReverseSort() { return AppPref.getBoolean(AppPref.PrefKey.PREF_FM_SORT_REVERSE_BOOL); } public static void setReverseSort(boolean reverseSort) { AppPref.set(AppPref.PrefKey.PREF_FM_SORT_REVERSE_BOOL, reverseSort); } } public static final class Installer { public static boolean installInBackground() { return AppPref.getBoolean(AppPref.PrefKey.PREF_INSTALLER_ALWAYS_ON_BACKGROUND_BOOL); } public static boolean displayChanges() { return AppPref.getBoolean(AppPref.PrefKey.PREF_INSTALLER_DISPLAY_CHANGES_BOOL); } public static boolean blockTrackers() { return AppPref.getBoolean(AppPref.PrefKey.PREF_INSTALLER_BLOCK_TRACKERS_BOOL); } public static boolean forceDexOpt() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && AppPref.getBoolean(AppPref.PrefKey.PREF_INSTALLER_FORCE_DEX_OPT_BOOL); } public static boolean canSignApk() { if (!AppPref.getBoolean(AppPref.PrefKey.PREF_INSTALLER_SIGN_APK_BOOL)) { // Signing not enabled return false; } return Signer.canSign(); } public static int getInstallLocation() { return AppPref.getInt(AppPref.PrefKey.PREF_INSTALLER_INSTALL_LOCATION_INT); } public static void setInstallLocation(int installLocation) { AppPref.set(AppPref.PrefKey.PREF_INSTALLER_INSTALL_LOCATION_INT, installLocation); } @NonNull public static String getInstallerPackageName() { if (!SelfPermissions.checkSelfOrRemotePermission(Manifest.permission.INSTALL_PACKAGES)) { return BuildConfig.APPLICATION_ID; } return AppPref.getString(AppPref.PrefKey.PREF_INSTALLER_INSTALLER_APP_STR); } public static void setInstallerPackageName(@NonNull String packageName) { AppPref.set(AppPref.PrefKey.PREF_INSTALLER_INSTALLER_APP_STR, packageName); } public static boolean isSetOriginatingPackage() { return AppPref.getBoolean(AppPref.PrefKey.PREF_INSTALLER_SET_ORIGIN_BOOL); } public static int getPackageSource() { return AppPref.getInt(AppPref.PrefKey.PREF_INSTALLER_DEFAULT_PKG_SOURCE_INT); } public static void setPackageSource(int source) { AppPref.set(AppPref.PrefKey.PREF_INSTALLER_DEFAULT_PKG_SOURCE_INT, source); } public static boolean requestUpdateOwnership() { // Shell default is false return AppPref.getBoolean(AppPref.PrefKey.PREF_INSTALLER_UPDATE_OWNERSHIP_BOOL); } public static boolean isDisableApkVerification() { return AppPref.getBoolean(AppPref.PrefKey.PREF_INSTALLER_DISABLE_VERIFICATION_BOOL); } } public static final class LogViewer { @LogcatHelper.LogBufferId public static int getBuffers() { return AppPref.getInt(AppPref.PrefKey.PREF_LOG_VIEWER_BUFFER_INT); } public static void setBuffers(@LogcatHelper.LogBufferId int buffers) { AppPref.set(AppPref.PrefKey.PREF_LOG_VIEWER_BUFFER_INT, buffers); } public static int getLogLevel() { return AppPref.getInt(AppPref.PrefKey.PREF_LOG_VIEWER_DEFAULT_LOG_LEVEL_INT); } public static void setLogLevel(int logLevel) { AppPref.set(AppPref.PrefKey.PREF_LOG_VIEWER_DEFAULT_LOG_LEVEL_INT, logLevel); } public static int getDisplayLimit() { return AppPref.getInt(AppPref.PrefKey.PREF_LOG_VIEWER_DISPLAY_LIMIT_INT); } public static void setDisplayLimit(int displayLimit) { AppPref.set(AppPref.PrefKey.PREF_LOG_VIEWER_DISPLAY_LIMIT_INT, displayLimit); } @NonNull public static String getFilterPattern() { return AppPref.getString(AppPref.PrefKey.PREF_LOG_VIEWER_FILTER_PATTERN_STR); } public static void setFilterPattern(@NonNull String filterPattern) { AppPref.set(AppPref.PrefKey.PREF_LOG_VIEWER_FILTER_PATTERN_STR, filterPattern); } public static int getLogWritingInterval() { return AppPref.getInt(AppPref.PrefKey.PREF_LOG_VIEWER_WRITE_PERIOD_INT); } public static void setLogWritingInterval(int logWritingInterval) { AppPref.set(AppPref.PrefKey.PREF_LOG_VIEWER_WRITE_PERIOD_INT, logWritingInterval); } public static boolean expandByDefault() { return AppPref.getBoolean(AppPref.PrefKey.PREF_LOG_VIEWER_EXPAND_BY_DEFAULT_BOOL); } public static boolean omitSensitiveInfo() { return AppPref.getBoolean(AppPref.PrefKey.PREF_LOG_VIEWER_OMIT_SENSITIVE_INFO_BOOL); } public static boolean showPidTidTimestamp() { return AppPref.getBoolean(AppPref.PrefKey.PREF_LOG_VIEWER_SHOW_PID_TID_TIMESTAMP_BOOL); } } public static final class MainPage { @MainListOptions.SortOrder public static int getSortOrder() { return AppPref.getInt(AppPref.PrefKey.PREF_MAIN_WINDOW_SORT_ORDER_INT); } public static void setSortOrder(@RunningAppsActivity.SortOrder int sortOrder) { AppPref.set(AppPref.PrefKey.PREF_MAIN_WINDOW_SORT_ORDER_INT, sortOrder); } public static boolean isReverseSort() { return AppPref.getBoolean(AppPref.PrefKey.PREF_MAIN_WINDOW_SORT_REVERSE_BOOL); } public static void setReverseSort(boolean reverseSort) { AppPref.set(AppPref.PrefKey.PREF_MAIN_WINDOW_SORT_REVERSE_BOOL, reverseSort); } @MainListOptions.Filter public static int getFilters() { return AppPref.getInt(AppPref.PrefKey.PREF_MAIN_WINDOW_FILTER_FLAGS_INT); } public static void setFilters(@MainListOptions.Filter int filters) { AppPref.set(AppPref.PrefKey.PREF_MAIN_WINDOW_FILTER_FLAGS_INT, filters); } @Nullable public static String getFilteredProfileName() { String profileName = AppPref.getString(AppPref.PrefKey.PREF_MAIN_WINDOW_FILTER_PROFILE_STR); if (TextUtils.isEmpty(profileName)) { return null; } return profileName; } public static void setFilteredProfileName(@Nullable String profileName) { AppPref.set(AppPref.PrefKey.PREF_MAIN_WINDOW_FILTER_PROFILE_STR, profileName == null ? "" : profileName); } } public static final class Misc { @Nullable public static int[] getSelectedUsers() { String usersStr = AppPref.getString(AppPref.PrefKey.PREF_SELECTED_USERS_STR); if (usersStr.isEmpty()) return null; String[] usersSplitStr = usersStr.split(","); int[] users = new int[usersSplitStr.length]; for (int i = 0; i < users.length; ++i) { users[i] = Integer.decode(usersSplitStr[i]); } return users; } public static void setSelectedUsers(@Nullable int[] users) { if (users == null) { AppPref.set(AppPref.PrefKey.PREF_SELECTED_USERS_STR, ""); return; } String[] userString = new String[users.length]; for (int i = 0; i < users.length; ++i) { userString[i] = String.valueOf(users[i]); } AppPref.set(AppPref.PrefKey.PREF_SELECTED_USERS_STR, TextUtils.join(",", userString)); } public static boolean sendNotificationsToConnectedDevices() { return AppPref.getBoolean(AppPref.PrefKey.PREF_SEND_NOTIFICATIONS_TO_CONNECTED_DEVICES_BOOL); } public static void setAdbLocalServerPort(int port) { AppPref.set(AppPref.PrefKey.PREF_ADB_LOCAL_SERVER_PORT_INT, port); } public static int getAdbLocalServerPort() { return AppPref.getInt(AppPref.PrefKey.PREF_ADB_LOCAL_SERVER_PORT_INT); } } public static final class RunningApps { @RunningAppsActivity.SortOrder public static int getSortOrder() { return AppPref.getInt(AppPref.PrefKey.PREF_RUNNING_APPS_SORT_ORDER_INT); } public static void setSortOrder(@RunningAppsActivity.SortOrder int sortOrder) { AppPref.set(AppPref.PrefKey.PREF_RUNNING_APPS_SORT_ORDER_INT, sortOrder); } @RunningAppsActivity.Filter public static int getFilters() { return AppPref.getInt(AppPref.PrefKey.PREF_RUNNING_APPS_FILTER_FLAGS_INT); } public static void setFilters(@RunningAppsActivity.Filter int filters) { AppPref.set(AppPref.PrefKey.PREF_RUNNING_APPS_FILTER_FLAGS_INT, filters); } public static boolean enableKillForSystemApps() { return AppPref.getBoolean(AppPref.PrefKey.PREF_ENABLE_KILL_FOR_SYSTEM_BOOL); } public static void setEnableKillForSystemApps(boolean enable) { AppPref.set(AppPref.PrefKey.PREF_ENABLE_KILL_FOR_SYSTEM_BOOL, enable); } } public static final class Privacy { public static boolean isScreenLockEnabled() { return AppPref.getBoolean(AppPref.PrefKey.PREF_ENABLE_SCREEN_LOCK_BOOL); } public static boolean isAutoLockEnabled() { return AppPref.getBoolean(AppPref.PrefKey.PREF_ENABLE_AUTO_LOCK_BOOL); } public static boolean isPersistentSessionAllowed() { return AppPref.getBoolean(AppPref.PrefKey.PREF_ENABLE_PERSISTENT_SESSION_BOOL); } } public static final class Signing { @NonNull public static SigSchemes getSigSchemes() { SigSchemes sigSchemes = new SigSchemes(AppPref.getInt(AppPref.PrefKey.PREF_SIGNATURE_SCHEMES_INT)); if (sigSchemes.isEmpty()) { // Use default if no flag is set return new SigSchemes(SigSchemes.DEFAULT_SCHEMES); } return sigSchemes; } public static void setSigSchemes(int flags) { AppPref.set(AppPref.PrefKey.PREF_SIGNATURE_SCHEMES_INT, flags); } public static boolean zipAlign() { return AppPref.getBoolean(AppPref.PrefKey.PREF_ZIP_ALIGN_BOOL); } } public static final class Storage { @NonNull public static Path getAppManagerDirectory() { Uri uri = getVolumePath(); Path path; if (Objects.equals(uri.getScheme(), ContentResolver.SCHEME_FILE)) { // Append AppManager String newPath = uri.getPath() + File.separator + "AppManager"; path = Paths.get(newPath); } else path = Paths.get(uri); if (!path.exists()) path.mkdirs(); return path; } public static Uri getVolumePath() { String uriOrBareFile = AppPref.getString(AppPref.PrefKey.PREF_BACKUP_VOLUME_STR); if (uriOrBareFile.startsWith("/")) { // A good URI starts with file:// or content://, if not, migrate Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_FILE).path(uriOrBareFile).build(); AppPref.set(AppPref.PrefKey.PREF_BACKUP_VOLUME_STR, uri.toString()); return uri; } return Uri.parse(uriOrBareFile); } public static void setVolumePath(@NonNull String path) { AppPref.set(AppPref.PrefKey.PREF_BACKUP_VOLUME_STR, path); } @NonNull public static Path getTempPath() { // This path is intended for storing temporary data for backup/restore and similar operations return Paths.get(FileUtils.getCachePath()); } } public static final class VirusTotal { @Nullable public static String getApiKey() { String apiKey = AppPref.getString(AppPref.PrefKey.PREF_VIRUS_TOTAL_API_KEY_STR); if (TextUtils.isEmpty(apiKey)) { return null; } return apiKey; } public static void setApiKey(@Nullable String apiKey) { AppPref.set(AppPref.PrefKey.PREF_VIRUS_TOTAL_API_KEY_STR, apiKey); } public static boolean promptBeforeUpload() { return AppPref.getBoolean(AppPref.PrefKey.PREF_VIRUS_TOTAL_PROMPT_BEFORE_UPLOADING_BOOL); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/settings/PrivacyPreferences.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.settings; import android.Manifest; import android.content.Intent; import android.os.Bundle; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.preference.SwitchPreferenceCompat; import com.google.android.material.transition.MaterialSharedAxis; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.crypto.auth.AuthManagerActivity; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.session.SessionMonitoringService; public class PrivacyPreferences extends PreferenceFragment { @Override public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { setPreferencesFromResource(R.xml.preferences_privacy, rootKey); getPreferenceManager().setPreferenceDataStore(new SettingsDataStore()); boolean isScreenLockEnabled = Prefs.Privacy.isScreenLockEnabled(); boolean isPersistentSessionEnabled = Prefs.Privacy.isPersistentSessionAllowed(); // Auto lock SwitchPreferenceCompat autoLock = requirePreference("enable_auto_lock"); autoLock.setVisible(isScreenLockEnabled && isPersistentSessionEnabled); autoLock.setChecked(Prefs.Privacy.isAutoLockEnabled()); autoLock.setOnPreferenceChangeListener((preference, newValue) -> { boolean enabled = (boolean) newValue; restartServiceIfNeeded(null, enabled, null); return true; }); // Screen lock SwitchPreferenceCompat screenLock = requirePreference("enable_screen_lock"); screenLock.setChecked(isScreenLockEnabled); screenLock.setOnPreferenceChangeListener((preference, newValue) -> { boolean enabled = (boolean) newValue; // Auto lock pref has to be updated depending on this if (enabled) { autoLock.setVisible(Prefs.Privacy.isPersistentSessionAllowed()); } else autoLock.setVisible(false); restartServiceIfNeeded(enabled, null, null); return true; }); // Persistent session SwitchPreferenceCompat persistentSession = requirePreference("enable_persistent_session"); persistentSession.setChecked(isPersistentSessionEnabled); persistentSession.setOnPreferenceChangeListener((preference, newValue) -> { boolean enabled = (boolean) newValue; // Auto lock pref has to be updated depending on this if (enabled) { autoLock.setVisible(Prefs.Privacy.isScreenLockEnabled()); } else autoLock.setVisible(false); restartServiceIfNeeded(null, null, enabled); return true; }); // Toggle Internet SwitchPreferenceCompat toggleInternet = requirePreference("toggle_internet"); toggleInternet.setEnabled(SelfPermissions.checkSelfPermission(Manifest.permission.INTERNET)); toggleInternet.setChecked(FeatureController.isInternetEnabled()); toggleInternet.setOnPreferenceChangeListener((preference, newValue) -> { boolean isEnabled = (boolean) newValue; FeatureController.getInstance().modifyState(FeatureController.FEAT_INTERNET, isEnabled); return true; }); // Authorization Management requirePreference("auth_manager").setOnPreferenceClickListener(preference -> { startActivity(new Intent(requireContext(), AuthManagerActivity.class)); return true; }); } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setEnterTransition(new MaterialSharedAxis(MaterialSharedAxis.Z, true)); setReturnTransition(new MaterialSharedAxis(MaterialSharedAxis.Z, false)); } @Override public int getTitle() { return R.string.pref_privacy; } public void restartServiceIfNeeded(@Nullable Boolean screenLockEnabled, @Nullable Boolean autoLockEnabled, @Nullable Boolean persistentSessionEnabled) { if (screenLockEnabled == null && autoLockEnabled == null && persistentSessionEnabled == null) { // Nothing is set return; } Intent service = new Intent(requireContext(), SessionMonitoringService.class); if (Boolean.FALSE.equals(persistentSessionEnabled)) { // Stop background session requireContext().stopService(service); return; } if (Boolean.TRUE.equals(persistentSessionEnabled)) { // Start background session ContextCompat.startForegroundService(requireContext(), service); return; } persistentSessionEnabled = Prefs.Privacy.isPersistentSessionAllowed(); if (!persistentSessionEnabled) { // Session not enabled and not running return; } // Session enabled if (autoLockEnabled != null || screenLockEnabled != null) { // Auto lock preference has changed, restart service requireContext().stopService(service); ContextCompat.startForegroundService(requireContext(), service); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/settings/RulesPreferences.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.settings; import android.os.Bundle; import android.text.SpannableStringBuilder; import androidx.annotation.Nullable; import androidx.lifecycle.ViewModelProvider; import androidx.preference.Preference; import androidx.preference.SwitchPreferenceCompat; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.transition.MaterialSharedAxis; import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.rules.struct.ComponentRule; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.utils.ArrayUtils; import io.github.muntashirakon.AppManager.utils.FreezeUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.dialog.DialogTitleBuilder; import io.github.muntashirakon.dialog.SearchableSingleChoiceDialogBuilder; public class RulesPreferences extends PreferenceFragment { private static final String[] BLOCKING_METHODS = new String[]{ ComponentRule.COMPONENT_TO_BE_BLOCKED_IFW_DISABLE, ComponentRule.COMPONENT_TO_BE_BLOCKED_IFW, ComponentRule.COMPONENT_TO_BE_DISABLED }; private static final Integer[] BLOCKING_METHOD_TITLES = new Integer[]{ R.string.intent_firewall_and_disable, R.string.intent_firewall, R.string.disable }; private static final Integer[] BLOCKING_METHOD_DESCRIPTIONS = new Integer[]{ R.string.pref_intent_firewall_and_disable_description, R.string.pref_intent_firewall_description, R.string.pref_disable_description }; private static final Integer[] FREEZING_METHODS = new Integer[]{ FreezeUtils.FREEZE_SUSPEND, FreezeUtils.FREEZE_ADV_SUSPEND, FreezeUtils.FREEZE_DISABLE, FreezeUtils.FREEZE_HIDE }; private static final Integer[] FREEZING_METHOD_TITLES = new Integer[]{ R.string.suspend_app, R.string.advanced_suspend_app, R.string.disable, R.string.hide_app }; private static final Integer[] FREEZING_METHOD_DESCRIPTIONS = new Integer[]{ R.string.suspend_app_description, R.string.advanced_suspend_app_description, R.string.disable_app_description, R.string.hide_app_description }; private SettingsActivity mActivity; @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { addPreferencesFromResource(R.xml.preferences_rules); getPreferenceManager().setPreferenceDataStore(new SettingsDataStore()); MainPreferencesViewModel model = new ViewModelProvider(requireActivity()).get(MainPreferencesViewModel.class); mActivity = (SettingsActivity) requireActivity(); // Default freezing method Preference defaultFreezingMethod = Objects.requireNonNull(findPreference("freeze_type")); AtomicInteger freezeTypeIdx = new AtomicInteger(ArrayUtils.indexOf(FREEZING_METHODS, Prefs.Blocking.getDefaultFreezingMethod())); if (freezeTypeIdx.get() != -1) { defaultFreezingMethod.setSummary(FREEZING_METHOD_TITLES[freezeTypeIdx.get()]); } defaultFreezingMethod.setEnabled(SelfPermissions.canFreezeUnfreezePackages()); defaultFreezingMethod.setOnPreferenceClickListener(preference -> { CharSequence[] itemDescription = new CharSequence[FREEZING_METHODS.length]; for (int i = 0; i < FREEZING_METHODS.length; ++i) { itemDescription[i] = new SpannableStringBuilder(getString(FREEZING_METHOD_TITLES[i])).append("\n") .append(UIUtils.getSmallerText(getString(FREEZING_METHOD_DESCRIPTIONS[i]))); } new SearchableSingleChoiceDialogBuilder<>(mActivity, FREEZING_METHODS, itemDescription) .setTitle(new DialogTitleBuilder(mActivity) .setTitle(R.string.pref_default_freezing_method) .setSubtitle(R.string.pref_default_freezing_method_description) .build()) .setSelection(Prefs.Blocking.getDefaultFreezingMethod()) .setOnSingleChoiceClickListener((dialog, which, selectedFreezingMethod, isChecked) -> { if (!isChecked) { return; } Prefs.Blocking.setDefaultFreezingMethod(selectedFreezingMethod); defaultFreezingMethod.setSummary(FREEZING_METHOD_TITLES[which]); freezeTypeIdx.set(which); dialog.dismiss(); }) .setNegativeButton(R.string.close, null) .show(); return true; }); // Default component blocking method Preference defaultBlockingMethod = Objects.requireNonNull(findPreference("default_blocking_method")); // Disable this option if IFW folder can't be accessed defaultBlockingMethod.setEnabled(SelfPermissions.canBlockByIFW()); int csIdx = ArrayUtils.indexOf(BLOCKING_METHODS, Prefs.Blocking.getDefaultBlockingMethod()); if (csIdx != -1) { defaultBlockingMethod.setSummary(BLOCKING_METHOD_TITLES[csIdx]); } defaultBlockingMethod.setOnPreferenceClickListener(preference -> { CharSequence[] itemDescription = new CharSequence[BLOCKING_METHODS.length]; for (int i = 0; i < BLOCKING_METHODS.length; ++i) { itemDescription[i] = new SpannableStringBuilder(getString(BLOCKING_METHOD_TITLES[i])).append("\n") .append(UIUtils.getSmallerText(getString(BLOCKING_METHOD_DESCRIPTIONS[i]))); } new SearchableSingleChoiceDialogBuilder<>(mActivity, BLOCKING_METHODS, itemDescription) .setTitle(new DialogTitleBuilder(mActivity) .setTitle(R.string.pref_default_blocking_method) .setSubtitle(R.string.pref_default_blocking_method_description) .build()) .setSelection(Prefs.Blocking.getDefaultBlockingMethod()) .setOnSingleChoiceClickListener((dialog, which, selectedBlockingMethod, isChecked) -> { if (!isChecked) { return; } Prefs.Blocking.setDefaultBlockingMethod(selectedBlockingMethod); defaultBlockingMethod.setSummary(BLOCKING_METHOD_TITLES[which]); dialog.dismiss(); }) .setNegativeButton(R.string.close, null) .show(); return true; }); // Global blocking enabled final SwitchPreferenceCompat gcb = Objects.requireNonNull(findPreference("global_blocking_enabled")); gcb.setChecked(Prefs.Blocking.globalBlockingEnabled()); gcb.setOnPreferenceChangeListener((preference, isEnabled) -> { if ((boolean) isEnabled) { model.applyAllRules(); } return true; }); // Remove all rules ((Preference) Objects.requireNonNull(findPreference("remove_all_rules"))).setOnPreferenceClickListener(preference -> { new MaterialAlertDialogBuilder(mActivity) .setTitle(R.string.pref_remove_all_rules) .setMessage(getString(R.string.are_you_sure) + " " + getString(R.string.pref_remove_all_rules_msg)) .setPositiveButton(R.string.yes, (dialog, which) -> { mActivity.progressIndicator.show(); model.removeAllRules(); }) .setNegativeButton(R.string.no, null) .show(); return true; }); } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setEnterTransition(new MaterialSharedAxis(MaterialSharedAxis.Z, true)); setReturnTransition(new MaterialSharedAxis(MaterialSharedAxis.Z, false)); } @Override public int getTitle() { return R.string.rules; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/settings/SecurityAndOpsViewModel.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.settings; import android.app.Application; import android.os.Build; import androidx.annotation.AnyThread; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.self.Migrations; import io.github.muntashirakon.AppManager.utils.AppPref; import io.github.muntashirakon.AppManager.utils.MultithreadedExecutor; public class SecurityAndOpsViewModel extends AndroidViewModel implements Ops.AdbConnectionInterface { public static final String TAG = SecurityAndOpsViewModel.class.getSimpleName(); private boolean mIsAuthenticating = false; private final MutableLiveData mAuthenticationStatus = new MutableLiveData<>(); private final MultithreadedExecutor mExecutor = MultithreadedExecutor.getNewInstance(); public SecurityAndOpsViewModel(@NonNull Application application) { super(application); } @Override protected void onCleared() { mExecutor.shutdown(); super.onCleared(); } public boolean isAuthenticating() { return mIsAuthenticating; } public void setAuthenticating(boolean authenticating) { mIsAuthenticating = authenticating; } public LiveData authenticationStatus() { return mAuthenticationStatus; } @AnyThread public void setModeOfOps() { mExecutor.submit(() -> { // Migration long thisVersion = BuildConfig.VERSION_CODE; long lastVersion = AppPref.getLong(AppPref.PrefKey.PREF_LAST_VERSION_CODE_LONG); if (lastVersion == 0) { // First version: set this as the last version AppPref.set(AppPref.PrefKey.PREF_LAST_VERSION_CODE_LONG, (long) BuildConfig.VERSION_CODE); AppPref.set(AppPref.PrefKey.PREF_DISPLAY_CHANGELOG_LAST_VERSION_LONG, (long) BuildConfig.VERSION_CODE); } if (lastVersion < thisVersion) { Log.d(TAG, "Start migration"); // App is updated AppPref.set(AppPref.PrefKey.PREF_DISPLAY_CHANGELOG_BOOL, true); Migrations.startMigration(lastVersion); // Migration is done: set this as the last version AppPref.set(AppPref.PrefKey.PREF_LAST_VERSION_CODE_LONG, (long) BuildConfig.VERSION_CODE); Log.d(TAG, "End migration"); } // Ops Log.d(TAG, "Before Ops::init"); int status = Ops.init(getApplication(), false); Log.d(TAG, "After Ops::init"); mAuthenticationStatus.postValue(status); }); } @AnyThread @RequiresApi(Build.VERSION_CODES.R) public void autoConnectWirelessDebugging() { mExecutor.submit(() -> { Log.d(TAG, "Before Ops::autoConnectWirelessDebugging"); int status = Ops.autoConnectWirelessDebugging(getApplication()); Log.d(TAG, "After Ops::autoConnectWirelessDebugging"); mAuthenticationStatus.postValue(status); }); } @Override @AnyThread public void connectAdb(int port) { mExecutor.submit(() -> { Log.d(TAG, "Before Ops::connectAdb"); int status = Ops.connectAdb(getApplication(), port, Ops.STATUS_FAILURE); Log.d(TAG, "After Ops::connectAdb"); mAuthenticationStatus.postValue(status); }); } @Override @AnyThread @RequiresApi(Build.VERSION_CODES.R) public void pairAdb() { mExecutor.submit(() -> { Log.d(TAG, "Before Ops::pairAdb"); int status = Ops.pairAdb(getApplication()); Log.d(TAG, "After Ops::pairAdb"); mAuthenticationStatus.postValue(status); }); } @Override public void onStatusReceived(int status) { mAuthenticationStatus.postValue(status); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/settings/SettingsActivity.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.settings; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.view.MenuItem; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentContainerView; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; import com.google.android.material.appbar.MaterialToolbar; import com.google.android.material.progressindicator.LinearProgressIndicator; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; import io.github.muntashirakon.AppManager.BaseActivity; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.self.SelfUriManager; import io.github.muntashirakon.util.UiUtils; public class SettingsActivity extends BaseActivity implements PreferenceFragmentCompat.OnPreferenceStartFragmentCallback { public static final String TAG = SettingsActivity.class.getSimpleName(); private static final String SAVED_KEYS = "saved_keys"; @NonNull public static Intent getSettingsIntent(@NonNull Context context, @Nullable String... paths) { Intent intent = new Intent(context, SettingsActivity.class); if (paths != null) { intent.setData(getSettingUri(paths)); } return intent; } @NonNull private static Uri getSettingUri(@NonNull String... pathSegments) { Uri.Builder builder = new Uri.Builder() .scheme(SelfUriManager.APP_MANAGER_SCHEME) .authority(SelfUriManager.SETTINGS_HOST); for (String pathSegment : pathSegments) { builder.appendPath(pathSegment); } return builder.build(); } public LinearProgressIndicator progressIndicator; @NonNull private List mKeys = Collections.emptyList(); @NonNull private ArrayList mSavedKeys = new ArrayList<>(); private int mLevel = 0; private boolean mDualPaneMode; @Nullable private MaterialToolbar mSecondaryToolbar; @Override protected void onAuthenticated(Bundle savedInstanceState) { int mainPrefSize = UiUtils.dpToPx(this, 450); int windowWidth = getResources().getDisplayMetrics().widthPixels; mDualPaneMode = windowWidth >= 2 * mainPrefSize; setContentView(mDualPaneMode ? R.layout.activity_settings_dual_pane : R.layout.activity_settings); setSupportActionBar(findViewById(R.id.toolbar)); mSecondaryToolbar = findViewById(R.id.toolbar2); FragmentContainerView secondaryContainer = findViewById(R.id.secondary_layout); progressIndicator = findViewById(R.id.progress_linear); progressIndicator.setVisibilityAfterHide(View.GONE); progressIndicator.hide(); // Apply necessary padding: ignore start if (mSecondaryToolbar != null) { UiUtils.applyWindowInsetsAsPadding(mSecondaryToolbar, true, false, false, true); } if (secondaryContainer != null) { UiUtils.applyWindowInsetsAsPadding(secondaryContainer, false, true, false, true); } if (savedInstanceState != null) { clearBackStack(); ArrayList savedKeys = savedInstanceState.getStringArrayList(SAVED_KEYS); if (savedKeys != null) { mSavedKeys = savedKeys; } } setKeysFromIntent(getIntent()); getSupportFragmentManager().addFragmentOnAttachListener((fragmentManager, fragment) -> { if (!(fragment instanceof MainPreferences)) { ++mLevel; } }); getSupportFragmentManager().addOnBackStackChangedListener(() -> { mLevel = getSupportFragmentManager().getBackStackEntryCount(); Log.d(TAG, "Backstack changed. Level: %d", mLevel); // Update saved level: Delete everything from mLevel to the last item) int size = mSavedKeys.size(); if (mLevel <= size - 1) { mSavedKeys.subList(mLevel, size).clear(); } }); String defaultPref = getKey(mLevel); if (defaultPref == null && mDualPaneMode) { defaultPref = "custom_locale"; } getSupportFragmentManager() .beginTransaction() .setCustomAnimations( R.animator.enter_from_left, R.animator.enter_from_right, R.animator.exit_from_right, R.animator.exit_from_left ) .replace(R.id.main_layout, MainPreferences.getInstance(defaultPref, mDualPaneMode)) .commit(); } @Override protected void onNewIntent(@NonNull Intent intent) { super.onNewIntent(intent); if (setKeysFromIntent(intent)) { // Clear old items mSavedKeys.clear(); clearBackStack(); Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.main_layout); if (fragment instanceof MainPreferences) { ((MainPreferences) fragment).setPrefKey(getKey(mLevel = 0)); Log.d(TAG, "Selected pref: %s", fragment.getClass().getName()); } } } @Override public void setTitle(int titleId) { if (mDualPaneMode) { Objects.requireNonNull(mSecondaryToolbar).setTitle(titleId); } else super.setTitle(titleId); } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == android.R.id.home) { getOnBackPressedDispatcher().onBackPressed(); return true; } return super.onOptionsItemSelected(item); } @Override public boolean onPreferenceStartFragment(@NonNull PreferenceFragmentCompat caller, @NonNull Preference pref) { if (pref.getFragment() == null) { return false; } FragmentManager fragmentManager = getSupportFragmentManager(); Bundle args = pref.getExtras(); Fragment fragment = fragmentManager.getFragmentFactory().instantiate(getClassLoader(), pref.getFragment()); if (fragment instanceof PreferenceFragment) { // Inject dual pane mode args.putBoolean(PreferenceFragment.PREF_SECONDARY, mDualPaneMode); // Inject subKey to the arguments String subKey = getKey(mLevel + 1); if (subKey != null && Objects.equals(pref.getKey(), getKey(mLevel))) { args.putString(PreferenceFragment.PREF_KEY, subKey); } // Save current key saveKey(mLevel, pref.getKey()); } fragment.setArguments(args); // The line below is kept because this is how it is handled in AndroidX library fragment.setTargetFragment(caller, 0); FragmentTransaction transaction = fragmentManager.beginTransaction(); if (!mDualPaneMode) { transaction.setCustomAnimations( R.animator.enter_from_left, R.animator.enter_from_right, R.animator.exit_from_right, R.animator.exit_from_left ).addToBackStack(null); } transaction .replace(mDualPaneMode ? R.id.secondary_layout : R.id.main_layout, fragment) .commit(); return true; } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { outState.putStringArrayList(SAVED_KEYS, mSavedKeys); super.onSaveInstanceState(outState); } @Nullable private String getKey(int level) { if (!mSavedKeys.isEmpty() && mSavedKeys.size() > level) { String key = mSavedKeys.get(level); if (key != null) { return key; } } if (mKeys.size() > level) { return mKeys.get(level); } return null; } private void saveKey(int level, @Nullable String key) { Log.d(TAG, "Save level: %d, Key: %s", level, key); int size = mSavedKeys.size(); if (level >= size) { // Create levels int count = level - size + 1; for (int i = 0; i < count; ++i) { mSavedKeys.add(null); } } // Add this level mSavedKeys.set(level, key); } private boolean setKeysFromIntent(@NonNull Intent intent) { Uri uri = intent.getData(); if (uri != null && SelfUriManager.APP_MANAGER_SCHEME.equals(uri.getScheme()) && SelfUriManager.SETTINGS_HOST.equals(uri.getHost()) && uri.getPath() != null) { mKeys = Objects.requireNonNull(uri.getPathSegments()); return true; } return false; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/settings/SettingsDataStore.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.settings; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.preference.PreferenceDataStore; import io.github.muntashirakon.AppManager.utils.AppPref; public class SettingsDataStore extends PreferenceDataStore { private final AppPref mAppPref; public SettingsDataStore() { super(); mAppPref = AppPref.getInstance(); } @Override public void putString(String key, @Nullable String value) { mAppPref.setPref(key, value); } @Override public void putInt(String key, int value) { mAppPref.setPref(key, value); } @Override public void putLong(String key, long value) { mAppPref.setPref(key, value); } @Override public void putFloat(String key, float value) { mAppPref.setPref(key, value); } @Override public void putBoolean(String key, boolean value) { mAppPref.setPref(key, value); } @NonNull @Override public String getString(String key, @Nullable String defValue) { return (String) mAppPref.get(key); } @Override public int getInt(String key, int defValue) { return (int) mAppPref.get(key); } @Override public long getLong(String key, long defValue) { return (long) mAppPref.get(key); } @Override public float getFloat(String key, float defValue) { return (float) mAppPref.get(key); } @Override public boolean getBoolean(String key, boolean defValue) { return (boolean) mAppPref.get(key); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/settings/TroubleshootingPreferences.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.settings; import android.os.Bundle; import androidx.annotation.Nullable; import androidx.lifecycle.ViewModelProvider; import androidx.preference.Preference; import com.google.android.material.transition.MaterialSharedAxis; import java.util.Objects; import io.github.muntashirakon.AppManager.R; public class TroubleshootingPreferences extends PreferenceFragment { @Override public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { setPreferencesFromResource(R.xml.preferences_troubleshooting, rootKey); getPreferenceManager().setPreferenceDataStore(new SettingsDataStore()); MainPreferencesViewModel model = new ViewModelProvider(requireActivity()).get(MainPreferencesViewModel.class); // Reload apps ((Preference) Objects.requireNonNull(findPreference("reload_apps"))) .setOnPreferenceClickListener(preference -> { model.reloadApps(); return true; }); } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setEnterTransition(new MaterialSharedAxis(MaterialSharedAxis.Z, true)); setReturnTransition(new MaterialSharedAxis(MaterialSharedAxis.Z, false)); } @Override public int getTitle() { return R.string.troubleshooting; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/settings/VirusTotalPreferences.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.settings; import android.graphics.Typeface; import android.os.Bundle; import android.text.InputType; import android.text.TextUtils; import android.view.inputmethod.EditorInfo; import androidx.annotation.Nullable; import androidx.core.view.inputmethod.EditorInfoCompat; import androidx.lifecycle.ViewModelProvider; import androidx.preference.Preference; import androidx.preference.SwitchPreferenceCompat; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.dialog.TextInputDialogBuilder; import io.github.muntashirakon.preference.DefaultAlertPreference; import io.github.muntashirakon.preference.TopSwitchPreference; public class VirusTotalPreferences extends PreferenceFragment { private MainPreferencesViewModel mModel; @Override public int getTitle() { return R.string.virus_total; } @Override public void onCreatePreferences(@Nullable Bundle bundle, @Nullable String rootKey) { setPreferencesFromResource(R.xml.preferences_virus_total, rootKey); getPreferenceManager().setPreferenceDataStore(new SettingsDataStore()); mModel = new ViewModelProvider(requireActivity()).get(MainPreferencesViewModel.class); boolean hasInternet = FeatureController.isInternetEnabled(); boolean isVtEnabled = FeatureController.isVirusTotalEnabled(); String apiKey = Prefs.VirusTotal.getApiKey(); TopSwitchPreference useVtPref = requirePreference("use_vt"); DefaultAlertPreference infoNoInternetPref = requirePreference("info_no_internet"); Preference vtApiKeyPref = requirePreference("virus_total_api_key"); SwitchPreferenceCompat promptBeforeUploadPref = requirePreference("virus_total_prompt_before_uploading"); DefaultAlertPreference infoPref = requirePreference("info"); // Set values useVtPref.setEnabled(hasInternet); useVtPref.setChecked(isVtEnabled); useVtPref.setOnPreferenceChangeListener((preference, newValue) -> { boolean isEnabled = (boolean) newValue; enablePrefs(isEnabled, vtApiKeyPref, promptBeforeUploadPref); FeatureController.getInstance().modifyState(FeatureController.FEAT_VIRUS_TOTAL, isEnabled); return true; }); infoNoInternetPref.setVisible(!hasInternet); enablePrefs(isVtEnabled, vtApiKeyPref, promptBeforeUploadPref); if (apiKey != null) { vtApiKeyPref.setSummary(apiKey); } else { vtApiKeyPref.setSummary(R.string.key_not_set); } vtApiKeyPref.setOnPreferenceClickListener(preference -> { new TextInputDialogBuilder(requireContext(), null) .setTitle(R.string.pref_vt_apikey) .setInputText(Prefs.VirusTotal.getApiKey()) .setInputTypeface(Typeface.MONOSPACE) .setInputInputType(InputType.TYPE_CLASS_TEXT) .setInputImeOptions(EditorInfo.IME_ACTION_DONE | EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING) .setNegativeButton(R.string.cancel, null) .setPositiveButton(R.string.save, (dialog, which, inputText, isChecked) -> { String newApiKey = !TextUtils.isEmpty(inputText) ? inputText.toString() : null; Prefs.VirusTotal.setApiKey(newApiKey); if (newApiKey != null) { vtApiKeyPref.setSummary(newApiKey); } else { vtApiKeyPref.setSummary(R.string.key_not_set); } }) .show(); return true; }); promptBeforeUploadPref.setChecked(Prefs.VirusTotal.promptBeforeUpload()); infoPref.setSummary(getString(R.string.pref_vt_apikey_description) + "\n\n" + getString(R.string.vt_disclaimer)); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/settings/crypto/AESCryptoSelectionDialogFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.settings.crypto; import static io.github.muntashirakon.AppManager.crypto.AESCrypto.AES_KEY_ALIAS; import android.app.Dialog; import android.content.DialogInterface; import android.os.Bundle; import android.text.Editable; import android.text.TextUtils; import android.widget.Button; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.FragmentActivity; import java.nio.CharBuffer; import java.security.SecureRandom; import java.util.Arrays; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import javax.security.auth.DestroyFailedException; import aosp.libcore.util.HexEncoding; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.backup.CryptoUtils; import io.github.muntashirakon.AppManager.crypto.ks.KeyStoreManager; import io.github.muntashirakon.AppManager.crypto.ks.SecretKeyCompat; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.AppManager.utils.Utils; import io.github.muntashirakon.dialog.TextInputDialogBuilder; import io.github.muntashirakon.dialog.TextInputDropdownDialogBuilder; public class AESCryptoSelectionDialogFragment extends DialogFragment { public static final String TAG = "AESCryptoSelectionDialogFragment"; private FragmentActivity mActivity; private TextInputDialogBuilder mBuilder; @Nullable private KeyStoreManager mKeyStoreManager; @Nullable private char[] mKeyChars; @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { mActivity = requireActivity(); mBuilder = new TextInputDialogBuilder(mActivity, R.string.input_key) .setTitle(R.string.aes) .setNeutralButton(R.string.generate_key, null) .setNegativeButton(R.string.cancel, null) .setPositiveButton(R.string.save, null) .setOnShowListener(dialog -> { AlertDialog dialog1 = (AlertDialog) dialog; Button positiveButton = dialog1.getButton(AlertDialog.BUTTON_POSITIVE); Button neutralButton = dialog1.getButton(AlertDialog.BUTTON_NEUTRAL); // Save positiveButton.setOnClickListener(v -> { Editable inputText = mBuilder.getInputText(); if (TextUtils.isEmpty(inputText)) return; if (mKeyStoreManager == null) { UIUtils.displayLongToast(R.string.failed_to_initialize_key_store); return; } mKeyChars = new char[inputText.length()]; inputText.getChars(0, inputText.length(), mKeyChars, 0); byte[] keyBytes; try { keyBytes = HexEncoding.decode(mKeyChars); } catch (IllegalArgumentException e) { UIUtils.displayLongToast(R.string.invalid_aes_key_size); return; } if (keyBytes.length != 16 && keyBytes.length != 32) { UIUtils.displayLongToast(R.string.invalid_aes_key_size); return; } SecretKey secretKey = new SecretKeySpec(keyBytes, "AES"); try { mKeyStoreManager.addSecretKey(AES_KEY_ALIAS, secretKey, true); Prefs.Encryption.setEncryptionMode(CryptoUtils.MODE_AES); } catch (Exception e) { Log.e(TAG, e); UIUtils.displayLongToast(R.string.failed_to_save_key); } Utils.clearBytes(keyBytes); try { SecretKeyCompat.destroy(secretKey); } catch (DestroyFailedException e) { Log.e(TAG, e); } dialog.dismiss(); }); // Key generator neutralButton.setOnClickListener(v -> new TextInputDropdownDialogBuilder(mActivity, R.string.crypto_key_size) .setDropdownItems(Arrays.asList(128, 256), 0, false) .setTitle(R.string.generate_key) .setNegativeButton(R.string.cancel, null) .setPositiveButton(R.string.generate_key, (dialog2, which, inputText, isChecked) -> { if (TextUtils.isEmpty(inputText)) return; int keySize = 128 / 8; try { //noinspection ConstantConditions keySize = Integer.decode(inputText.toString().trim()) / 8; } catch (NumberFormatException ignore) { } SecureRandom random = new SecureRandom(); byte[] key = new byte[keySize]; random.nextBytes(key); mKeyChars = HexEncoding.encode(key); mBuilder.setInputText(CharBuffer.wrap(mKeyChars)); }) .show()); }); ThreadUtils.postOnBackgroundThread(() -> { try { mKeyStoreManager = KeyStoreManager.getInstance(); SecretKey secretKey = mKeyStoreManager.getSecretKey(AES_KEY_ALIAS); if (secretKey != null) { mKeyChars = HexEncoding.encode(secretKey.getEncoded()); try { SecretKeyCompat.destroy(secretKey); } catch (Exception ex) { Log.e(TAG, ex); } mActivity.runOnUiThread(() -> mBuilder.setInputText(CharBuffer.wrap(mKeyChars))); } } catch (Exception e) { Log.e(TAG, e); } }); return mBuilder.create(); } @Override public void onDismiss(@NonNull DialogInterface dialog) { if (mKeyChars != null) Utils.clearChars(mKeyChars); } @Override public void onDestroy() { super.onDestroy(); if (mKeyChars != null) Utils.clearChars(mKeyChars); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/settings/crypto/ECCCryptoSelectionDialogFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.settings.crypto; import android.app.Dialog; import android.os.Bundle; import android.widget.Button; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.annotation.WorkerThread; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.FragmentActivity; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.backup.CryptoUtils; import io.github.muntashirakon.AppManager.crypto.ECCCrypto; import io.github.muntashirakon.AppManager.crypto.ks.KeyPair; import io.github.muntashirakon.AppManager.crypto.ks.KeyStoreManager; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.utils.PackageUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.dialog.ScrollableDialogBuilder; public class ECCCryptoSelectionDialogFragment extends DialogFragment { public static final String TAG = ECCCryptoSelectionDialogFragment.class.getSimpleName(); public interface OnKeyPairUpdatedListener { @UiThread void keyPairUpdated(@Nullable KeyPair keyPair, @Nullable byte[] certificateBytes); } private FragmentActivity mActivity; private ScrollableDialogBuilder mBuilder; @Nullable private OnKeyPairUpdatedListener mListener; @Nullable private KeyStoreManager mKeyStoreManager; private final String mTargetAlias = ECCCrypto.ECC_KEY_ALIAS; public void setOnKeyPairUpdatedListener(OnKeyPairUpdatedListener listener) { mListener = listener; } @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { mActivity = requireActivity(); mBuilder = new ScrollableDialogBuilder(mActivity) .setTitle(R.string.ecc) .setPositiveButton(R.string.ok, null) .setNegativeButton(R.string.pref_import, null) .setNeutralButton(R.string.generate_key, null); ThreadUtils.postOnBackgroundThread(() -> { if (ThreadUtils.isInterrupted()) return; CharSequence info = getSigningInfo(); ThreadUtils.postOnMainThread(() -> mBuilder.setMessage(info)); }); AlertDialog alertDialog = mBuilder.create(); alertDialog.setOnShowListener(dialog -> { AlertDialog dialog1 = (AlertDialog) dialog; Button defaultOrOkButton = dialog1.getButton(AlertDialog.BUTTON_POSITIVE); Button importButton = dialog1.getButton(AlertDialog.BUTTON_NEGATIVE); Button generateButton = dialog1.getButton(AlertDialog.BUTTON_NEUTRAL); importButton.setOnClickListener(v -> { KeyPairImporterDialogFragment fragment = new KeyPairImporterDialogFragment(); Bundle args = new Bundle(); args.putString(KeyPairImporterDialogFragment.EXTRA_ALIAS, mTargetAlias); fragment.setArguments(args); fragment.setOnKeySelectedListener((keyPair) -> ThreadUtils.postOnBackgroundThread(() -> addKeyPair(keyPair))); fragment.show(getParentFragmentManager(), KeyPairImporterDialogFragment.TAG); }); generateButton.setOnClickListener(v -> { KeyPairGeneratorDialogFragment fragment = new KeyPairGeneratorDialogFragment(); Bundle args = new Bundle(); args.putString(KeyPairGeneratorDialogFragment.EXTRA_KEY_TYPE, CryptoUtils.MODE_ECC); fragment.setArguments(args); fragment.setOnGenerateListener((keyPair) -> ThreadUtils.postOnBackgroundThread(() -> addKeyPair(keyPair))); fragment.show(getParentFragmentManager(), KeyPairGeneratorDialogFragment.TAG); }); defaultOrOkButton.setOnClickListener(v -> ThreadUtils.postOnBackgroundThread(() -> { try { if (ThreadUtils.isInterrupted()) return; ThreadUtils.postOnMainThread(() -> UIUtils.displayShortToast(R.string.done)); keyPairUpdated(); } catch (Exception e) { Log.e(TAG, e); ThreadUtils.postOnMainThread(() -> UIUtils.displayLongToast(R.string.failed_to_save_key)); } finally { alertDialog.dismiss(); } })); }); return alertDialog; } @WorkerThread private CharSequence getSigningInfo() { KeyPair keyPair = getKeyPair(); if (keyPair != null) { try { return PackageUtils.getSigningCertificateInfo(mActivity, (X509Certificate) keyPair.getCertificate()); } catch (CertificateEncodingException e) { return getString(R.string.failed_to_load_key); } } return getString(R.string.key_not_set); } @WorkerThread private void addKeyPair(@Nullable KeyPair keyPair) { try { if (keyPair == null) { throw new Exception("Keypair can't be null."); } mKeyStoreManager = KeyStoreManager.getInstance(); mKeyStoreManager.addKeyPair(mTargetAlias, keyPair, true); if (ThreadUtils.isInterrupted()) return; ThreadUtils.postOnMainThread(() -> UIUtils.displayShortToast(R.string.done)); keyPairUpdated(); if (ThreadUtils.isInterrupted()) return; CharSequence info = getSigningInfo(); ThreadUtils.postOnMainThread(() -> mBuilder.setMessage(info)); } catch (Exception e) { Log.e(TAG, e); ThreadUtils.postOnMainThread(() -> UIUtils.displayLongToast(R.string.failed_to_save_key)); } } @WorkerThread private void keyPairUpdated() { try { KeyPair keyPair = getKeyPair(); if (keyPair != null) { if (mListener != null) { byte[] bytes = keyPair.getCertificate().getEncoded(); ThreadUtils.postOnMainThread(() -> mListener.keyPairUpdated(keyPair, bytes)); } return; } } catch (Exception e) { Log.e(TAG, e); } if (mListener != null) { ThreadUtils.postOnMainThread(() -> mListener.keyPairUpdated(null, null)); } } @WorkerThread @Nullable private KeyPair getKeyPair() { try { mKeyStoreManager = KeyStoreManager.getInstance(); if (mKeyStoreManager.containsKey(mTargetAlias)) { return mKeyStoreManager.getKeyPair(mTargetAlias); } } catch (Exception e) { Log.e(TAG, e); } return null; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/settings/crypto/ImportExportKeyStoreDialogFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.settings.crypto; import static io.github.muntashirakon.AppManager.crypto.ks.KeyStoreManager.AM_KEYSTORE_FILE; import android.app.Dialog; import android.os.Bundle; import android.widget.Button; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.FragmentActivity; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.crypto.ks.KeyStoreManager; import io.github.muntashirakon.AppManager.utils.ExUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.io.IoUtils; public class ImportExportKeyStoreDialogFragment extends DialogFragment { public static final String TAG = "IEKeyStoreDialogFragment"; private FragmentActivity mActivity; private final ActivityResultLauncher mExportKeyStore = registerForActivityResult( new ActivityResultContracts.CreateDocument("application/octet-stream"), uri -> { if (uri == null) { dismiss(); return; } ThreadUtils.postOnBackgroundThread(() -> { try (InputStream is = new FileInputStream(AM_KEYSTORE_FILE); OutputStream os = mActivity.getContentResolver().openOutputStream(uri)) { if (os == null) throw new IOException("Unable to open URI"); IoUtils.copy(is, os); ThreadUtils.postOnMainThread(() -> { UIUtils.displayShortToast(R.string.done); ExUtils.exceptionAsIgnored(this::dismiss); }); } catch (IOException e) { ThreadUtils.postOnMainThread(() -> { UIUtils.displayShortToast(R.string.failed); ExUtils.exceptionAsIgnored(this::dismiss); }); } }); }); private final ActivityResultLauncher mImportKeyStore = registerForActivityResult( new ActivityResultContracts.GetContent(), uri -> { if (uri == null) { dismiss(); return; } new MaterialAlertDialogBuilder(mActivity) .setTitle(R.string.import_keystore) .setMessage(R.string.confirm_import_keystore) .setPositiveButton(R.string.yes, (dialog, which) -> ThreadUtils.postOnBackgroundThread(() -> { // Rename old file that will be restored in case of error File tmpFile = new File(AM_KEYSTORE_FILE.getAbsolutePath() + ".tmp"); if (AM_KEYSTORE_FILE.exists()) { AM_KEYSTORE_FILE.renameTo(tmpFile); } try (InputStream is = mActivity.getContentResolver().openInputStream(uri); OutputStream os = new FileOutputStream(AM_KEYSTORE_FILE)) { if (is == null) throw new IOException("Unable to open URI"); IoUtils.copy(is, os); if (KeyStoreManager.hasKeyStorePassword()) { CountDownLatch waitForKs = new CountDownLatch(1); KeyStoreManager.inputKeyStorePassword(mActivity, waitForKs::countDown); waitForKs.await(2, TimeUnit.MINUTES); if (waitForKs.getCount() == 1) { throw new Exception(); } } KeyStoreManager.reloadKeyStore(); // TODO: 21/4/21 Only import the keys that we use instead of replacing the entire keystore ThreadUtils.postOnMainThread(() -> { UIUtils.displayShortToast(R.string.done); ExUtils.exceptionAsIgnored(this::dismiss); }); } catch (Exception e) { if (tmpFile.exists()) { AM_KEYSTORE_FILE.delete(); tmpFile.renameTo(AM_KEYSTORE_FILE); try { KeyStoreManager.reloadKeyStore(); } catch (Exception ignore) { } } ThreadUtils.postOnMainThread(() -> { UIUtils.displayShortToast(R.string.failed); ExUtils.exceptionAsIgnored(this::dismiss); }); } })) .setNegativeButton(R.string.close, (dialog, which) -> dismiss()) .setCancelable(false) .show(); }); @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { mActivity = requireActivity(); MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(mActivity) .setTitle(R.string.pref_import_export_keystore) .setMessage(R.string.choose_what_to_do) .setPositiveButton(R.string.pref_export, null) .setNegativeButton(R.string.cancel, null) .setNeutralButton(R.string.pref_import, null); AlertDialog alertDialog = builder.create(); alertDialog.setOnShowListener(dialog -> { AlertDialog dialog1 = (AlertDialog) dialog; Button exportButton = dialog1.getButton(AlertDialog.BUTTON_POSITIVE); Button importButton = dialog1.getButton(AlertDialog.BUTTON_NEUTRAL); if (AM_KEYSTORE_FILE.exists()) { exportButton.setOnClickListener(v -> mExportKeyStore.launch(KeyStoreManager.AM_KEYSTORE_FILE_NAME)); } importButton.setOnClickListener(v -> mImportKeyStore.launch("application/*")); }); return alertDialog; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/settings/crypto/KeyPairGeneratorDialogFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.settings.crypto; import android.app.Dialog; import android.os.Bundle; import android.text.TextUtils; import android.view.View; import android.widget.EditText; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.FragmentActivity; import com.google.android.material.datepicker.MaterialDatePicker; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.atomic.AtomicReference; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.backup.CryptoUtils; import io.github.muntashirakon.AppManager.crypto.ks.KeyPair; import io.github.muntashirakon.AppManager.crypto.ks.KeyStoreUtils; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.utils.DateUtils; import io.github.muntashirakon.AppManager.utils.ExUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.adapters.SelectedArrayAdapter; import io.github.muntashirakon.dialog.AlertDialogBuilder; import io.github.muntashirakon.widget.MaterialSpinner; public class KeyPairGeneratorDialogFragment extends DialogFragment { public static final String TAG = "KeyPairGeneratorDialogFragment"; public static final String EXTRA_KEY_TYPE = "type"; public static final List SUPPORTED_RSA_KEY_SIZES = Arrays.asList(2048, 4096); public interface OnGenerateListener { void onGenerate(@Nullable KeyPair keyPair); } private OnGenerateListener mListener; private int mKeySize; private long mExpiryDate; @CryptoUtils.Mode private String mKeyType; public void setOnGenerateListener(OnGenerateListener listener) { mListener = listener; } @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { FragmentActivity activity = requireActivity(); mKeyType = requireArguments().getString(EXTRA_KEY_TYPE, CryptoUtils.MODE_RSA); View view = View.inflate(activity, R.layout.dialog_certificate_generator, null); MaterialSpinner keySizeSpinner = view.findViewById(R.id.key_size_selector_spinner); if (mKeyType.equals(CryptoUtils.MODE_RSA)) { mKeySize = 2048; keySizeSpinner.setAdapter(new SelectedArrayAdapter<>(activity, androidx.appcompat.R.layout.support_simple_spinner_dropdown_item, SUPPORTED_RSA_KEY_SIZES)); keySizeSpinner.setOnItemClickListener((parent, view1, position, id) -> mKeySize = SUPPORTED_RSA_KEY_SIZES.get(position)); } else { // There's no keysize for ECC keySizeSpinner.setVisibility(View.GONE); } EditText expiryDate = view.findViewById(R.id.expiry_date); expiryDate.setKeyListener(null); expiryDate.setOnFocusChangeListener((v, hasFocus) -> { if (v.isInTouchMode() && hasFocus) { v.performClick(); } }); expiryDate.setOnClickListener(v -> pickExpiryDate(expiryDate)); EditText commonName = view.findViewById(R.id.common_name); EditText orgUnit = view.findViewById(R.id.organization_unit); EditText orgName = view.findViewById(R.id.organization_name); EditText locality = view.findViewById(R.id.locality_name); EditText state = view.findViewById(R.id.state_name); EditText country = view.findViewById(R.id.country_name); AlertDialogBuilder builder = new AlertDialogBuilder(activity, true) .setTitle(R.string.generate_key) .setView(view) .setExitOnButtonPress(false) .setPositiveButton(R.string.generate_key, (dialog, which) -> ThreadUtils.postOnBackgroundThread(() -> { AtomicReference keyPair = new AtomicReference<>(null); String formattedSubject = getFormattedSubject(commonName.getText().toString(), orgUnit.getText().toString(), orgName.getText().toString(), locality.getText().toString(), state.getText().toString(), country.getText().toString()); if (mExpiryDate == 0) { ThreadUtils.postOnMainThread(() -> UIUtils.displayShortToast(R.string.expiry_date_cannot_be_empty)); return; } if (formattedSubject.isEmpty()) { formattedSubject = "CN=App Manager"; } try { if (mKeyType.equals(CryptoUtils.MODE_RSA)) { keyPair.set(KeyStoreUtils.generateRSAKeyPair(formattedSubject, mKeySize, mExpiryDate)); } else if (mKeyType.equals(CryptoUtils.MODE_ECC)) { keyPair.set(KeyStoreUtils.generateECCKeyPair(formattedSubject, mExpiryDate)); } } catch (Exception e) { Log.e(TAG, e); } finally { ThreadUtils.postOnMainThread(() -> { if (mListener != null) mListener.onGenerate(keyPair.get()); ExUtils.exceptionAsIgnored(dialog::dismiss); }); } })) .setNegativeButton(R.string.cancel, null); return builder.create(); } @NonNull public String getFormattedSubject(@Nullable String commonName, @Nullable String organizationUnit, @Nullable String organizationName, @Nullable String localityName, @Nullable String stateName, @Nullable String countryName) { List subjectArray = new ArrayList<>(6); if (!TextUtils.isEmpty(commonName)) subjectArray.add("CN=" + commonName); if (!TextUtils.isEmpty(organizationUnit)) subjectArray.add("OU=" + organizationUnit); if (!TextUtils.isEmpty(organizationName)) subjectArray.add("O=" + organizationName); if (!TextUtils.isEmpty(localityName)) subjectArray.add("L=" + localityName); if (!TextUtils.isEmpty(stateName)) subjectArray.add("ST=" + stateName); if (!TextUtils.isEmpty(countryName)) subjectArray.add("C=" + countryName); return TextUtils.join(", ", subjectArray); } @UiThread public void pickExpiryDate(EditText expiryDate) { MaterialDatePicker datePicker = MaterialDatePicker.Builder.datePicker() .setTitleText(R.string.expiry_date) .setSelection(MaterialDatePicker.todayInUtcMilliseconds()) .build(); datePicker.addOnPositiveButtonClickListener(selection -> { mExpiryDate = selection; expiryDate.setText(DateUtils.formatDate(requireContext(), selection)); }); datePicker.show(getChildFragmentManager(), "DatePicker"); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/settings/crypto/KeyPairImporterDialogFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.settings.crypto; import android.app.Dialog; import android.net.Uri; import android.os.Bundle; import android.text.method.KeyListener; import android.view.View; import android.widget.Button; import android.widget.EditText; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.FragmentActivity; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.textfield.TextInputLayout; import java.util.ArrayList; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.crypto.ks.KeyPair; import io.github.muntashirakon.AppManager.crypto.ks.KeyStoreUtils; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.utils.BetterActivityResult; import io.github.muntashirakon.AppManager.utils.ExUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.AppManager.utils.Utils; import io.github.muntashirakon.adapters.SelectedArrayAdapter; import io.github.muntashirakon.dialog.TextInputDropdownDialogBuilder; import io.github.muntashirakon.widget.MaterialSpinner; public class KeyPairImporterDialogFragment extends DialogFragment { public static final String TAG = "KeyPairImporterDialogFragment"; public static final String EXTRA_ALIAS = "alias"; public interface OnKeySelectedListener { void onKeySelected(@Nullable KeyPair keyPair); } @Nullable private OnKeySelectedListener mListener; private FragmentActivity mActivity; private TextInputLayout mKsPassOrPk8Layout; private EditText mKsPassOrPk8; private KeyListener mKeyListener; private TextInputLayout mKsLocationOrPemLayout; private EditText mKsLocationOrPem; @KeyStoreUtils.KeyType private int mKeyType; @Nullable private Uri mKsOrPemFile; @Nullable private Uri mPk8File; private final BetterActivityResult mImportFile = BetterActivityResult .registerForActivityResult(this, new ActivityResultContracts.GetContent()); public void setOnKeySelectedListener(OnKeySelectedListener listener) { mListener = listener; } @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { mActivity = requireActivity(); String targetAlias = requireArguments().getString(EXTRA_ALIAS); if (targetAlias == null) { return super.onCreateDialog(savedInstanceState); } View view = getLayoutInflater().inflate(R.layout.dialog_key_pair_importer, null); MaterialSpinner keyTypeSpinner = view.findViewById(R.id.key_type_selector_spinner); mKsPassOrPk8Layout = view.findViewById(R.id.hint); mKsPassOrPk8 = view.findViewById(R.id.text); mKeyListener = mKsPassOrPk8.getKeyListener(); mKsLocationOrPemLayout = view.findViewById(R.id.hint2); mKsLocationOrPem = view.findViewById(R.id.text2); mKsLocationOrPem.setKeyListener(null); mKsLocationOrPem.setOnFocusChangeListener((v, hasFocus) -> { if (v.isInTouchMode() && hasFocus) { v.performClick(); } }); mKsLocationOrPem.setOnClickListener(v -> mImportFile.launch("application/*", result -> { mKsOrPemFile = result; if (result != null) { mKsLocationOrPem.setText(result.toString()); } })); keyTypeSpinner.setAdapter(SelectedArrayAdapter.createFromResource(mActivity, R.array.crypto_import_types, io.github.muntashirakon.ui.R.layout.auto_complete_dropdown_item)); keyTypeSpinner.setOnItemClickListener((parent, view1, position, id) -> { mKsPassOrPk8.setText(null); mKsLocationOrPem.setText(null); if (position == KeyStoreUtils.KeyType.PK8) { // PKCS #8 and PEM mKsPassOrPk8Layout.setHint(R.string.pk8_file); mKsPassOrPk8.setKeyListener(null); mKsPassOrPk8.setOnFocusChangeListener((v, hasFocus) -> { if (v.isInTouchMode() && hasFocus) { v.performClick(); } }); mKsPassOrPk8.setOnClickListener(v -> mImportFile.launch("application/*", result -> { mPk8File = result; if (result != null) { mKsPassOrPk8.setText(result.toString()); } })); mKsLocationOrPemLayout.setHint(R.string.pem_file); } else { // KeyStore setDefault(); } mKeyType = position; }); setDefault(); AlertDialog alertDialog = new MaterialAlertDialogBuilder(mActivity) .setTitle(R.string.import_key) .setView(view) .setPositiveButton(R.string.ok, null) .setNegativeButton(R.string.cancel, null) .create(); alertDialog.setOnShowListener(dialog -> { AlertDialog dialog1 = (AlertDialog) dialog; Button okButton = dialog1.getButton(AlertDialog.BUTTON_POSITIVE); okButton.setOnClickListener(v -> { if (mListener == null) return; if (mKeyType == KeyStoreUtils.KeyType.PK8) { // PKCS #8 and PEM try { if (mPk8File == null || mKsOrPemFile == null) { throw new Exception("PK8 or PEM can't be null."); } KeyPair keyPair = KeyStoreUtils.getKeyPair(mActivity, mPk8File, mKsOrPemFile); mListener.onKeySelected(keyPair); } catch (Exception e) { Log.e(TAG, e); mListener.onKeySelected(null); } dialog.dismiss(); } else { // KeyStore char[] ksPassword = Utils.getChars(mKsPassOrPk8.getText()); ThreadUtils.postOnBackgroundThread(() -> { try { if (mKsOrPemFile == null) { throw new Exception("KeyStore file can't be null."); } ArrayList aliases = KeyStoreUtils.listAliases(mActivity, mKsOrPemFile, mKeyType, ksPassword); if (mListener == null) return; ThreadUtils.postOnMainThread(() -> { if (aliases.isEmpty()) { UIUtils.displayLongToast(R.string.found_no_alias_in_keystore); ExUtils.exceptionAsIgnored(dialog::dismiss); return; } TextInputDropdownDialogBuilder builder; builder = new TextInputDropdownDialogBuilder(mActivity, R.string.choose_an_alias) .setDropdownItems(aliases, -1, true) .setAuxiliaryInputLabel(R.string.alias_pass) .setTitle(R.string.choose_an_alias) .setNegativeButton(R.string.cancel, null); builder.setPositiveButton(R.string.ok, (dialog2, which, inputText, isChecked) -> { String aliasName = inputText == null ? null : inputText.toString(); char[] aliasPassword = Utils.getChars(builder.getAuxiliaryInput()); ThreadUtils.postOnBackgroundThread(() -> { try { KeyPair keyPair = KeyStoreUtils.getKeyPair(mActivity, mKsOrPemFile, mKeyType, aliasName, ksPassword, aliasPassword); mListener.onKeySelected(keyPair); } catch (Exception e) { Log.e(TAG, e); mListener.onKeySelected(null); } ThreadUtils.postOnMainThread(() -> ExUtils.exceptionAsIgnored(dialog::dismiss)); }); }).show(); }); } catch (Exception e) { Log.e(TAG, e); ThreadUtils.postOnMainThread(() -> UIUtils.displayLongToast(R.string.failed_to_read_keystore)); } }); } }); }); return alertDialog; } private void setDefault() { mKeyType = KeyStoreUtils.KeyType.JKS; // KeyStore mKsPassOrPk8Layout.setHint(R.string.keystore_pass); mKsPassOrPk8.setKeyListener(mKeyListener); mKsPassOrPk8.setOnFocusChangeListener(null); mKsPassOrPk8.setOnClickListener(null); mKsLocationOrPemLayout.setHint(R.string.keystore_file); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/settings/crypto/OpenPgpKeySelectionDialogFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.settings.crypto; import android.app.Dialog; import android.app.PendingIntent; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; import android.os.Bundle; import android.text.TextUtils; import android.widget.Button; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.IntentSenderRequest; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.FragmentActivity; import org.openintents.openpgp.IOpenPgpService2; import org.openintents.openpgp.OpenPgpError; import org.openintents.openpgp.util.OpenPgpApi; import org.openintents.openpgp.util.OpenPgpServiceConnection; import org.openintents.openpgp.util.OpenPgpUtils; import java.util.List; import java.util.Objects; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.intercept.IntentCompat; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.settings.Prefs; import io.github.muntashirakon.dialog.SearchableSingleChoiceDialogBuilder; public class OpenPgpKeySelectionDialogFragment extends DialogFragment { public static final String TAG = "OpenPgpKeySelectionDialogFragment"; private String mOpenPgpProvider; private OpenPgpServiceConnection mServiceConnection; private AlertDialog mDialog; private FragmentActivity mActivity; private final ActivityResultLauncher mKeyIdResultLauncher = registerForActivityResult( new ActivityResultContracts.StartIntentSenderForResult(), result -> { if (result.getData() != null) { getUserId(result.getData()); } }); private final ExecutorService mExecutor = Executors.newSingleThreadExecutor(runnable -> { Thread thread = new Thread(runnable); thread.setDaemon(true); return thread; }); @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { mActivity = requireActivity(); mOpenPgpProvider = Prefs.Encryption.getOpenPgpProvider(); List serviceInfoList = OpenPgpUtils.getPgpClientServices(mActivity); CharSequence[] packageLabels = new String[serviceInfoList.size()]; String[] packageNames = new String[serviceInfoList.size()]; ServiceInfo serviceInfo; PackageManager pm = mActivity.getPackageManager(); for (int i = 0; i < packageLabels.length; ++i) { serviceInfo = serviceInfoList.get(i); packageLabels[i] = serviceInfo.loadLabel(pm); packageNames[i] = serviceInfo.packageName; } mDialog = new SearchableSingleChoiceDialogBuilder<>(mActivity, packageNames, packageLabels) .setTitle(R.string.open_pgp_provider) .setSelection(mOpenPgpProvider) .setNegativeButton(R.string.cancel, null) .setPositiveButton(R.string.save, (dialog1, which, selectedItem) -> { if (selectedItem != null) { mOpenPgpProvider = selectedItem; Prefs.Encryption.setOpenPgpProvider(mOpenPgpProvider); } }) .create(); mDialog.setOnShowListener(dialog1 -> { Button positiveButton = ((AlertDialog) dialog1).getButton(AlertDialog.BUTTON_POSITIVE); positiveButton.setOnClickListener(v -> chooseKey()); }); return mDialog; } private void chooseKey() { // Bind to service mServiceConnection = new OpenPgpServiceConnection(requireContext(), mOpenPgpProvider, new OpenPgpServiceConnection.OnBound() { @Override public void onBound(IOpenPgpService2 service) { getUserId(new Intent()); } @Override public void onError(Exception e) { Log.e(OpenPgpApi.TAG, "exception on binding!", e); } } ); mServiceConnection.bindToService(); } private void getUserId(@NonNull Intent data) { data.setAction(OpenPgpApi.ACTION_GET_KEY_IDS); data.putExtra(OpenPgpApi.EXTRA_USER_IDS, new String[]{}); OpenPgpApi api = new OpenPgpApi(mActivity, mServiceConnection.getService()); api.executeApiAsync(mExecutor, data, null, null, result -> { switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { case OpenPgpApi.RESULT_CODE_SUCCESS: { long[] keyIds = result.getLongArrayExtra(OpenPgpApi.EXTRA_KEY_IDS); if (keyIds == null || keyIds.length == 0) { // Remove encryption Prefs.Encryption.setOpenPgpProvider(""); Prefs.Encryption.setOpenPgpKeyIds(""); } else { String[] keyIdsStr = new String[keyIds.length]; for (int i = 0; i < keyIds.length; ++i) { keyIdsStr[i] = String.valueOf(keyIds[i]); } Prefs.Encryption.setOpenPgpKeyIds(TextUtils.join(",", keyIdsStr)); } mDialog.dismiss(); break; } case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: { PendingIntent pi = Objects.requireNonNull(IntentCompat.getParcelableExtra(result, OpenPgpApi.RESULT_INTENT, PendingIntent.class)); mKeyIdResultLauncher.launch(new IntentSenderRequest.Builder(pi).build()); break; } case OpenPgpApi.RESULT_CODE_ERROR: { OpenPgpError error = IntentCompat.getParcelableExtra(result, OpenPgpApi.RESULT_ERROR, OpenPgpError.class); if (error != null) { Log.e(OpenPgpApi.TAG, "RESULT_CODE_ERROR: %s", error.getMessage()); } mDialog.dismiss(); break; } } }); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/settings/crypto/RSACryptoSelectionDialogFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.settings.crypto; import android.app.Application; import android.app.Dialog; import android.os.Bundle; import android.widget.Button; import androidx.annotation.AnyThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.annotation.WorkerThread; import androidx.appcompat.app.AlertDialog; import androidx.core.util.Pair; import androidx.fragment.app.DialogFragment; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModelProvider; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; import java.util.Objects; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.backup.CryptoUtils; import io.github.muntashirakon.AppManager.crypto.ks.KeyPair; import io.github.muntashirakon.AppManager.crypto.ks.KeyStoreManager; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.utils.PackageUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.dialog.ScrollableDialogBuilder; public class RSACryptoSelectionDialogFragment extends DialogFragment { public static final String TAG = RSACryptoSelectionDialogFragment.class.getSimpleName(); private static final String EXTRA_ALIAS = "alias"; @NonNull public static RSACryptoSelectionDialogFragment getInstance(@NonNull String alias) { RSACryptoSelectionDialogFragment fragment = new RSACryptoSelectionDialogFragment(); Bundle args = new Bundle(); args.putString(EXTRA_ALIAS, alias); fragment.setArguments(args); return fragment; } public interface OnKeyPairUpdatedListener { @UiThread void keyPairUpdated(@Nullable KeyPair keyPair, @Nullable byte[] certificateBytes); } @Nullable ScrollableDialogBuilder mBuilder; @Nullable private OnKeyPairUpdatedListener mListener; private String mTargetAlias; @Nullable private RSACryptoSelectionViewModel mModel; public void setOnKeyPairUpdatedListener(OnKeyPairUpdatedListener listener) { mListener = listener; } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); mModel = new ViewModelProvider(this).get(RSACryptoSelectionViewModel.class); mModel.observeStatus().observe(this, status -> { if (status.second /* long toast */) { UIUtils.displayLongToast(status.first); } else { UIUtils.displayShortToast(status.first); } }); mModel.observeKeyUpdated().observe(this, updatedKeyPair -> { if (mListener == null) return; mListener.keyPairUpdated(updatedKeyPair.first, updatedKeyPair.second); }); mModel.observeSigningInfo().observe(this, keyPair -> { if (mBuilder != null) mBuilder.setMessage(getSigningInfo(keyPair)); }); } @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { mTargetAlias = requireArguments().getString(EXTRA_ALIAS); mBuilder = new ScrollableDialogBuilder(requireActivity()) .setTitle(R.string.rsa) .setNegativeButton(R.string.pref_import, null) .setNeutralButton(R.string.generate_key, null) .setPositiveButton(R.string.ok, null); Objects.requireNonNull(mModel).loadSigningInfo(mTargetAlias); AlertDialog dialog = Objects.requireNonNull(mBuilder).create(); dialog.setOnShowListener(dialog3 -> { AlertDialog dialog1 = (AlertDialog) dialog3; Button importButton = dialog1.getButton(AlertDialog.BUTTON_NEGATIVE); Button generateButton = dialog1.getButton(AlertDialog.BUTTON_NEUTRAL); importButton.setOnClickListener(v -> { KeyPairImporterDialogFragment fragment = new KeyPairImporterDialogFragment(); Bundle args = new Bundle(); args.putString(KeyPairImporterDialogFragment.EXTRA_ALIAS, mTargetAlias); fragment.setArguments(args); fragment.setOnKeySelectedListener(keyPair -> mModel.addKeyPair(mTargetAlias, keyPair)); fragment.show(getParentFragmentManager(), KeyPairImporterDialogFragment.TAG); }); generateButton.setOnClickListener(v -> { KeyPairGeneratorDialogFragment fragment = new KeyPairGeneratorDialogFragment(); Bundle args = new Bundle(); args.putString(KeyPairGeneratorDialogFragment.EXTRA_KEY_TYPE, CryptoUtils.MODE_RSA); fragment.setArguments(args); fragment.setOnGenerateListener(keyPair -> mModel.addKeyPair(mTargetAlias, keyPair)); fragment.show(getParentFragmentManager(), KeyPairGeneratorDialogFragment.TAG); }); }); return dialog; } private CharSequence getSigningInfo(@Nullable KeyPair keyPair) { if (keyPair != null) { try { return PackageUtils.getSigningCertificateInfo(requireActivity(), (X509Certificate) keyPair.getCertificate()); } catch (CertificateEncodingException e) { return getString(R.string.failed_to_load_key); } } return getString(R.string.key_not_set); } public static class RSACryptoSelectionViewModel extends AndroidViewModel { // StringRes, isLongToast private final MutableLiveData> status = new MutableLiveData<>(); private final MutableLiveData> keyUpdated = new MutableLiveData<>(); private final MutableLiveData signingInfo = new MutableLiveData<>(); public RSACryptoSelectionViewModel(@NonNull Application application) { super(application); } public LiveData> observeStatus() { return status; } public LiveData> observeKeyUpdated() { return keyUpdated; } public LiveData observeSigningInfo() { return signingInfo; } @AnyThread public void loadSigningInfo(String targetAlias) { ThreadUtils.postOnBackgroundThread(() -> signingInfo.postValue(getKeyPair(targetAlias))); } @AnyThread private void addKeyPair(String targetAlias, @Nullable KeyPair keyPair) { ThreadUtils.postOnBackgroundThread(() -> { try { if (keyPair == null) { throw new Exception("Keypair can't be null."); } KeyStoreManager keyStoreManager = KeyStoreManager.getInstance(); keyStoreManager.addKeyPair(targetAlias, keyPair, true); status.postValue(new Pair<>(R.string.done, false)); keyPairUpdated(targetAlias); signingInfo.postValue(getKeyPair(targetAlias)); } catch (Exception e) { Log.e(TAG, e); status.postValue(new Pair<>(R.string.failed_to_save_key, true)); } }); } @WorkerThread @Nullable private KeyPair getKeyPair(String targetAlias) { try { KeyStoreManager keyStoreManager = KeyStoreManager.getInstance(); if (keyStoreManager.containsKey(targetAlias)) { return keyStoreManager.getKeyPair(targetAlias); } } catch (Exception e) { Log.e(TAG, e); } return null; } @WorkerThread private void keyPairUpdated(String targetAlias) { try { KeyPair keyPair = getKeyPair(targetAlias); if (keyPair != null) { byte[] bytes = keyPair.getCertificate().getEncoded(); keyUpdated.postValue(new Pair<>(keyPair, bytes)); return; } } catch (Exception e) { Log.e(TAG, e); } keyUpdated.postValue(new Pair<>(null, null)); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/sharedpref/EditPrefItemFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.sharedpref; import android.app.Dialog; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.TextView; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.os.BundleCompat; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.FragmentActivity; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.materialswitch.MaterialSwitch; import com.google.android.material.textfield.TextInputEditText; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Set; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.adapters.SelectedArrayAdapter; import io.github.muntashirakon.widget.MaterialSpinner; public class EditPrefItemFragment extends DialogFragment { public static final String TAG = EditPrefItemFragment.class.getSimpleName(); public static final String ARG_PREF_ITEM = "ARG_PREF_ITEM"; public static final String ARG_MODE = "ARG_MODE"; @IntDef(value = { MODE_EDIT, MODE_CREATE, MODE_DELETE }) @Retention(RetentionPolicy.SOURCE) public @interface Mode { } public static final int MODE_EDIT = 1; // Key name is disabled public static final int MODE_CREATE = 2; // Key name is not disabled public static final int MODE_DELETE = 3; @IntDef(value = { TYPE_BOOLEAN, TYPE_FLOAT, TYPE_INTEGER, TYPE_LONG, TYPE_STRING, TYPE_SET }) @Retention(RetentionPolicy.SOURCE) public @interface Type { } private static final int TYPE_BOOLEAN = 0; private static final int TYPE_FLOAT = 1; private static final int TYPE_INTEGER = 2; private static final int TYPE_LONG = 3; private static final int TYPE_STRING = 4; private static final int TYPE_SET = 5; private InterfaceCommunicator mInterfaceCommunicator; public interface InterfaceCommunicator { void sendInfo(@Mode int mode, PrefItem prefItem); } public static class PrefItem implements Parcelable { public String keyName; public Object keyValue; public PrefItem() { } protected PrefItem(@NonNull Parcel in) { keyName = in.readString(); } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeString(keyName); } @Override public int describeContents() { return 0; } public static final Creator CREATOR = new Creator() { @Override public PrefItem createFromParcel(Parcel in) { return new PrefItem(in); } @Override public PrefItem[] newArray(int size) { return new PrefItem[size]; } }; } private final ViewGroup[] mLayoutTypes = new ViewGroup[6]; private final TextView[] mValues = new TextView[6]; @Type private int mCurrentType; @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { FragmentActivity activity = requireActivity(); Bundle args = requireArguments(); PrefItem prefItem = BundleCompat.getParcelable(args, ARG_PREF_ITEM, PrefItem.class); @Mode int mode = args.getInt(ARG_MODE); View view = View.inflate(activity, R.layout.dialog_edit_pref_item, null); MaterialSpinner spinner = view.findViewById(R.id.type_selector_spinner); ArrayAdapter spinnerAdapter = SelectedArrayAdapter.createFromResource(activity, R.array.shared_pref_types, io.github.muntashirakon.ui.R.layout.auto_complete_dropdown_item); spinner.setAdapter(spinnerAdapter); spinner.setOnItemClickListener((parent, view1, position, id) -> { for (ViewGroup layout : mLayoutTypes) layout.setVisibility(View.GONE); mLayoutTypes[position].setVisibility(View.VISIBLE); mCurrentType = position; }); // Set layouts mLayoutTypes[TYPE_BOOLEAN] = view.findViewById(R.id.layout_bool); mLayoutTypes[TYPE_FLOAT] = view.findViewById(R.id.layout_float); mLayoutTypes[TYPE_INTEGER] = view.findViewById(R.id.layout_int); mLayoutTypes[TYPE_LONG] = view.findViewById(R.id.layout_long); mLayoutTypes[TYPE_STRING] = view.findViewById(R.id.layout_string); mLayoutTypes[TYPE_SET] = view.findViewById(R.id.layout_string); // Set views mValues[TYPE_BOOLEAN] = view.findViewById(R.id.input_bool); mValues[TYPE_FLOAT] = view.findViewById(R.id.input_float); mValues[TYPE_INTEGER] = view.findViewById(R.id.input_int); mValues[TYPE_LONG] = view.findViewById(R.id.input_long); mValues[TYPE_STRING] = view.findViewById(R.id.input_string); mValues[TYPE_SET] = view.findViewById(R.id.input_string); // Key name TextInputEditText editKeyName = view.findViewById(R.id.key_name); if (prefItem != null) { String keyName = prefItem.keyName; Object keyValue = prefItem.keyValue; editKeyName.setText(keyName); if (mode == MODE_EDIT) { editKeyName.setKeyListener(null); } // Key value if (keyValue instanceof Boolean) { mCurrentType = TYPE_BOOLEAN; mLayoutTypes[TYPE_BOOLEAN].setVisibility(View.VISIBLE); ((MaterialSwitch) mValues[TYPE_BOOLEAN]).setChecked((Boolean) keyValue); spinner.setSelection(TYPE_BOOLEAN); } else if (keyValue instanceof Float) { mCurrentType = TYPE_FLOAT; mLayoutTypes[TYPE_FLOAT].setVisibility(View.VISIBLE); mValues[TYPE_FLOAT].setText(keyValue.toString()); spinner.setSelection(TYPE_FLOAT); } else if (keyValue instanceof Integer) { mCurrentType = TYPE_INTEGER; mLayoutTypes[TYPE_INTEGER].setVisibility(View.VISIBLE); mValues[TYPE_INTEGER].setText(keyValue.toString()); spinner.setSelection(TYPE_INTEGER); } else if (keyValue instanceof Long) { mCurrentType = TYPE_LONG; mLayoutTypes[TYPE_LONG].setVisibility(View.VISIBLE); mValues[TYPE_LONG].setText(keyValue.toString()); spinner.setSelection(TYPE_LONG); } else if (keyValue instanceof String) { mCurrentType = TYPE_STRING; mLayoutTypes[TYPE_STRING].setVisibility(View.VISIBLE); mValues[TYPE_STRING].setText((String) keyValue); spinner.setSelection(TYPE_STRING); } else if (keyValue instanceof Set) { mCurrentType = TYPE_SET; mLayoutTypes[TYPE_SET].setVisibility(View.VISIBLE); //noinspection unchecked mValues[TYPE_SET].setText(SharedPrefsUtil.flattenToString((Set) keyValue)); spinner.setSelection(TYPE_SET); } } mInterfaceCommunicator = (InterfaceCommunicator) activity; MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(activity); builder.setView(view) .setPositiveButton(mode == MODE_CREATE ? R.string.add : R.string.done, (dialog, which) -> { PrefItem newPrefItem; if (prefItem != null) newPrefItem = prefItem; else { newPrefItem = new PrefItem(); newPrefItem.keyName = editKeyName.getText().toString(); } if (newPrefItem.keyName == null) { UIUtils.displayLongToast(R.string.key_name_cannot_be_null); return; } try { switch (mCurrentType) { case TYPE_BOOLEAN: newPrefItem.keyValue = ((MaterialSwitch) mValues[mCurrentType]).isChecked(); break; case TYPE_FLOAT: newPrefItem.keyValue = Float.valueOf(mValues[mCurrentType].getText().toString()); break; case TYPE_INTEGER: newPrefItem.keyValue = Integer.valueOf(mValues[mCurrentType].getText().toString()); break; case TYPE_LONG: newPrefItem.keyValue = Long.valueOf(mValues[mCurrentType].getText().toString()); break; case TYPE_STRING: newPrefItem.keyValue = mValues[mCurrentType].getText().toString(); break; case TYPE_SET: newPrefItem.keyValue = SharedPrefsUtil.unflattenToSet(mValues[mCurrentType].getText().toString()); break; } } catch (Exception e) { e.printStackTrace(); UIUtils.displayLongToast(R.string.error_evaluating_input); return; } mInterfaceCommunicator.sendInfo(mode, newPrefItem); }) .setNegativeButton(R.string.cancel, (dialog, which) -> { if (getDialog() != null) getDialog().cancel(); }); if (mode == MODE_EDIT) builder.setNeutralButton(R.string.delete, (dialog, which) -> mInterfaceCommunicator.sendInfo(MODE_DELETE, prefItem)); return builder.create(); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/sharedpref/SharedPrefsActivity.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.sharedpref; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.Filter; import android.widget.Filterable; import android.widget.TextView; import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.SearchView; import androidx.fragment.app.DialogFragment; import androidx.lifecycle.ViewModelProvider; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.progressindicator.LinearProgressIndicator; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; import io.github.muntashirakon.AppManager.BaseActivity; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.intercept.IntentCompat; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.AppManager.utils.appearance.ColorCodes; import io.github.muntashirakon.io.Paths; import io.github.muntashirakon.util.AdapterUtils; import io.github.muntashirakon.util.UiUtils; import io.github.muntashirakon.widget.RecyclerView; public class SharedPrefsActivity extends BaseActivity implements SearchView.OnQueryTextListener, EditPrefItemFragment.InterfaceCommunicator { public static final String EXTRA_PREF_LOCATION = "loc"; public static final String EXTRA_PREF_LABEL = "label"; // Optional public static final int REASONABLE_STR_SIZE = 200; private SharedPrefsListingAdapter mAdapter; private LinearProgressIndicator mProgressIndicator; private SharedPrefsViewModel mViewModel; private boolean mWriteAndExit = false; private final OnBackPressedCallback mOnBackPressedCallback = new OnBackPressedCallback(false) { @Override public void handleOnBackPressed() { if (mViewModel.isModified()) { new MaterialAlertDialogBuilder(SharedPrefsActivity.this) .setTitle(R.string.exit_confirmation) .setMessage(R.string.file_modified_are_you_sure) .setCancelable(false) .setPositiveButton(R.string.no, null) .setNegativeButton(R.string.yes, (dialog, which) -> { setEnabled(false); getOnBackPressedDispatcher().onBackPressed(); }) .setNeutralButton(R.string.save_and_exit, (dialog, which) -> { mWriteAndExit = true; mViewModel.writeSharedPrefs(); setEnabled(false); }) .show(); return; } setEnabled(false); getOnBackPressedDispatcher().onBackPressed(); } }; @Override protected void onAuthenticated(Bundle savedInstanceState) { setContentView(R.layout.activity_shared_prefs); setSupportActionBar(findViewById(R.id.toolbar)); getOnBackPressedDispatcher().addCallback(this, mOnBackPressedCallback); Uri sharedPrefUri = IntentCompat.getParcelableExtra(getIntent(), EXTRA_PREF_LOCATION, Uri.class); String appLabel = getIntent().getStringExtra(EXTRA_PREF_LABEL); if (sharedPrefUri == null) { finish(); return; } mViewModel = new ViewModelProvider(this).get(SharedPrefsViewModel.class); mViewModel.setSharedPrefsFile(Paths.get(sharedPrefUri)); ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setTitle(appLabel); actionBar.setSubtitle(mViewModel.getSharedPrefFilename()); actionBar.setDisplayShowCustomEnabled(true); UIUtils.setupSearchView(actionBar, this); } mProgressIndicator = findViewById(R.id.progress_linear); mProgressIndicator.setVisibilityAfterHide(View.GONE); mProgressIndicator.show(); RecyclerView recyclerView = findViewById(android.R.id.list); recyclerView.setLayoutManager(UIUtils.getGridLayoutAt450Dp(this)); recyclerView.setEmptyView(findViewById(android.R.id.empty)); mAdapter = new SharedPrefsListingAdapter(this); recyclerView.setAdapter(mAdapter); FloatingActionButton fab = findViewById(R.id.floatingActionButton); UiUtils.applyWindowInsetsAsMargin(fab); fab.setOnClickListener(v -> { DialogFragment dialogFragment = new EditPrefItemFragment(); Bundle args = new Bundle(); args.putInt(EditPrefItemFragment.ARG_MODE, EditPrefItemFragment.MODE_CREATE); dialogFragment.setArguments(args); dialogFragment.show(getSupportFragmentManager(), EditPrefItemFragment.TAG); }); mViewModel.getSharedPrefsMapLiveData().observe(this, sharedPrefsMap -> { mProgressIndicator.hide(); mAdapter.setDefaultList(sharedPrefsMap); }); mViewModel.getSharedPrefsSavedLiveData().observe(this, saved -> { if (saved) { UIUtils.displayShortToast(R.string.saved_successfully); if (mWriteAndExit) { getOnBackPressedDispatcher().onBackPressed(); mWriteAndExit = false; } } else { UIUtils.displayShortToast(R.string.saving_failed); } }); mViewModel.getSharedPrefsDeletedLiveData().observe(this, deleted -> { if (deleted) { UIUtils.displayShortToast(R.string.deleted_successfully); finish(); } else { UIUtils.displayShortToast(R.string.deletion_failed); } }); mViewModel.getSharedPrefsModifiedLiveData().observe(this, modified -> { mOnBackPressedCallback.setEnabled(modified); if (modified) { if (actionBar != null) { actionBar.setTitle("* " + mViewModel.getSharedPrefFilename()); } } else { if (actionBar != null) { actionBar.setTitle(mViewModel.getSharedPrefFilename()); } } }); mViewModel.loadSharedPrefs(); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.activity_shared_prefs_actions, menu); return super.onCreateOptionsMenu(menu); } @Override public void sendInfo(@EditPrefItemFragment.Mode int mode, EditPrefItemFragment.PrefItem prefItem) { if (prefItem != null) { switch (mode) { case EditPrefItemFragment.MODE_CREATE: case EditPrefItemFragment.MODE_EDIT: mViewModel.add(prefItem.keyName, prefItem.keyValue); break; case EditPrefItemFragment.MODE_DELETE: mViewModel.remove(prefItem.keyName); break; } } } @Override public boolean onPrepareOptionsMenu(Menu menu) { MenuItem newWindow = menu.findItem(R.id.action_separate_window); if (newWindow != null) { newWindow.setEnabled(!mViewModel.isModified()); } return super.onPrepareOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int id = item.getItemId(); if (id == android.R.id.home) { getOnBackPressedDispatcher().onBackPressed(); } else if (id == R.id.action_discard) { finish(); } else if (id == R.id.action_delete) { mViewModel.deleteSharedPrefFile(); } else if (id == R.id.action_save) { mViewModel.writeSharedPrefs(); } else if (id == R.id.action_separate_window) { if (!mViewModel.isModified()) { Intent intent = new Intent(getIntent()); intent.setClass(this, SharedPrefsActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); startActivity(intent); finish(); } } else return super.onOptionsItemSelected(item); return true; } @Override protected void onResume() { super.onResume(); if (mAdapter != null && !TextUtils.isEmpty(mAdapter.mConstraint)) { mAdapter.getFilter().filter(mAdapter.mConstraint); } } @Override public boolean onQueryTextSubmit(String query) { return false; } @Override public boolean onQueryTextChange(String newText) { if (mAdapter != null) mAdapter.getFilter().filter(newText.toLowerCase(Locale.ROOT)); return true; } private void displayEditor(@NonNull String prefName) { EditPrefItemFragment.PrefItem prefItem = new EditPrefItemFragment.PrefItem(); prefItem.keyName = prefName; prefItem.keyValue = mViewModel.getValue(prefName); EditPrefItemFragment dialogFragment = new EditPrefItemFragment(); Bundle args = new Bundle(); args.putParcelable(EditPrefItemFragment.ARG_PREF_ITEM, prefItem); args.putInt(EditPrefItemFragment.ARG_MODE, EditPrefItemFragment.MODE_EDIT); dialogFragment.setArguments(args); dialogFragment.show(getSupportFragmentManager(), EditPrefItemFragment.TAG); } static class SharedPrefsListingAdapter extends RecyclerView.Adapter implements Filterable { private final SharedPrefsActivity mActivity; private Filter mFilter; private String mConstraint; private String[] mDefaultList; private String[] mAdapterList; private Map mAdapterMap; private final int mQueryStringHighlightColor; static class ViewHolder extends RecyclerView.ViewHolder { TextView itemName; TextView itemValue; public ViewHolder(@NonNull View itemView) { super(itemView); itemName = itemView.findViewById(android.R.id.title); itemValue = itemView.findViewById(android.R.id.summary); itemView.findViewById(R.id.icon_frame).setVisibility(View.GONE); } } SharedPrefsListingAdapter(@NonNull SharedPrefsActivity activity) { mActivity = activity; mQueryStringHighlightColor = ColorCodes.getQueryStringHighlightColor(activity); } void setDefaultList(@NonNull Map list) { mDefaultList = list.keySet().toArray(new String[0]); mAdapterMap = list; if (!TextUtils.isEmpty(mConstraint)) { getFilter().filter(mConstraint); } else { int previousCount = mAdapterList != null ? mAdapterList.length : 0; mAdapterList = mDefaultList; AdapterUtils.notifyDataSetChanged(this, previousCount, mAdapterList.length); } } @Override public int getItemCount() { return mAdapterList == null ? 0 : mAdapterList.length; } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View v = LayoutInflater.from(parent.getContext()).inflate(io.github.muntashirakon.ui.R.layout.m3_preference, parent, false); return new ViewHolder(v); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { String prefName = mAdapterList[position]; if (mConstraint != null && prefName.toLowerCase(Locale.ROOT).contains(mConstraint)) { // Highlight searched query holder.itemName.setText(UIUtils.getHighlightedText(prefName, mConstraint, mQueryStringHighlightColor)); } else { holder.itemName.setText(prefName); } Object value = mAdapterMap.get(prefName); String strValue = (value != null) ? value.toString() : ""; holder.itemValue.setText(strValue.length() > REASONABLE_STR_SIZE ? strValue.substring(0, REASONABLE_STR_SIZE) : strValue); holder.itemView.setOnClickListener(v -> mActivity.displayEditor(prefName)); } @Override public long getItemId(int position) { return position; } @Override public Filter getFilter() { if (mFilter == null) mFilter = new Filter() { @Override protected FilterResults performFiltering(CharSequence charSequence) { String constraint = charSequence.toString().toLowerCase(Locale.ROOT); mConstraint = constraint; FilterResults filterResults = new FilterResults(); if (constraint.isEmpty()) { filterResults.count = 0; filterResults.values = null; return filterResults; } List list = new ArrayList<>(mDefaultList.length); for (String item : mDefaultList) { if (item.toLowerCase(Locale.ROOT).contains(constraint)) list.add(item); } filterResults.count = list.size(); filterResults.values = list.toArray(new String[0]); return filterResults; } @Override protected void publishResults(CharSequence charSequence, FilterResults filterResults) { int previousCount = mAdapterList != null ? mAdapterList.length : 0; if (filterResults.values == null) { mAdapterList = mDefaultList; } else { mAdapterList = (String[]) filterResults.values; } AdapterUtils.notifyDataSetChanged(SharedPrefsListingAdapter.this, previousCount, mAdapterList.length); } }; return mFilter; } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/sharedpref/SharedPrefsUtil.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.sharedpref; import android.text.TextUtils; import android.util.Xml; import androidx.annotation.NonNull; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlSerializer; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.StringWriter; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; public final class SharedPrefsUtil { public static final String TAG_ROOT = "map"; // public static final String TAG_BOOLEAN = "boolean"; // public static final String TAG_FLOAT = "float"; // public static final String TAG_INTEGER = "int"; // public static final String TAG_LONG = "long"; // public static final String TAG_STRING = "string"; // public static final String TAG_SET = "set"; // @NonNull public static HashMap readSharedPref(@NonNull InputStream is) throws XmlPullParserException, IOException { HashMap prefs = new HashMap<>(); XmlPullParser parser = Xml.newPullParser(); parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false); parser.setInput(is, null); parser.nextTag(); parser.require(XmlPullParser.START_TAG, null, TAG_ROOT); int event = parser.next(); while (event != XmlPullParser.END_DOCUMENT) { if (event == XmlPullParser.START_TAG) { String tagName = parser.getName(); String attrName = parser.getAttributeValue(null, "name"); if (attrName == null) attrName = ""; String attrValue = parser.getAttributeValue(null, "value"); switch (tagName) { case TAG_BOOLEAN: prefs.put(attrName, Objects.equals(attrValue, "true")); break; case TAG_FLOAT: if (attrValue != null) { prefs.put(attrName, Float.valueOf(attrValue)); } break; case TAG_INTEGER: if (attrValue != null) { prefs.put(attrName, Integer.valueOf(attrValue)); } break; case TAG_LONG: if (attrValue != null) { prefs.put(attrName, Long.valueOf(attrValue)); } break; case TAG_STRING: prefs.put(attrName, parser.nextText()); break; case TAG_SET: Set stringSet = new HashSet<>(); prefs.put(attrName, stringSet); // Grab all strings event = parser.next(); tagName = parser.getName(); while (event != XmlPullParser.END_TAG || !Objects.equals(tagName, TAG_SET)) { if (event == XmlPullParser.START_TAG) { if (!Objects.equals(tagName, TAG_STRING)) { throw new XmlPullParserException("Invalid tag inside : " + tagName); } stringSet.add(parser.nextText()); } event = parser.next(); tagName = parser.getName(); } break; default: throw new XmlPullParserException("Invalid tag: " + tagName); } } event = parser.next(); } return prefs; } public static void writeSharedPref(@NonNull OutputStream os, @NonNull Map hashMap) throws IOException { XmlSerializer xmlSerializer = Xml.newSerializer(); StringWriter stringWriter = new StringWriter(); xmlSerializer.setOutput(stringWriter); xmlSerializer.startDocument("UTF-8", true); xmlSerializer.startTag("", TAG_ROOT); // Add values for (String name : hashMap.keySet()) { Object value = hashMap.get(name); if (value instanceof Boolean) { xmlSerializer.startTag("", TAG_BOOLEAN); xmlSerializer.attribute("", "name", name); xmlSerializer.attribute("", "value", value.toString()); xmlSerializer.endTag("", TAG_BOOLEAN); } else if (value instanceof Float) { xmlSerializer.startTag("", TAG_FLOAT); xmlSerializer.attribute("", "name", name); xmlSerializer.attribute("", "value", value.toString()); xmlSerializer.endTag("", TAG_FLOAT); } else if (value instanceof Integer) { xmlSerializer.startTag("", TAG_INTEGER); xmlSerializer.attribute("", "name", name); xmlSerializer.attribute("", "value", value.toString()); xmlSerializer.endTag("", TAG_INTEGER); } else if (value instanceof Long) { xmlSerializer.startTag("", TAG_LONG); xmlSerializer.attribute("", "name", name); xmlSerializer.attribute("", "value", value.toString()); xmlSerializer.endTag("", TAG_LONG); } else if (value instanceof String) { xmlSerializer.startTag("", TAG_STRING); xmlSerializer.attribute("", "name", name); xmlSerializer.text(value.toString()); xmlSerializer.endTag("", TAG_STRING); } else if (value instanceof Set) { xmlSerializer.startTag("", TAG_SET); xmlSerializer.attribute("", "name", name); //noinspection unchecked for (String v : (Set) value) { xmlSerializer.startTag("", TAG_STRING); xmlSerializer.text(v); xmlSerializer.endTag("", TAG_STRING); } xmlSerializer.endTag("", TAG_SET); } else { throw new IOException("Invalid value for key: " + name + " (value: " + value + ")"); } } xmlSerializer.endTag("", TAG_ROOT); xmlSerializer.endDocument(); xmlSerializer.flush(); os.write(stringWriter.toString().getBytes()); } @NonNull public static String flattenToString(@NonNull Set stringSet) { List stringList = new ArrayList<>(stringSet.size()); for (String string : stringSet) { stringList.add(string.replace(",", "\\,")); } return TextUtils.join(",", stringList); } @NonNull public static Set unflattenToSet(@NonNull String rawValue) { // Split on commas unless they are preceded by an escape. // The escape character must be escaped for the string and // again for the regex, thus four escape characters become one. String[] strings = rawValue.split("(? stringSet = new HashSet<>(strings.length); Collections.addAll(stringSet, strings); return stringSet; } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/sharedpref/SharedPrefsViewModel.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.sharedpref; import android.app.Application; import androidx.annotation.AnyThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.HashMap; import java.util.Map; import io.github.muntashirakon.AppManager.utils.MultithreadedExecutor; import io.github.muntashirakon.io.Path; public class SharedPrefsViewModel extends AndroidViewModel { private final MultithreadedExecutor mExecutor = MultithreadedExecutor.getNewInstance(); private final MutableLiveData> mSharedPrefsMapLiveData = new MutableLiveData<>(); private final MutableLiveData mSharedPrefsSavedLiveData = new MutableLiveData<>(); private final MutableLiveData mSharedPrefsDeletedLiveData = new MutableLiveData<>(); private final MutableLiveData mSharedPrefsModifiedLiveData = new MutableLiveData<>(); // TODO: 8/2/22 Use AtomicExtendedFile to better handle errors private Path mSharedPrefsFile; private Map mSharedPrefsMap; private boolean mModified; public SharedPrefsViewModel(@NonNull Application application) { super(application); } @Override protected void onCleared() { mExecutor.shutdownNow(); super.onCleared(); } public void setSharedPrefsFile(@NonNull Path sharedPrefFile) { mSharedPrefsFile = sharedPrefFile; } public boolean isModified() { return mModified; } @Nullable public String getSharedPrefFilename() { if (mSharedPrefsFile != null) { return mSharedPrefsFile.getName(); } return null; } @Nullable public Object getValue(@NonNull String key) { return mSharedPrefsMap.get(key); } public void remove(@NonNull String key) { mSharedPrefsModifiedLiveData.postValue(mModified = true); mSharedPrefsMap.remove(key); mSharedPrefsMapLiveData.postValue(mSharedPrefsMap); } public void add(@NonNull String key, @NonNull Object value) { mSharedPrefsModifiedLiveData.postValue(mModified = true); mSharedPrefsMap.put(key, value); mSharedPrefsMapLiveData.postValue(mSharedPrefsMap); } public LiveData> getSharedPrefsMapLiveData() { return mSharedPrefsMapLiveData; } public LiveData getSharedPrefsSavedLiveData() { return mSharedPrefsSavedLiveData; } public LiveData getSharedPrefsDeletedLiveData() { return mSharedPrefsDeletedLiveData; } public LiveData getSharedPrefsModifiedLiveData() { return mSharedPrefsModifiedLiveData; } @AnyThread public void deleteSharedPrefFile() { mExecutor.submit(() -> mSharedPrefsDeletedLiveData.postValue(mSharedPrefsFile.delete())); } @AnyThread public void writeSharedPrefs() { mExecutor.submit(() -> { try (OutputStream xmlFile = mSharedPrefsFile.openOutputStream()) { SharedPrefsUtil.writeSharedPref(xmlFile, mSharedPrefsMap); // TODO: 9/7/21 Investigate the state of permission (should be unchanged) mSharedPrefsSavedLiveData.postValue(true); mSharedPrefsModifiedLiveData.postValue(mModified = false); } catch (IOException e) { e.printStackTrace(); mSharedPrefsSavedLiveData.postValue(false); } }); } @AnyThread public void loadSharedPrefs() { mExecutor.submit(() -> { try (InputStream rulesStream = mSharedPrefsFile.openInputStream()) { mSharedPrefsModifiedLiveData.postValue(mModified = false); mSharedPrefsMap = SharedPrefsUtil.readSharedPref(rulesStream); mSharedPrefsMapLiveData.postValue(mSharedPrefsMap); } catch (IOException | XmlPullParserException e) { e.printStackTrace(); mSharedPrefsMap = new HashMap<>(); mSharedPrefsMapLiveData.postValue(mSharedPrefsMap); } }); } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/shortcut/CreateShortcutDialogFragment.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.shortcut; import android.app.Dialog; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.pm.ShortcutInfoCompat; import androidx.core.content.pm.ShortcutManagerCompat; import androidx.core.graphics.drawable.IconCompat; import androidx.core.os.BundleCompat; import androidx.fragment.app.DialogFragment; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.imageview.ShapeableImageView; import com.google.android.material.textfield.TextInputEditText; import com.google.android.material.textfield.TextInputLayout; import com.google.android.material.textview.MaterialTextView; import java.lang.ref.WeakReference; import java.util.Objects; import java.util.UUID; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.details.IconPickerDialogFragment; import io.github.muntashirakon.AppManager.utils.ResourceUtil; import io.github.muntashirakon.AppManager.utils.UIUtils; import io.github.muntashirakon.lifecycle.SoftInputLifeCycleObserver; import io.github.muntashirakon.view.TextInputLayoutCompat; public class CreateShortcutDialogFragment extends DialogFragment { public static final String TAG = CreateShortcutDialogFragment.class.getSimpleName(); private static final String ARG_SHORTCUT_INFO = "info"; @NonNull public static CreateShortcutDialogFragment getInstance(@NonNull ShortcutInfo shortcutInfo) { CreateShortcutDialogFragment dialog = new CreateShortcutDialogFragment(); Bundle args = new Bundle(); args.putParcelable(ARG_SHORTCUT_INFO, shortcutInfo); dialog.setArguments(args); return dialog; } private boolean mValidName = true; private ShortcutInfo mShortcutInfo; private View mDialogView; private TextInputEditText mShortcutNameField; private TextInputEditText mShortcutIconField; private TextInputLayout mShortcutIconLayout; private ShapeableImageView mShortcutIconPreview; private MaterialTextView mShortcutNamePreview; private PackageManager mPm; @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { mPm = requireActivity().getPackageManager(); mShortcutInfo = Objects.requireNonNull(BundleCompat.getParcelable(requireArguments(), ARG_SHORTCUT_INFO, ShortcutInfo.class)); mDialogView = View.inflate(requireActivity(), R.layout.dialog_create_shortcut, null); mShortcutNameField = mDialogView.findViewById(R.id.shortcut_name); mShortcutIconField = mDialogView.findViewById(R.id.insert_icon); mShortcutIconLayout = TextInputLayoutCompat.fromTextInputEditText(mShortcutIconField); mShortcutIconPreview = mDialogView.findViewById(R.id.icon); mShortcutNamePreview = mDialogView.findViewById(R.id.name); mShortcutNameField.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } @Override public void afterTextChanged(Editable s) { if (!TextUtils.isEmpty(s)) { mValidName = true; mShortcutInfo.setName(s); mShortcutNamePreview.setText(s); } else mValidName = false; } }); mShortcutIconField.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } @Override public void afterTextChanged(Editable s) { Drawable drawable = getDrawable(s.toString()); if (drawable != null) { mShortcutInfo.setIcon(UIUtils.getBitmapFromDrawable(drawable)); mShortcutIconPreview.setImageDrawable(drawable); } } }); mShortcutIconLayout.setEndIconOnClickListener(v -> { IconPickerDialogFragment dialog = new IconPickerDialogFragment(); dialog.attachIconPickerListener(icon -> { mShortcutIconField.setText(icon.name); Drawable drawable = icon.loadIcon(mPm); mShortcutInfo.setIcon(UIUtils.getBitmapFromDrawable(drawable)); mShortcutIconPreview.setImageDrawable(drawable); }); dialog.show(getParentFragmentManager(), IconPickerDialogFragment.TAG); }); mShortcutNameField.setText(mShortcutInfo.getName()); mShortcutNamePreview.setText(mShortcutInfo.getName()); mShortcutIconPreview.setImageBitmap(mShortcutInfo.getIcon()); return new MaterialAlertDialogBuilder(requireActivity()) .setTitle(R.string.create_shortcut) .setView(mDialogView) .setPositiveButton(R.string.ok, (dialog, which) -> { if (mValidName) { requestPinShortcut(mShortcutInfo); } }) .setNegativeButton(R.string.cancel, null) .create(); } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return mDialogView; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { getLifecycle().addObserver(new SoftInputLifeCycleObserver(new WeakReference<>(mShortcutNameField))); } @Nullable private Drawable getDrawable(@Nullable String iconResString) { if (TextUtils.isEmpty(iconResString)) { return null; } try { Drawable drawable = ResourceUtil.getResourceFromName(mPm, iconResString).getDrawable(requireActivity().getTheme()); if (drawable != null) { return drawable; } } catch (PackageManager.NameNotFoundException | Resources.NotFoundException ignore) { } return null; // return mPm.getDefaultActivityIcon(); } private void requestPinShortcut(@NonNull ShortcutInfo shortcutInfo) { Context context = requireContext().getApplicationContext(); CharSequence name = Objects.requireNonNull(shortcutInfo.getName()); String shortcutId = shortcutInfo.getId(); if (shortcutId == null) { shortcutId = UUID.randomUUID().toString(); } Intent shortcutIntent = shortcutInfo.toShortcutIntent(context); // Set action for shortcut shortcutIntent.setAction(Intent.ACTION_CREATE_SHORTCUT); ShortcutInfoCompat shortcutInfoCompat = new ShortcutInfoCompat.Builder(context, shortcutId) // Enforce shortcut name to be a String .setShortLabel(name.toString()) .setLongLabel(name) .setIcon(IconCompat.createWithBitmap(shortcutInfo.getIcon())) .setIntent(shortcutIntent) .build(); boolean shortcutPinned = ShortcutManagerCompat.isRequestPinShortcutSupported(context) && ShortcutManagerCompat.requestPinShortcut(context, shortcutInfoCompat, null); if (!shortcutPinned) { new MaterialAlertDialogBuilder(context) .setTitle(context.getString(R.string.error_creating_shortcut)) .setMessage(context.getString(R.string.error_verbose_pin_shortcut)) .setPositiveButton(context.getString(R.string.ok), (dialog, which) -> dialog.cancel()) .show(); } } } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/shortcut/ShortcutInfo.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.shortcut; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.core.os.ParcelCompat; public abstract class ShortcutInfo implements Parcelable { private String mId; private CharSequence mName; private Bitmap mIcon; public ShortcutInfo() { } protected ShortcutInfo(Parcel in) { mName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); mIcon = ParcelCompat.readParcelable(in, Bitmap.class.getClassLoader(), Bitmap.class); } public String getId() { return mId; } public void setId(String id) { mId = id; } public CharSequence getName() { return mName; } public void setName(CharSequence name) { mName = name; } public Bitmap getIcon() { return mIcon; } public void setIcon(Bitmap icon) { mIcon = icon; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { TextUtils.writeToParcel(mName, dest, flags); dest.writeParcelable(mIcon, flags); } public abstract Intent toShortcutIntent(@NonNull Context context); } ================================================ FILE: app/src/main/java/io/github/muntashirakon/AppManager/ssaid/ChangeSsaidDialog.java ================================================ // SPDX-License-Identifier: GPL-3.0-or-later package io.github.muntashirakon.AppManager.ssaid; import android.app.Dialog; import android.content.DialogInterface; import android.os.Build; import android.os.Bundle; import android.os.UserHandleHidden; import android.text.Editable; import android.text.TextWatcher; import android.view.View; import android.widget.Button; import androidx.annotation.MainThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.FragmentActivity; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.textfield.TextInputEditText; import com.google.android.material.textfield.TextInputLayout; import java.io.IOException; import java.util.Objects; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicReference; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.utils.ThreadUtils; @RequiresApi(Build.VERSION_CODES.O) public class ChangeSsaidDialog extends DialogFragment { public static final String TAG = ChangeSsaidDialog.class.getSimpleName(); @NonNull public static ChangeSsaidDialog getInstance(@NonNull String packageName, int uid, @Nullable String ssaid) { ChangeSsaidDialog dialog = new ChangeSsaidDialog(); Bundle args = new Bundle(); args.putString(ARG_PACKAGE_NAME, packageName); args.putInt(ARG_UID, uid); args.putString(ARG_OPTIONAL_SSAID, ssaid); dialog.setArguments(args); return dialog; } public interface SsaidChangedInterface { @MainThread void onSsaidChanged(String newSsaid, boolean isSuccessful); } public static final String ARG_PACKAGE_NAME = "pkg"; public static final String ARG_UID = "uid"; public static final String ARG_OPTIONAL_SSAID = "ssaid"; private String mSsaid; private String mOldSsaid; @Nullable private SsaidChangedInterface mSsaidChangedInterface; @Nullable private Future mSsaidChangedResult; @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { FragmentActivity activity = requireActivity(); mSsaid = requireArguments().getString(ARG_OPTIONAL_SSAID); mOldSsaid = mSsaid; String packageName = Objects.requireNonNull(requireArguments().getString(ARG_PACKAGE_NAME)); int uid = requireArguments().getInt(ARG_UID); int sizeByte = packageName.equals("android") ? 32 : 8; View view = getLayoutInflater().inflate(R.layout.dialog_ssaid_info, null); AlertDialog alertDialog = new MaterialAlertDialogBuilder(activity) .setTitle(R.string.ssaid) .setView(view) .setPositiveButton(R.string.apply, null) .setNegativeButton(R.string.close, null) .setNeutralButton(R.string.reset_to_default, null) .create(); TextInputEditText ssaidEditText = view.findViewById(android.R.id.text1); TextInputLayout ssaidInputLayout = view.findViewById(R.id.ssaid_layout); AtomicReference