Skip to content

Commit

Permalink
feat(android_intent_plus): adds getResolvedActivity method to return …
Browse files Browse the repository at this point in the history
…default resolved activity details
  • Loading branch information
josh-burton committed Oct 9, 2024
1 parent 93fed63 commit 0f97238
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.Nullable;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;

/** Forms and launches intents. */
public final class IntentSender {
Expand Down Expand Up @@ -102,6 +105,41 @@ boolean canResolveActivity(Intent intent) {
return packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null;
}

/**
* Get the default activity that will resolve the intent
*
* <p>This will fail to create and send the intent if {@code applicationContext} hasn't been set *
* at the time of calling.
*
* <p>This currently only supports resolving activities.
*
* @param intent Fully built intent.
* @return Whether the package manager found {@link android.content.pm.ResolveInfo} using its
* {@link PackageManager#resolveActivity(Intent, int)} method.
* @see #buildIntent(String, Integer, String, Uri, Bundle, String, ComponentName, String)
*/
@Nullable
Map<String, Object> getResolvedActivity(Intent intent) {
if (applicationContext == null) {
Log.wtf(TAG, "Trying to resolve an activity before the applicationContext was initialized.");
return null;
}

final PackageManager packageManager = applicationContext.getPackageManager();
ResolveInfo resolveInfo =
packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);

if (resolveInfo != null) {
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("activityName", resolveInfo.activityInfo.name);
resultMap.put("packageName", resolveInfo.activityInfo.packageName);
resultMap.put("appName", resolveInfo.loadLabel(packageManager));
return resultMap;
}

return null;
}

/** Caches the given {@code activity} to use for {@link #send}. */
void setActivity(@Nullable Activity activity) {
this.activity = activity;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
result.success(null);
} else if ("canResolveActivity".equalsIgnoreCase(call.method)) {
result.success(sender.canResolveActivity(intent));
} else if ("getResolvedActivity".equalsIgnoreCase(call.method)) {
result.success(sender.getResolvedActivity(intent));
} else {
result.notImplemented();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@
-->
<uses-permission android:name="android.permission.INTERNET" />

<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="http" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
</queries>

<application
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
Expand Down
21 changes: 21 additions & 0 deletions packages/android_intent_plus/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,20 @@ class ExplicitIntentsWidget extends StatelessWidget {
intent.launch();
}

void _getResolvedActivity(BuildContext context) async {
final intent = AndroidIntent(
action: 'action_view',
data: Uri.encodeFull('http://'),
);

final details = await intent.getResolvedActivity();
if (details != null) {
ScaffoldMessenger.of(context).showSnackBar(

Check notice on line 213 in packages/android_intent_plus/example/lib/main.dart

View workflow job for this annotation

GitHub Actions / Dart Analyzer

Don't use 'BuildContext's across async gaps.

Try rewriting the code to not use the 'BuildContext', or guard the use with a 'mounted' check. See https://dart.dev/diagnostics/use_build_context_synchronously to learn more about this problem.
SnackBar(content: Text("${details.appName} - ${details.packageName}")),
);
}
}

void _openGmail() {
const intent = AndroidIntent(
action: 'android.intent.action.SEND',
Expand Down Expand Up @@ -277,6 +291,13 @@ class ExplicitIntentsWidget extends StatelessWidget {
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => _getResolvedActivity(context),
child: const Text(
'Tap here to get default resolved activity',
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _openGmail,
child: const Text(
Expand Down
52 changes: 52 additions & 0 deletions packages/android_intent_plus/lib/android_intent.dart
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,33 @@ class AndroidIntent {
);
}

/// Get the default activity that will resolve the intent
///
/// This works only on Android platforms.
///
/// Note: ensure the calling app's AndroidManifest contains queries that match the intent.
/// See: https://developer.android.com/guide/topics/manifest/queries-element
Future<ResolvedActivity?> getResolvedActivity() async {
if (!_platform.isAndroid) {
return null;
}

final result = await _channel.invokeMethod<Map<Object?, Object?>>(
'getResolvedActivity',
_buildArguments(),
);

if (result != null) {
return ResolvedActivity(
appName: result["appName"] as String,
activityName: result["activityName"] as String,
packageName: result["packageName"] as String,
);
}

return null;
}

/// Constructs the map of arguments which is passed to the plugin.
Map<String, dynamic> _buildArguments() {
return {
Expand All @@ -224,3 +251,28 @@ class AndroidIntent {
};
}
}

class ResolvedActivity {
final String appName;
final String activityName;
final String packageName;

ResolvedActivity({
required this.appName,
required this.activityName,
required this.packageName,
});

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ResolvedActivity &&
runtimeType == other.runtimeType &&
appName == other.appName &&
activityName == other.activityName &&
packageName == other.packageName;

@override
int get hashCode =>
appName.hashCode ^ activityName.hashCode ^ packageName.hashCode;
}
80 changes: 80 additions & 0 deletions packages/android_intent_plus/test/android_intent_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,86 @@ void main() {
});
});

group('getResolvedActivity', () {
test('pass right params', () async {
androidIntent = AndroidIntent.private(
action: 'action_view',
data: Uri.encodeFull('https://flutter.dev'),
flags: <int>[Flag.FLAG_ACTIVITY_NEW_TASK],
channel: mockChannel,
platform: FakePlatform(operatingSystem: 'android'),
type: 'video/*');
await androidIntent.getResolvedActivity();
verify(mockChannel
.invokeMethod<void>('getResolvedActivity', <String, Object>{
'action': 'action_view',
'data': Uri.encodeFull('https://flutter.dev'),
'flags':
androidIntent.convertFlags(<int>[Flag.FLAG_ACTIVITY_NEW_TASK]),
'type': 'video/*',
}));
});

test('returns a ResolvedActivity', () async {
androidIntent = AndroidIntent.private(
action: 'action_view',
data: Uri.encodeFull('https://flutter.dev'),
channel: mockChannel,
platform: FakePlatform(operatingSystem: 'android'),
);

when(mockChannel.invokeMethod("getResolvedActivity", any))
.thenAnswer((_) async => <String, dynamic>{
"activityName": "activity name",
"appName": "App Name",
"packageName": "com.packagename",
});

final result = await androidIntent.getResolvedActivity();

expect(result?.activityName, equals("activity name"));
expect(result?.appName, equals("App Name"));
expect(result?.packageName, equals("com.packagename"));
});

test('can send Intent with an action and no component', () async {
androidIntent = AndroidIntent.private(
action: 'action_view',
channel: mockChannel,
platform: FakePlatform(operatingSystem: 'android'),
);
await androidIntent.getResolvedActivity();
verify(mockChannel
.invokeMethod<void>('getResolvedActivity', <String, Object>{
'action': 'action_view',
}));
});

test('can send Intent with a component and no action', () async {
androidIntent = AndroidIntent.private(
package: 'packageName',
componentName: 'componentName',
channel: mockChannel,
platform: FakePlatform(operatingSystem: 'android'),
);
await androidIntent.getResolvedActivity();
verify(mockChannel
.invokeMethod<void>('getResolvedActivity', <String, Object>{
'package': 'packageName',
'componentName': 'componentName',
}));
});

test('call in ios platform', () async {
androidIntent = AndroidIntent.private(
action: 'action_view',
channel: mockChannel,
platform: FakePlatform(operatingSystem: 'ios'));
await androidIntent.getResolvedActivity();
verifyZeroInteractions(mockChannel);
});
});

group('launchChooser', () {
test('pass title', () async {
androidIntent = AndroidIntent.private(
Expand Down

0 comments on commit 0f97238

Please sign in to comment.