조용한 담장

Flutter: device_apps package 본문

Flutter

Flutter: device_apps package

iosroid 2020. 6. 11. 15:16

설치된 앱의 목록을 가져올 때 쓸 수 있는 package 이다.

앱의 정보를 얻을 수도 있고 앱의 실행도 가능하다.

Android 만 지원한다.

 

https://pub.dev/packages/device_apps

https://github.com/g123k/flutter_plugin_device_apps

예제 실행해 보기

 

예제 코드를 실행해 보자.

https://github.com/g123k/flutter_plugin_device_apps/tree/master/example

 

예제 실행 화면:

 

device_apps

Platform-specific code 를 사용해서 설치된 정보를 얻어 화면에 뿌리는 동작을 하는데

어떻게 동작하는지 살펴보자...

코드로 동작 살펴보기

android/src/main/java/fr/g123k/deviceapps/DeviceAppsPlugin.java

public class DeviceAppsPlugin implements MethodCallHandler, PluginRegistry.ViewDestroyListener {
  // ...
  @Override
  public void onMethodCall(MethodCall call, final Result result) {
    switch (call.method) {
      case "getInstalledApps":
        // ...
        fetchInstalledApps(systemApps, includeAppIcons, onlyAppsWithLaunchIntent, new InstalledAppsCallback() {
          @Override
          public void onInstalledAppsListAvailable(final List<Map<String, Object>> apps) {
            if (!activity.isFinishing()) {
              activity.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                  result.success(apps);
                }
              });
            }
          }
        });
        break;
    }
  }

  private void fetchInstalledApps(final boolean includeSystemApps, final boolean includeAppIcons, final boolean onlyAppsWithLaunchIntent, final InstalledAppsCallback callback) {
    asyncWork.run(new Runnable() {

      @Override
      public void run() {
        List<Map<String, Object>> installedApps = getInstalledApps(includeSystemApps, includeAppIcons, onlyAppsWithLaunchIntent);
        // ...
      }

    });
  }

  private List<Map<String, Object>> getInstalledApps(boolean includeSystemApps, boolean includeAppIcons, boolean onlyAppsWithLaunchIntent) {
    PackageManager packageManager = activity.getPackageManager();
    List<PackageInfo> apps = packageManager.getInstalledPackages(0);
    List<Map<String, Object>> installedApps = new ArrayList<>(apps.size());

    for (PackageInfo pInfo : apps) {
      // ...
      Map<String, Object> map = getAppData(packageManager, pInfo, includeAppIcons);
      installedApps.add(map);
    }

    return installedApps;
  }

  private Map<String, Object> getAppData(PackageManager packageManager, PackageInfo pInfo, boolean includeAppIcon) {
    Map<String, Object> map = new HashMap<>();
    map.put("app_name", pInfo.applicationInfo.loadLabel(packageManager).toString());
    map.put("apk_file_path", pInfo.applicationInfo.sourceDir);
    map.put("package_name", pInfo.packageName);
    map.put("version_code", pInfo.versionCode);
    map.put("version_name", pInfo.versionName);
    map.put("data_dir", pInfo.applicationInfo.dataDir);
    map.put("system_app", isSystemApp(pInfo));
    map.put("install_time", pInfo.firstInstallTime);
    map.put("update_time", pInfo.lastUpdateTime);

    // ...

    return map;
  }
}

getInstalledApps method call 이 호출되면 fetchInstalledApps() 이 호출되고 getInstalledApps() 를 호출하는데

여기서 packageManager.getInstalledPackages() 를 호출하여 설치된 패키지의 정보를 가진 PackageInfo 들을 얻는다.

PackageInfo 의 정보를 getAppData() 에서 Map 으로 필요한 데이터를 정리해서 List<Map<String, Object>> 의 형태로 최종 목록을 result.success(apps); 로 Flutter app 으로 전달한다.

 

example/lib/main.dart

class _ListAppsPagesContent extends StatelessWidget {
  // ...
  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
        future: DeviceApps.getInstalledApplications(
            includeAppIcons: true,
            includeSystemApps: includeSystemApps,
            onlyAppsWithLaunchIntent: onlyAppsWithLaunchIntent),
        builder: (context, data) {
          if (data.data == null) {
            return Center(child: CircularProgressIndicator());
          } else {
            List<Application> apps = data.data;
            print(apps);
            return ListView.builder(
                itemBuilder: (context, position) {
                  Application app = apps[position];
                  return Column(
                    children: <Widget>[
                      ListTile(
                        leading: app is ApplicationWithIcon
                            ? CircleAvatar(
                                backgroundImage: MemoryImage(app.icon),
                                backgroundColor: Colors.white,
                              )
                            : null,
                        onTap: () => DeviceApps.openApp(app.packageName),
                        title: Text("${app.appName} (${app.packageName})"),
                        subtitle: Text('Version: ${app.versionName}\nSystem app: ${app.systemApp}\nAPK file path: ${app.apkFilePath}\nData dir : ${app.dataDir}\nInstalled: ${DateTime.fromMillisecondsSinceEpoch(app.installTimeMilis).toString()}\nUpdated: ${DateTime.fromMillisecondsSinceEpoch(app.updateTimeMilis).toString()}'),
                      ),
                      Divider(
                        height: 1.0,
                      )
                    ],
                  );
                },
                itemCount: apps.length);
          }
        });
  }
}

 

lib/device_apps.dartgetInstalledApplications() 를 호출해 invokeMethod('getInstalledApps') 의 결과를 얻어 ListView.builder() 로 화면에 설치된 앱 들을 리스트로 표시한다.

 

lib/device_apps.dart

class DeviceApps {
  static const MethodChannel _channel =
      const MethodChannel('g123k/device_apps');

  static Future<List<Application>> getInstalledApplications(
      {bool includeSystemApps: false,
      bool includeAppIcons: false,
      bool onlyAppsWithLaunchIntent: false}) async {
    return _channel.invokeMethod('getInstalledApps', {
      'system_apps': includeSystemApps,
      'include_app_icons': includeAppIcons,
      'only_apps_with_launch_intent': onlyAppsWithLaunchIntent
    }).then((apps) {
      if (apps != null && apps is List) {
        List<Application> list = new List();
          // ...
        }

        return list;
      } else {
        return List<Application>(0);
      }
      // ...
    });
  }

똑같이 만들어 보기

앱 목록을 가져오는 기능만 따라 만들어 보자...

android/app/src/main/kotlin/com.example.simplelauncher/MainActivity.kt

class MainActivity: FlutterActivity() {
    private val _channel = "my.device_app.copy"
    private val _systemAppMask = ApplicationInfo.FLAG_SYSTEM or ApplicationInfo.FLAG_UPDATED_SYSTEM_APP

    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, _channel).setMethodCallHandler {
            call, result ->
            if (call.method == "getInstalledApps") {
                val apps = packageManager.getInstalledPackages(0)
                val installedApps = mutableListOf<Map<String, Any>>()
                for (pInfo in apps) {
                    if (pInfo.applicationInfo.flags and _systemAppMask != 0) {
                        continue
                    }
                    val newMap = mapOf<String, Any>(
                            "app_name" to pInfo.applicationInfo.loadLabel(packageManager).toString(),
                            "apk_file_path" to pInfo.applicationInfo.sourceDir,
                            "package_name" to pInfo.packageName,
                            "version_name" to pInfo.versionName,
                            "data_dir" to pInfo.applicationInfo.dataDir,
                            "install_time" to pInfo.firstInstallTime,
                            "update_time" to pInfo.lastUpdateTime
                    )
                    //println("app_name: ${newMap["app_name"]}")
                    installedApps.add(newMap)
                }
                result.success(installedApps)
            } else {
                result.notImplemented()
            }
        }
    }
}

getInstalledApps method 를 만들었고 packageManager.getInstalledPackages() 를 통해 List<PackageInfo> 를 얻어 mutableListOf<Map<String, Any>> 로 저장해 리턴한다.

 

lib/my_device_app_copy.dart

class _MyDeviceApps extends StatelessWidget {
  List apps;

  static const _platform = const MethodChannel('my.device_app.copy');

  Future<void> _getInstalledApps() async {
    try {
      final result = await _platform.invokeMethod('getInstalledApps');
      //print(result);
      return result;
    } on PlatformException catch (e) {
      print('error ${e.message}');
      return null;
    }
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      future: _getInstalledApps(),
      builder: (context, data) {
        if (data == null) {
          return Center(child: CircularProgressIndicator());
        } else {
          apps = data.data;
          return ListView.builder(
            itemBuilder: (context, position) {
              return Column(
                children: <Widget>[
                  ListTile(
                    title: Text(
                        "${apps[position]['app_name']}, ${apps[position]['package_name']}"),
                    subtitle: Text(
                        "Version: ${apps[position]['version_name']}\nAPK file path:${apps[position]['apk_file_path']}\nData dir:${apps[position]['data_dir']}\nInstall time:${DateTime.fromMillisecondsSinceEpoch(apps[position]['install_time']).toString()}\nUpdate time:${DateTime.fromMillisecondsSinceEpoch(apps[position]['update_time']).toString()}"),
                  )
                ],
              );
            },
            itemCount: apps.length,
          );
        }
      },
    );
  }

_getInstalledApps() 를 통해 invokeMethod('getInstalledApps') 를 호출하여 결과를 얻고 ListView.builder() 로 결과를 화면에 생성한다.

 

 

코드는 허접하지만 동작을 확인한 것에 의의를...

 

번외로

안드로이드 스튜디오의 Java 코드를 Kotlin 코드로 변경해주는 기능을 써보면

어떻게 바꿔주는지 해봤는데...

// ...
fun onMethodCall(call: MethodCall, result: Result) {
  when (call.method) {
    "getInstalledApps" -> {
      val systemApps = call.hasArgument("system_apps") && (call.argument<Any>("system_apps") as Boolean?)!!
      val includeAppIcons = call.hasArgument("include_app_icons") && (call.argument<Any>("include_app_icons") as Boolean?)!!
      val onlyAppsWithLaunchIntent = call.hasArgument("only_apps_with_launch_intent") && (call.argument<Any>("only_apps_with_launch_intent") as Boolean?)!!
      fetchInstalledApps(systemApps, includeAppIcons, onlyAppsWithLaunchIntent, InstalledAppsCallback { apps ->
        if (!activity.isFinishing()) {
          activity.runOnUiThread(Runnable { result.success(apps) })
        }
      })
    }
// ...

private fun fetchInstalledApps(includeSystemApps: Boolean, includeAppIcons: Boolean, onlyAppsWithLaunchIntent: Boolean, callback: InstalledAppsCallback?) {
  asyncWork.run(Runnable {
    val installedApps = getInstalledApps(includeSystemApps, includeAppIcons, onlyAppsWithLaunchIntent)
    callback?.onInstalledAppsListAvailable(installedApps)
  })
}

private fun getInstalledApps(includeSystemApps: Boolean, includeAppIcons: Boolean, onlyAppsWithLaunchIntent: Boolean): List<Map<String, Any>> {
  val packageManager: PackageManager = activity.getPackageManager()
  val apps: List<PackageInfo> = packageManager.getInstalledPackages(0)
  val installedApps: MutableList<Map<String, Any>> = ArrayList(apps.size)
  // ...
    val map = getAppData(packageManager, pInfo, includeAppIcons)
    installedApps.add(map)
  }
  return installedApps
}

private fun getAppData(packageManager: PackageManager, pInfo: PackageInfo, includeAppIcon: Boolean): Map<String, Any> {
  val map: MutableMap<String, Any> = HashMap()
  map["app_name"] = pInfo.applicationInfo.loadLabel(packageManager).toString()
  map["apk_file_path"] = pInfo.applicationInfo.sourceDir
  map["package_name"] = pInfo.packageName
  map["version_code"] = pInfo.versionCode
  map["version_name"] = pInfo.versionName
  map["data_dir"] = pInfo.applicationInfo.dataDir
  map["system_app"] = isSystemApp(pInfo)
  map["install_time"] = pInfo.firstInstallTime
  map["update_time"] = pInfo.lastUpdateTime
  // ...
  return map
}

예쁘게 잘 바꿔준다...

 

'Flutter' 카테고리의 다른 글

Flutter Package: school_ui_toolkit  (0) 2020.07.30
Flutter Gems  (0) 2020.07.28
Flutter: New Material Motion  (0) 2020.05.22
Flutter 1.17  (0) 2020.05.07
Flutter Open Source App 1 : Time Cop  (0) 2020.04.02
Comments