Files
pj d856c40d7c feat(permissions): grant dangerous permissions via aapt + adb
Inspector parses uses-permission entries from `aapt dump permissions`
and the granter shells out to `adb shell pm grant`. Both are pluggable
so tests can drive logic without aapt or a device. Granter failures
become warnings rather than errors — Android refuses non-runtime
permissions and we'd rather keep going than abort the run.
2026-04-17 23:57:17 +07:00

126 lines
5.2 KiB
Go

package permissions
import (
"context"
"fmt"
"os/exec"
"regexp"
"strings"
)
// Inspector returns the list of uses-permission entries declared by the APK.
type Inspector func(ctx context.Context, apkPath string) ([]string, error)
// Granter grants a single permission to the given package on the connected
// device. Implementations typically wrap `adb shell pm grant`.
type Granter func(ctx context.Context, packageName, permission string) error
// GrantDangerous filters declared permissions to the dangerous set and grants
// each one via the supplied granter. Errors from individual grants are
// collected as warnings rather than aborting — Android refuses to grant
// non-runtime permissions and we prefer to soldier on.
func GrantDangerous(
ctx context.Context,
apkPath, packageName string,
inspector Inspector,
granter Granter,
) (granted []string, warnings []string, err error) {
declared, err := inspector(ctx, apkPath)
if err != nil {
return nil, nil, fmt.Errorf("inspect permissions: %w", err)
}
for _, permission := range declared {
if !IsDangerous(permission) {
continue
}
if err := granter(ctx, packageName, permission); err != nil {
warnings = append(warnings, fmt.Sprintf("%s: %v", permission, err))
continue
}
granted = append(granted, permission)
}
return granted, warnings, nil
}
var aaptPermissionPattern = regexp.MustCompile(`uses-permission:\s+name='([^']+)'`)
// AaptInspector shells out to `aapt dump permissions` to list permissions.
func AaptInspector(ctx context.Context, apkPath string) ([]string, error) {
output, err := exec.CommandContext(ctx, "aapt", "dump", "permissions", apkPath).Output()
if err != nil {
return nil, fmt.Errorf("aapt dump permissions: %w", err)
}
var permissions []string
for _, match := range aaptPermissionPattern.FindAllStringSubmatch(string(output), -1) {
permissions = append(permissions, match[1])
}
return permissions, nil
}
// AdbGranter returns a Granter that runs `adb shell pm grant` against the
// supplied device serial (empty = default device).
func AdbGranter(adbPath, deviceSerial string) Granter {
return func(ctx context.Context, packageName, permission string) error {
arguments := []string{"shell", "pm", "grant", packageName, permission}
if deviceSerial != "" {
arguments = append([]string{"-s", deviceSerial}, arguments...)
}
command := exec.CommandContext(ctx, adbPath, arguments...)
output, err := command.CombinedOutput()
if err != nil {
return fmt.Errorf("adb pm grant %s: %w (%s)", permission, err, strings.TrimSpace(string(output)))
}
return nil
}
}
// dangerousPermissions captures Android's PROTECTION_DANGEROUS set as of
// API 34. New runtime permissions added in later releases should be appended
// here when needed.
var dangerousPermissions = map[string]bool{
"android.permission.READ_CALENDAR": true,
"android.permission.WRITE_CALENDAR": true,
"android.permission.CAMERA": true,
"android.permission.READ_CONTACTS": true,
"android.permission.WRITE_CONTACTS": true,
"android.permission.GET_ACCOUNTS": true,
"android.permission.ACCESS_FINE_LOCATION": true,
"android.permission.ACCESS_COARSE_LOCATION": true,
"android.permission.ACCESS_BACKGROUND_LOCATION": true,
"android.permission.RECORD_AUDIO": true,
"android.permission.READ_PHONE_STATE": true,
"android.permission.READ_PHONE_NUMBERS": true,
"android.permission.CALL_PHONE": true,
"android.permission.ANSWER_PHONE_CALLS": true,
"android.permission.READ_CALL_LOG": true,
"android.permission.WRITE_CALL_LOG": true,
"android.permission.ADD_VOICEMAIL": true,
"android.permission.USE_SIP": true,
"android.permission.PROCESS_OUTGOING_CALLS": true,
"android.permission.BODY_SENSORS": true,
"android.permission.SEND_SMS": true,
"android.permission.RECEIVE_SMS": true,
"android.permission.READ_SMS": true,
"android.permission.RECEIVE_WAP_PUSH": true,
"android.permission.RECEIVE_MMS": true,
"android.permission.READ_EXTERNAL_STORAGE": true,
"android.permission.WRITE_EXTERNAL_STORAGE": true,
"android.permission.ACCESS_MEDIA_LOCATION": true,
"android.permission.ACTIVITY_RECOGNITION": true,
"android.permission.POST_NOTIFICATIONS": true,
"android.permission.NEARBY_WIFI_DEVICES": true,
"android.permission.READ_MEDIA_IMAGES": true,
"android.permission.READ_MEDIA_VIDEO": true,
"android.permission.READ_MEDIA_AUDIO": true,
"android.permission.READ_MEDIA_VISUAL_USER_SELECTED": true,
"android.permission.BLUETOOTH_CONNECT": true,
"android.permission.BLUETOOTH_ADVERTISE": true,
"android.permission.BLUETOOTH_SCAN": true,
"android.permission.UWB_RANGING": true,
"android.permission.BODY_SENSORS_BACKGROUND": true,
}
func IsDangerous(permission string) bool {
return dangerousPermissions[permission]
}