diff --git a/android/src/main/kotlin/com/sharmadhiraj/installed_apps/BuiltWithUtil.kt b/android/src/main/kotlin/com/sharmadhiraj/installed_apps/BuiltWithUtil.kt index cb3c31d..226a94f 100644 --- a/android/src/main/kotlin/com/sharmadhiraj/installed_apps/BuiltWithUtil.kt +++ b/android/src/main/kotlin/com/sharmadhiraj/installed_apps/BuiltWithUtil.kt @@ -55,4 +55,4 @@ class BuiltWithUtil { } -} \ No newline at end of file +} diff --git a/android/src/main/kotlin/com/sharmadhiraj/installed_apps/InstalledAppsPlugin.kt b/android/src/main/kotlin/com/sharmadhiraj/installed_apps/InstalledAppsPlugin.kt index 5cf0e6c..0152a8d 100644 --- a/android/src/main/kotlin/com/sharmadhiraj/installed_apps/InstalledAppsPlugin.kt +++ b/android/src/main/kotlin/com/sharmadhiraj/installed_apps/InstalledAppsPlugin.kt @@ -1,12 +1,21 @@ package com.sharmadhiraj.installed_apps +import android.util.Log +import android.provider.Settings +import android.app.ActivityManager +import android.view.accessibility.AccessibilityManager +import android.app.AppOpsManager +import android.os.Build import android.content.Context import android.content.Intent import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.content.pm.PackageManager import android.content.pm.ApplicationInfo import android.net.Uri +import android.app.usage.UsageStatsManager +import android.provider.Settings.ACTION_USAGE_ACCESS_SETTINGS import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS +import android.provider.Settings.ACTION_ACCESSIBILITY_SETTINGS import android.widget.Toast import android.widget.Toast.LENGTH_LONG import android.widget.Toast.LENGTH_SHORT @@ -19,34 +28,22 @@ import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import io.flutter.plugin.common.PluginRegistry.Registrar -import java.util.Locale.ENGLISH - - -class InstalledAppsPlugin() : MethodCallHandler, FlutterPlugin, ActivityAware { - companion object { - - var context: Context? = null +import java.util.Locale.ENGLISH - @JvmStatic - fun registerWith(registrar: Registrar) { - context = registrar.context() - register(registrar.messenger()) - } +class InstalledAppsPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { - @JvmStatic - fun register(messenger: BinaryMessenger) { - val channel = MethodChannel(messenger, "installed_apps") - channel.setMethodCallHandler(InstalledAppsPlugin()) - } - } + private lateinit var channel: MethodChannel + private var context: Context? = null override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { - register(binding.binaryMessenger) + context = binding.applicationContext + channel = MethodChannel(binding.binaryMessenger, "installed_apps") + channel.setMethodCallHandler(this) } override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) } override fun onAttachedToActivity(activityPluginBinding: ActivityPluginBinding) { @@ -68,23 +65,34 @@ class InstalledAppsPlugin() : MethodCallHandler, FlutterPlugin, ActivityAware { } when (call.method) { "getInstalledApps" -> { - val includeSystemApps = call.argument("exclude_system_apps") ?: true - val withIcon = call.argument("with_icon") ?: false - val packageNamePrefix: String = call.argument("package_name_prefix") ?: "" + val includeSystemApps = call.argument("exclude_system_apps") ?: true + val withIcon = call.argument("with_icon") ?: false + val packageNamePrefix = call.argument("package_name_prefix") ?: "" + val platformTypeName = call.argument("platform_type") ?: "" Thread { - val apps: List> = - getInstalledApps(includeSystemApps, withIcon, packageNamePrefix) + val apps: List> = + getInstalledApps(includeSystemApps, withIcon, packageNamePrefix, PlatformType.fromString(platformTypeName)) result.success(apps) }.start() } + "getRunningApps" -> { + val excludeSystemApps = call.argument("exclude_system_apps") ?: true + val withIcon = call.argument("with_icon") ?: false + val platformTypeName: String = call.argument("platform_type") ?: "" + Thread { + val apps = getRunningApps(excludeSystemApps, withIcon, PlatformType.fromString(platformTypeName)) + result.success(apps) + }.start() + } + "startApp" -> { - val packageName: String? = call.argument("package_name") + val packageName = call.argument("package_name") result.success(startApp(packageName)) } "openSettings" -> { - val packageName: String? = call.argument("package_name") + val packageName = call.argument("package_name") openSettings(packageName) } @@ -95,48 +103,183 @@ class InstalledAppsPlugin() : MethodCallHandler, FlutterPlugin, ActivityAware { } "getAppInfo" -> { - val packageName: String = call.argument("package_name") ?: "" - result.success(getAppInfo(getPackageManager(context!!), packageName)) + val packageName = call.argument("package_name") ?: "" + val platformTypeName = call.argument("platform_type") ?: "" + val platformType = PlatformType.fromString(platformTypeName) + result.success(getAppInfo(getPackageManager(context!!), packageName, platformType)) } "isSystemApp" -> { - val packageName: String = call.argument("package_name") ?: "" + val packageName = call.argument("package_name") ?: "" result.success(isSystemApp(getPackageManager(context!!), packageName)) } "uninstallApp" -> { - val packageName: String = call.argument("package_name") ?: "" + val packageName = call.argument("package_name") ?: "" result.success(uninstallApp(packageName)) } "isAppInstalled" -> { - val packageName: String = call.argument("package_name") ?: "" + val packageName = call.argument("package_name") ?: "" result.success(isAppInstalled(packageName)) } + "checkUsageAccessPermission" -> { + val isGranted = isUsageAccessGranted() + result.success(isGranted) + } + + "checkAccessibilityPermission" -> { + result.success(isAccessibilityPermissionGranted()) + } + + "requestAccessibilityPermission" -> { + checkAndRequestAccessibilityPermission() + result.success(null) // İşlem başlatıldığı için geri dönecek bir sonuç yok + } + + "openUsageAccessSettings" -> { + openUsageAccessSettings() + result.success(null) + } + else -> result.notImplemented() } } + + private fun openUsageAccessSettings() { + val intent = Intent().apply { + flags = FLAG_ACTIVITY_NEW_TASK + action = ACTION_USAGE_ACCESS_SETTINGS + } + + context!!.startActivity(intent) + } + + private fun checkAndRequestAccessibilityPermission() { + val intent = Intent().apply { + flags = FLAG_ACTIVITY_NEW_TASK + action = ACTION_ACCESSIBILITY_SETTINGS + } + context!!.startActivity(intent) + } + + private fun isAccessibilityPermissionGranted(): Boolean { + val accessibilityManager = context!!.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager + + val enabledServices = Settings.Secure.getString( + context!!.contentResolver, + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES + ) + val myServiceName = "${context!!.packageName}/net.permission.man.MyAccessibilityService" + + // Kontrol: MyAccessibilityService etkin mi? + return enabledServices?.contains(myServiceName) == true && + accessibilityManager.isEnabled + } + + private fun isUsageAccessGranted(): Boolean { + val appOpsManager = context!!.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager + val mode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + appOpsManager.unsafeCheckOpNoThrow( + AppOpsManager.OPSTR_GET_USAGE_STATS, + android.os.Process.myUid(), + context!!.packageName + ) + } else { + appOpsManager.checkOpNoThrow( + AppOpsManager.OPSTR_GET_USAGE_STATS, + android.os.Process.myUid(), + context!!.packageName + ) + } + return mode == AppOpsManager.MODE_ALLOWED + } + private fun getInstalledApps( excludeSystemApps: Boolean, withIcon: Boolean, - packageNamePrefix: String + packageNamePrefix: String, + platformType: PlatformType?, ): List> { val packageManager = getPackageManager(context!!) - var installedApps = packageManager.getInstalledApplications(0) + var installedApps = packageManager.getInstalledApplications(PackageManager.GET_PERMISSIONS) if (excludeSystemApps) - installedApps = - installedApps.filter { app -> !isSystemApp(packageManager, app.packageName) } + installedApps = installedApps.filter { app -> !isSystemApp(packageManager, app.packageName) } if (packageNamePrefix.isNotEmpty()) installedApps = installedApps.filter { app -> app.packageName.startsWith( packageNamePrefix.lowercase(ENGLISH) ) } - return installedApps.map { app -> convertAppToMap(packageManager, app, withIcon) } + return installedApps.map { app -> convertAppToMap(packageManager, app, withIcon, platformType) } } + private fun getRunningApps( + excludeSystemApps: Boolean, + withIcon: Boolean, + platformType: PlatformType? + ): List> { + val packageManager = getPackageManager(context!!) + val runningApps = mutableListOf>() + + // 1. Kullanıcı İstatistiklerini Kullanma + val usageStatsManager = context!!.getSystemService(Context.USAGE_STATS_SERVICE) as android.app.usage.UsageStatsManager + val currentTime = System.currentTimeMillis() + val endTime = currentTime + val startTime = currentTime - 1000 * 60 * 60 * 1 // Last 1 hour + + val usageStats = usageStatsManager.queryUsageStats( + android.app.usage.UsageStatsManager.INTERVAL_DAILY, + startTime, + endTime + ) + + if (usageStats != null) { + val uniquePackages = mutableSetOf() + + usageStats.forEach { stat -> + val packageName = stat.packageName + if (packageName != null && uniquePackages.add(packageName)) { + try { + val appInfo = packageManager.getApplicationInfo(packageName, 0) + val isSystemApp = (appInfo.flags and ApplicationInfo.FLAG_SYSTEM) != 0 + if (!excludeSystemApps || !isSystemApp) { + val appMap = convertAppToMap(packageManager, appInfo, withIcon, platformType) + runningApps.add(appMap) + } + } catch (e: PackageManager.NameNotFoundException) { + // Paket bulunamadı, devam et + } + } + } + } + + // 2. Çalışan İşlemleri Kullanma (Eski Yöntem) + val activityManager = context!!.getSystemService(Context.ACTIVITY_SERVICE) as android.app.ActivityManager + val runningProcesses = activityManager.runningAppProcesses + runningProcesses?.forEach { processInfo -> + processInfo.pkgList?.forEach { packageName -> + try { + if (!runningApps.any { it["packageName"] == packageName }) { // Eğer daha önce eklenmediyse + val appInfo = packageManager.getApplicationInfo(packageName, 0) + val isSystemApp = (appInfo.flags and ApplicationInfo.FLAG_SYSTEM) != 0 + if (!excludeSystemApps || !isSystemApp) { + val appMap = convertAppToMap(packageManager, appInfo, withIcon, platformType) + runningApps.add(appMap) + } + } + } catch (e: PackageManager.NameNotFoundException) { + // Uygulama bilgisi bulunamadı, devam et + } + } + } + + return runningApps + } + + private fun startApp(packageName: String?): Boolean { if (packageName.isNullOrBlank()) return false return try { @@ -178,12 +321,13 @@ class InstalledAppsPlugin() : MethodCallHandler, FlutterPlugin, ActivityAware { private fun getAppInfo( packageManager: PackageManager, - packageName: String + packageName: String, + platformType: PlatformType? ): Map? { var installedApps = packageManager.getInstalledApplications(0) installedApps = installedApps.filter { app -> app.packageName == packageName } return if (installedApps.isEmpty()) null - else convertAppToMap(packageManager, installedApps[0], true) + else convertAppToMap(packageManager, installedApps[0], true, platformType) } private fun uninstallApp(packageName: String): Boolean { diff --git a/android/src/main/kotlin/com/sharmadhiraj/installed_apps/PlatfromType.kt b/android/src/main/kotlin/com/sharmadhiraj/installed_apps/PlatfromType.kt new file mode 100644 index 0000000..805d1e0 --- /dev/null +++ b/android/src/main/kotlin/com/sharmadhiraj/installed_apps/PlatfromType.kt @@ -0,0 +1,21 @@ +package com.sharmadhiraj.installed_apps +enum class PlatformType(val value: String) { + FLUTTER("flutter"), + REACT_NATIVE("react_native"), + XAMARIN("xamarin"), + IONIC("ionic"), + NATIVE_OR_OTHERS("native_or_others"); + + companion object { + fun fromString(platform: String): PlatformType? { + if (platform.isEmpty()) return null; + return when (platform.lowercase()) { + "flutter" -> FLUTTER + "react_native" -> REACT_NATIVE + "xamarin" -> XAMARIN + "ionic" -> IONIC + else -> NATIVE_OR_OTHERS + } + } + } +} diff --git a/android/src/main/kotlin/com/sharmadhiraj/installed_apps/Util.kt b/android/src/main/kotlin/com/sharmadhiraj/installed_apps/Util.kt index 79a1924..d65b603 100644 --- a/android/src/main/kotlin/com/sharmadhiraj/installed_apps/Util.kt +++ b/android/src/main/kotlin/com/sharmadhiraj/installed_apps/Util.kt @@ -15,7 +15,8 @@ class Util { fun convertAppToMap( packageManager: PackageManager, app: ApplicationInfo, - withIcon: Boolean + withIcon: Boolean, + platformType: PlatformType?, ): HashMap { val map = HashMap() map["name"] = packageManager.getApplicationLabel(app) @@ -23,13 +24,30 @@ class Util { map["icon"] = if (withIcon) DrawableUtil.drawableToByteArray(app.loadIcon(packageManager)) else ByteArray(0) - val packageInfo = packageManager.getPackageInfo(app.packageName, 0) + + val packageInfo = packageManager.getPackageInfo(app.packageName, PackageManager.GET_PERMISSIONS) map["version_name"] = packageInfo.versionName map["version_code"] = getVersionCode(packageInfo) - map["built_with"] = BuiltWithUtil.getPlatform(packageInfo.applicationInfo) + map["built_with"] = platformType?.value ?: BuiltWithUtil.getPlatform(packageInfo.applicationInfo) map["installed_timestamp"] = File(packageInfo.applicationInfo.sourceDir).lastModified() + + if (packageInfo.requestedPermissions != null) { + val permissionsStatus = packageInfo.requestedPermissions.map { permission -> + val isGranted = checkPermissionStatus(packageManager, app.packageName, permission) + mapOf("permission" to permission, "granted" to isGranted) + } + map["permissions"] = permissionsStatus + } else { + map["permissions"] = emptyList>() + } + return map } + + fun checkPermissionStatus(packageManager: PackageManager, packageName: String, permission: String): Boolean { + val permissionCheck = packageManager.checkPermission(permission, packageName) + return permissionCheck == PackageManager.PERMISSION_GRANTED + } fun getPackageManager(context: Context): PackageManager { @@ -44,4 +62,4 @@ class Util { } -} \ No newline at end of file +} diff --git a/lib/app_info.dart b/lib/app_info.dart index 3d729f6..dc39c36 100644 --- a/lib/app_info.dart +++ b/lib/app_info.dart @@ -1,5 +1,12 @@ import 'dart:typed_data'; +class PermissionStatus { + final String permission; + final bool granted; + + PermissionStatus({required this.permission, required this.granted}); +} + class AppInfo { String name; Uint8List? icon; @@ -8,6 +15,7 @@ class AppInfo { int versionCode; BuiltWith builtWith; int installedTimestamp; + List permissions; AppInfo({ required this.name, @@ -17,6 +25,7 @@ class AppInfo { required this.versionCode, required this.builtWith, required this.installedTimestamp, + required this.permissions, }); factory AppInfo.create(dynamic data) { @@ -28,6 +37,12 @@ class AppInfo { versionCode: data["version_code"] ?? 1, builtWith: parseBuiltWith(data["built_with"]), installedTimestamp: data["installed_timestamp"] ?? 0, + permissions: (data["permissions"] as List?) + ?.map((perm) => PermissionStatus( + permission: perm["permission"], + granted: perm["granted"], + )) + .toList() ?? [], ); } diff --git a/lib/installed_apps.dart b/lib/installed_apps.dart index a3d12d2..9ad6161 100644 --- a/lib/installed_apps.dart +++ b/lib/installed_apps.dart @@ -10,12 +10,14 @@ class InstalledApps { /// [excludeSystemApps] specifies whether to exclude system apps from the list. /// [withIcon] specifies whether to include app icons in the list. /// [packageNamePrefix] is an optional parameter to filter apps with package names starting with a specific prefix. + /// [platformType] is an optional parameter to set the app type. Default is [AppPlatformType.flutter]. /// /// Returns a list of [AppInfo] objects representing the installed apps. static Future> getInstalledApps([ bool excludeSystemApps = true, bool withIcon = false, String packageNamePrefix = "", + BuiltWith platformType = BuiltWith.flutter, ]) async { dynamic apps = await _channel.invokeMethod( "getInstalledApps", @@ -23,6 +25,7 @@ class InstalledApps { "exclude_system_apps": excludeSystemApps, "with_icon": withIcon, "package_name_prefix": packageNamePrefix, + "platform_type": platformType.name, }, ); return AppInfo.parseList(apps); @@ -40,6 +43,42 @@ class InstalledApps { ); } + /// isAccessibilityPermissionGranted + static Future isAccessibilityPermissionGranted() async { + return await _channel.invokeMethod('checkAccessibilityPermission') ?? false; + } + + /// requestAccessibilityPermission + static Future requestAccessibilityPermission() async { + await _channel.invokeMethod('requestAccessibilityPermission'); + } + + /// closeBackgroundApps + static Future closeBackgroundApps(List packages) async { + return await _channel.invokeMethod("closeBackgroundApps", { + "package_names": packages, + }) ?? false; + } + + /// Getting running apps + /// [excludeSystemApps] specifies whether to exclude system apps from the list.wld + /// Returns a list of [AppInfo] objects representing the installed apps. + static Future> getRunningApps({ + bool excludeSystemApps = true, + bool withIcon = true, + }) async { + dynamic apps = await _channel.invokeMethod( + "getRunningApps", + { + "exclude_system_apps": excludeSystemApps, + "with_icon": withIcon, + }, + ); + + return AppInfo.parseList(apps); + } + + /// Opens the settings screen (App Info) of an app with the specified package name. /// /// [packageName] is the package name of the app whose settings screen should be opened. @@ -50,6 +89,27 @@ class InstalledApps { ); } + /// Check Usage Access Permission + /// + static Future isUsageAccessGranted() async { + try { + final bool isGranted = + await _channel.invokeMethod('checkUsageAccessPermission'); + return isGranted; + } on PlatformException catch (e) { + print("Error checking Usage Access permission: ${e.message}"); + return false; + } + } + + /// Opens Usage Access Settings + /// + static openUsageAccessSettings() { + _channel.invokeMethod( + "openUsageAccessSettings" + ); + } + /// Displays a toast message on the device. /// /// [message] is the message to display. @@ -69,10 +129,16 @@ class InstalledApps { /// [packageName] is the package name of the app to retrieve information for. /// /// Returns [AppInfo] for the given package name, or null if not found. - static Future getAppInfo(String packageName) async { + static Future getAppInfo( + String packageName, + BuiltWith? platformType, + ) async { var app = await _channel.invokeMethod( "getAppInfo", - {"package_name": packageName}, + { + "package_name": packageName, + "platform_type": platformType?.name ?? '', + }, ); if (app == null) { return null;