Skip to content
This repository has been archived by the owner on Jul 11, 2024. It is now read-only.

Support lazy loading through DeviceApps.streamInstalledApplications() #90

Closed
wants to merge 3 commits into from
Closed
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
93 changes: 76 additions & 17 deletions android/src/main/java/fr/g123k/deviceapps/DeviceAppsPlugin.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package fr.g123k.deviceapps;

import static fr.g123k.deviceapps.utils.Base64Utils.encodeToBase64;
import static fr.g123k.deviceapps.utils.DrawableUtils.getBitmapFromDrawable;

import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
Expand Down Expand Up @@ -35,9 +38,6 @@
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;

import static fr.g123k.deviceapps.utils.Base64Utils.encodeToBase64;
import static fr.g123k.deviceapps.utils.DrawableUtils.getBitmapFromDrawable;

/**
* DeviceAppsPlugin
*/
Expand Down Expand Up @@ -79,6 +79,11 @@ public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
@SuppressWarnings("ConstantConditions")
public void onMethodCall(MethodCall call, @NonNull final Result result) {
switch (call.method) {
case "getInstalledPackagesCount":
PackageManager packageManager = context.getPackageManager();
List<PackageInfo> apps = packageManager.getInstalledPackages(0);
result.success(apps.size());
break;
case "getInstalledApps":
boolean systemApps = call.hasArgument("system_apps") && (Boolean) (call.argument("system_apps"));
boolean includeAppIcons = call.hasArgument("include_app_icons") && (Boolean) (call.argument("include_app_icons"));
Expand Down Expand Up @@ -143,28 +148,32 @@ public void run() {

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);
OnAppLoaded onAppLoadedCallback = new OnAppLoaded() {
@Override
public void onAppAvailable(Map<String, Object> app) {
apps.add(app);
}
};

getInstalledApps(includeSystemApps, includeAppIcons, onlyAppsWithLaunchIntent, onAppLoadedCallback);

if (callback != null) {
callback.onInstalledAppsListAvailable(installedApps);
callback.onInstalledAppsListAvailable(onAppLoadedCallback.apps);
}
}

});
}

private List<Map<String, Object>> getInstalledApps(boolean includeSystemApps, boolean includeAppIcons, boolean onlyAppsWithLaunchIntent) {
private void getInstalledApps(boolean includeSystemApps, boolean includeAppIcons, boolean onlyAppsWithLaunchIntent, OnAppLoaded callback) {
if (context == null) {
Log.e(LOG_TAG, "Context is null");
return new ArrayList<>(0);
return;
}

PackageManager packageManager = context.getPackageManager();
List<PackageInfo> apps = packageManager.getInstalledPackages(0);
List<Map<String, Object>> installedApps = new ArrayList<>(apps.size());

for (PackageInfo packageInfo : apps) {
if (!includeSystemApps && isSystemApp(packageInfo)) {
Expand All @@ -178,10 +187,9 @@ private List<Map<String, Object>> getInstalledApps(boolean includeSystemApps, bo
packageInfo,
packageInfo.applicationInfo,
includeAppIcons);
installedApps.add(map);
}

return installedApps;
callback.onAppAvailable(map);
}
}

private boolean openApp(@NonNull String packageName) {
Expand Down Expand Up @@ -295,14 +303,59 @@ private boolean uninstallApp(@NonNull String packageName) {
return false;
}

private void attachAppsChangedListener(Object arguments, final EventChannel.EventSink events) {
if (appsListener == null) {
appsListener = new DeviceAppsChangedListener(this);
}

appsListener.register(context, events);
}

private void attachGetInstalledAppsListener(Object arguments, final EventChannel.EventSink events) {
HashMap args = (HashMap) arguments;
final boolean systemApps = args.containsKey("system_apps") && (boolean) args.get("system_apps");
final boolean includeAppIcons = args.containsKey("include_app_icons") && (boolean) args.get("include_app_icons");
final boolean onlyAppsWithLaunchIntent = args.containsKey("only_apps_with_launch_intent") && (boolean) args.get("only_apps_with_launch_intent");

asyncWork.run(new Runnable() {
@Override
public void run() {
getInstalledApps(systemApps, includeAppIcons, onlyAppsWithLaunchIntent, new OnAppLoaded() {
@Override
public void onAppAvailable(final Map<String, Object> app) {
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
events.success(app);
}
});
}
});

new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
events.endOfStream();
}
});
}
});
}

@Override
public void onListen(Object arguments, final EventChannel.EventSink events) {
if (context != null) {
if (appsListener == null) {
appsListener = new DeviceAppsChangedListener(this);
HashMap args = (HashMap) arguments;
String event = (String) args.get("event");

switch (event) {
case "listenToAppsChanges":
attachAppsChangedListener(arguments, events);
break;
case "getInstalledApps":
attachGetInstalledAppsListener(arguments, events);
break;
}

appsListener.register(context, events);
}
}

Expand Down Expand Up @@ -379,3 +432,9 @@ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
context = null;
}
}

interface OnAppLoaded {
List<Map<String, Object>> apps = new ArrayList<>(0);

void onAppAvailable(Map<String, Object> app);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,18 @@
public class DeviceAppsChangedListener {

private final DeviceAppsChangedListenerInterface callback;
private final Set<EventChannel.EventSink> sinks;
private EventChannel.EventSink eventSink;

private BroadcastReceiver appsBroadcastReceiver;

public DeviceAppsChangedListener(DeviceAppsChangedListenerInterface callback) {
this.callback = callback;
this.sinks = new HashSet<>(1);
this.eventSink = null;
}

public void register(@NonNull Context context, EventChannel.EventSink events) {
unregister(context);

if (appsBroadcastReceiver == null) {
createBroadcastReceiver();
}
Expand All @@ -36,7 +38,7 @@ public void register(@NonNull Context context, EventChannel.EventSink events) {
intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
intentFilter.addDataScheme("package");

sinks.add(events);
eventSink = events;

context.registerReceiver(appsBroadcastReceiver, intentFilter);
}
Expand Down Expand Up @@ -75,35 +77,31 @@ public void onReceive(Context context, Intent intent) {
}

void onPackageInstalled(String packageName) {
for (EventChannel.EventSink sink : sinks) {
callback.onPackageInstalled(packageName, sink);
}
callback.onPackageInstalled(packageName, eventSink);
}

void onPackageUpdated(String packageName) {
for (EventChannel.EventSink sink : sinks) {
callback.onPackageUpdated(packageName, sink);
}
callback.onPackageUpdated(packageName, eventSink);
}

void onPackageUninstalled(String packageName) {
for (EventChannel.EventSink sink : sinks) {
callback.onPackageUninstalled(packageName, sink);
}
callback.onPackageUninstalled(packageName, eventSink);
}

void onPackageChanged(String packageName) {
for (EventChannel.EventSink sink : sinks) {
callback.onPackageChanged(packageName, sink);
}
callback.onPackageChanged(packageName, eventSink);
}

public void unregister(@NonNull Context context) {
if (appsBroadcastReceiver != null) {
context.unregisterReceiver(appsBroadcastReceiver);
appsBroadcastReceiver = null;
}

if (eventSink != null) {
eventSink.endOfStream();
eventSink = null;
}

sinks.clear();
}

}
28 changes: 27 additions & 1 deletion lib/src/plugin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,23 @@ class DeviceApps {
static const EventChannel _eventChannel =
EventChannel('g123k/device_apps_events');

static Stream<Application> streamInstalledApplications({
bool includeSystemApps: false,
bool includeAppIcons: false,
bool onlyAppsWithLaunchIntent: false,
}) {
final Stream<dynamic> apps = _eventChannel.receiveBroadcastStream(
<String, dynamic>{
'system_apps': includeSystemApps,
'include_app_icons': includeAppIcons,
'only_apps_with_launch_intent': onlyAppsWithLaunchIntent,
'event': 'getInstalledApps',
},
);

return apps.map<Application>((dynamic app) => Application._(app));
}

/// List installed applications on the device
/// [includeSystemApps] will also include system apps (or pre-installed) like
/// Phone, Settings...
Expand Down Expand Up @@ -108,6 +125,13 @@ class DeviceApps {
.catchError((dynamic err) => false);
}

static Future<int> getInstalledPackagesCount() {
return _methodChannel
.invokeMethod<int>('getInstalledPackagesCount')
.then((int? value) => value ?? 0)
.catchError((dynamic err) => 0);
}

/// Launch an app based on its [packageName]
/// You will then receive in return if the app was opened
/// (will be false if the app is not installed, or if no "launcher" intent is
Expand Down Expand Up @@ -164,7 +188,9 @@ class DeviceApps {
/// is too verbose for you
static Stream<ApplicationEvent> listenToAppsChanges() {
return _eventChannel
.receiveBroadcastStream()
.receiveBroadcastStream(
<String, dynamic>{'event': 'listenToAppsChanges'},
)
.map(((dynamic event) =>
ApplicationEvent._(event as Map<dynamic, dynamic>)))
.handleError((Object err) => null);
Expand Down