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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/android-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Android Tests

on:
pull_request:
branches: ["develop"]

jobs:
android-test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "17"

- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
cache: true

- name: Create temporary Flutter project
run: |
flutter create --template=app /tmp/test_app
cd /tmp/test_app

# Add the plugin with path dependency
flutter pub add flutter_device_apps_android --path $GITHUB_WORKSPACE

# Build to generate Gradle files
flutter build apk --debug

- name: Run Android unit tests
run: |
cd /tmp/test_app/android
./gradlew :flutter_device_apps_android:testDebugUnitTest
18 changes: 18 additions & 0 deletions .github/workflows/only-develop-to-main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: only-develop-to-main

on:
pull_request:
branches: ["main"]

jobs:
only-develop-to-main:
runs-on: ubuntu-latest
steps:
- name: Allow only develop -> main PRs
run: |
echo "head_ref: ${{ github.head_ref }}"
echo "base_ref: ${{ github.base_ref }}"
if [ "${{ github.base_ref }}" = "main" ] && [ "${{ github.head_ref }}" != "develop" ]; then
echo "Only PRs from 'develop' to 'main' are allowed."
exit 1
fi
30 changes: 30 additions & 0 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Quality

on:
pull_request:
branches: ["develop"]

jobs:
quality:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
cache: true

- name: Install dependencies
run: flutter pub get

- name: Check formatting
run: dart format --set-exit-if-changed .

- name: Static analysis
run: flutter analyze

- name: pub.dev dry-run
run: flutter pub publish --dry-run
24 changes: 24 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Test

on:
pull_request:
branches: ["develop"]

jobs:
test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
cache: true

- name: Install dependencies
run: flutter pub get

- name: Run tests
run: flutter test
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
## 0.6.0
- **BREAKING**: Updated to match platform interface 0.6.0 - `requestedPermissions` removed from `AppInfo`
- Added `getRequestedPermissions(String packageName)` method implementation for on-demand permission retrieval
- Added GitHub Actions workflows for Android unit tests, quality checks, and PR enforcement
- Added comprehensive Dart unit tests (27 tests) for method channel mocking
- Added Kotlin unit tests (11 tests) with Robolectric for Android plugin
- Made `FlutterDeviceAppsAndroidPlugin` class `open` with `protected` fields for testability

## 0.5.1
- Added support for additional `AppInfo` fields from the Android package manager: `category`, `targetSdkVersion`, `minSdkVersion`, `enabled`, `processName`, `installLocation`, `requestedPermissions`.
- Populates `requestedPermissions` via `PackageManager.GET_PERMISSIONS`.
Expand Down
21 changes: 12 additions & 9 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,21 @@ android {
}

dependencies {
testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.mockito:mockito-core:5.0.0")
testImplementation("junit:junit:4.13.2")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
testImplementation("org.mockito:mockito-inline:5.2.0")
testImplementation("org.robolectric:robolectric:4.11.1")
}

testOptions {
unitTests.all {
useJUnitPlatform()

testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
outputs.upToDateWhen {false}
showStandardStreams = true
unitTests {
includeAndroidResources = true
all {
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
outputs.upToDateWhen {false}
showStandardStreams = true
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.*
import java.io.ByteArrayOutputStream

class FlutterDeviceAppsAndroidPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, EventChannel.StreamHandler {
open class FlutterDeviceAppsAndroidPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, EventChannel.StreamHandler {

private lateinit var methodChannel: MethodChannel
private lateinit var eventChannel: EventChannel
private lateinit var appContext: Context
private lateinit var pm: PackageManager
protected lateinit var appContext: Context
protected lateinit var pm: PackageManager

private val mainHandler = Handler(Looper.getMainLooper())
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
Expand Down Expand Up @@ -99,6 +99,35 @@ class FlutterDeviceAppsAndroidPlugin : FlutterPlugin, MethodChannel.MethodCallHa
}
}
}
"getRequestedPermissions" -> {
val pkg = call.argument<String>("packageName")
if (pkg == null) {
result.error("ARG", "packageName required", null)
return
}
scope.launch {
try {
val pInfo: PackageInfo = try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
pm.getPackageInfo(
pkg,
PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS.toLong())
)
} else {
@Suppress("DEPRECATION")
pm.getPackageInfo(pkg, PackageManager.GET_PERMISSIONS)
}
} catch (_: Exception) {
mainHandler.post { result.success(null) }
return@launch
}
val requested = pInfo.requestedPermissions?.toList()
mainHandler.post { result.success(requested) }
} catch (e: Exception) {
mainHandler.post { result.error("ERR_PERMS", e.message, null) }
}
}
}
"openApp" -> {
val pkg = call.argument<String>("packageName")
if (pkg == null) return result.error("ARG", "packageName required", null)
Expand Down Expand Up @@ -248,10 +277,13 @@ class FlutterDeviceAppsAndroidPlugin : FlutterPlugin, MethodChannel.MethodCallHa
private fun getAppMap(packageName: String, includeIcon: Boolean): Map<String, Any?>? {
val pInfo: PackageInfo = try {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
pm.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS.toLong()))
pm.getPackageInfo(
packageName,
PackageManager.PackageInfoFlags.of(0)
)
} else {
@Suppress("DEPRECATION")
pm.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS)
pm.getPackageInfo(packageName, 0)
}
} catch (_: Exception) {
return null
Expand All @@ -260,7 +292,6 @@ class FlutterDeviceAppsAndroidPlugin : FlutterPlugin, MethodChannel.MethodCallHa
val aInfo: ApplicationInfo = pInfo.applicationInfo ?: return null

val category: Int? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) aInfo.category else null
val requestedPermissionsList: List<String>? = pInfo.requestedPermissions?.toList()

val isSystem = (aInfo.flags and ApplicationInfo.FLAG_SYSTEM) != 0
val label = try {
Expand Down Expand Up @@ -300,8 +331,7 @@ class FlutterDeviceAppsAndroidPlugin : FlutterPlugin, MethodChannel.MethodCallHa
"minSdkVersion" to aInfo.minSdkVersion,
"enabled" to aInfo.enabled,
"processName" to aInfo.processName,
"installLocation" to pInfo.installLocation,
"requestedPermissions" to requestedPermissionsList
"installLocation" to pInfo.installLocation
)
}

Expand Down
Loading