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
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,11 @@ class _DeepLinkListViewMainPanel extends StatelessWidget {
message: 'Your Flutter project has no Links to verify.',
);
case PagePhase.analyzeErrorPage:
assert(controller.currentAppLinkSettings?.error != null);
final error = [
controller.currentAppLinkSettings?.error,
controller.currentUniversalLinkSettings?.error,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this pr introduced a new way things can return error, I make sure both android and ios error will surface to the ui.

].nonNulls.join('\n\n');
assert(error.isNotEmpty);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expand All @@ -104,12 +108,8 @@ class _DeepLinkListViewMainPanel extends StatelessWidget {
const SizedBox(height: densePadding),
Expanded(
child: Scrollbar(
thumbVisibility: true,
child: SingleChildScrollView(
child: Text(
controller.currentAppLinkSettings!.error!,
style: theme.errorTextStyle,
),
child: Text(error, style: theme.errorTextStyle),
),
),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -663,8 +663,8 @@ class DeepLinksController extends DevToolsScreenController
}

Future<void> validateLinks() async {
final appLinkSettings = currentAppLinkSettings;
if (appLinkSettings?.error != null) {
if (currentAppLinkSettings?.error != null ||
currentUniversalLinkSettings?.error != null) {
pagePhase.value = PagePhase.analyzeErrorPage;
ga.select(
gac.deeplink,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,14 @@ Future<UniversalLinkSettings> requestIosUniversalLinkSettings(
},
);
final resp = await request(uri.toString());
if (resp?.statusOk ?? false) {
return UniversalLinkSettings.fromJson(resp!.body);
} else {
logWarning(resp, DeeplinkApi.iosUniversalLinkSettings);
if (resp != null) {
if (resp.statusOk) {
return UniversalLinkSettings.fromJson(resp.body);
} else {
logWarning(resp, DeeplinkApi.iosUniversalLinkSettings);
return UniversalLinkSettings.fromErrorJson(resp.body.toString());
}
}
}
return UniversalLinkSettings.empty;
return UniversalLinkSettings.error('DevTools server is not available');
}
3 changes: 3 additions & 0 deletions packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ TODO: Remove this section if there are not any updates.
* Added a "Watch tutorial" link to the status line that points to the
[deep links video tutorial](https://youtu.be/d7sZL6hIElw).
[#9925](https://github.com/flutter/devtools/pull/9925)
* Validated build option parameters and surfaced iOS universal link settings
errors in the Deep Links tool.
[#10022](https://github.com/flutter/devtools/pull/10022)

## VS Code sidebar updates

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -655,5 +655,29 @@ void main() {
expect(find.text('/ios-path1'), findsOneWidget);
expect(find.text('NOT /ios-path2'), findsOneWidget);
});

testWidgetsWithWindowSize(
'surfaces iOS universal link settings error to UI',
windowSize,
(WidgetTester tester) async {
final deepLinksController = TestDeepLinksController();
const iosErrorMessage =
'Unknown iOS build configuration (Debug) or target (Runner).';

deepLinksController
..selectedProject.value = FlutterProject(
path: '/abc',
androidVariants: ['debug', 'release'],
iosBuildOptions: xcodeBuildOptions,
)
..fakeAndroidDeepLinks = [defaultAndroidDeeplink]
..fakeIosError = iosErrorMessage;

await pumpDeepLinkScreen(tester, controller: deepLinksController);

expect(deepLinksController.pagePhase.value, PagePhase.analyzeErrorPage);
expect(find.text(iosErrorMessage), findsOneWidget);
},
);
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import 'package:devtools_app/devtools_app.dart';
import 'package:devtools_app/src/screens/deep_link_validation/deep_links_model.dart';
import 'package:devtools_app/src/screens/deep_link_validation/deep_links_services.dart';
import 'package:devtools_shared/devtools_deeplink.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart';
import 'package:http/testing.dart';
Expand Down Expand Up @@ -58,6 +59,7 @@ class TestDeepLinksController extends DeepLinksController {
bool hasAndroidDomainErrors = false;
String iosValidationResponse = '';
List<String> fakeIosDomains = [];
String? fakeIosError;

late DeepLinksService _deepLinksService;

Expand All @@ -74,9 +76,9 @@ class TestDeepLinksController extends DeepLinksController {
androidAppLinks[selectedAndroidVariantIndex.value] = fakeAppLinkSettings(
fakeAndroidDeepLinks,
);
iosLinks[selectedIosConfigurationIndex.value] = fakeUniversalLinkSettings(
fakeIosDomains,
);
iosLinks[selectedIosConfigurationIndex.value] = fakeIosError != null
? UniversalLinkSettings.error(fakeIosError!)
: fakeUniversalLinkSettings(fakeIosDomains);

await super.validateLinks();
}
Expand Down
4 changes: 4 additions & 0 deletions packages/devtools_shared/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ found in the LICENSE file or at https://developers.google.com/open-source/licens

* Track providing package name for DevTools extensions to isolate extension enablement,
deduplication, and asset loading.
* Validate `rootPath`, `buildVariant`, `configuration`, and `target` parameters in
`DeeplinkManager`.
* Add `UniversalLinkSettings.fromErrorJson`, `UniversalLinkSettings.error`, and
`UniversalLinkSettings.error` getter to surface iOS universal link settings errors.

# 14.0.0

Expand Down
130 changes: 109 additions & 21 deletions packages/devtools_shared/lib/src/deeplink/deeplink_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import 'dart:io';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;

import 'xcode_build_options.dart';

class DeeplinkManager {
/// A regex to retrieve the json part from the stdout of Android analyzer.
///
Expand All @@ -32,6 +34,25 @@ class DeeplinkManager {
/// APIs.
static const kOutputJsonField = 'json';

/// Cached Android build variants keyed by normalized project root path.
///
/// Populated by [getAndroidBuildVariants] and used to validate `buildVariant`
/// in [getAndroidAppLinkSettings].
static final _androidBuildVariantsCache = <String, Set<String>>{};

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This unfortunately means the server api handler will be stateful, but this is needed to filter bad request


/// Cached iOS Xcode build options keyed by normalized project root path.
///
/// Populated by [getIosBuildOptions] and used to validate `configuration` and
/// `target` in [getIosUniversalLinkSettings].
static final _iosBuildOptionsCache = <String, XcodeBuildOptions>{};

/// Clears the cached Android build variants and iOS build options.
@visibleForTesting
static void clearBuildOptionsCache() {
_androidBuildVariantsCache.clear();
_iosBuildOptionsCache.clear();
}

// TODO(https://github.com/flutter/devtools/issues/9702): Use the `DashTool`
// and `DashEnvVar` enums and `getEnvironment()` helper directly from
// `package:unified_analytics` once the pinned Flutter candidate SDK in this
Expand Down Expand Up @@ -66,16 +87,22 @@ class DeeplinkManager {
Future<ProcessResult> runProcess(
String executable, {
required List<String> arguments,
String? ide,
bool suppressAnalytics = false,
required String workingDirectory,
required String? ide,
required bool suppressAnalytics,
}) {
final environment = <String, String>{
...Platform.environment,
'DASH__SUPPRESS_ANALYTICS': suppressAnalytics.toString(),
'DASH__TOOL': ide != null ? _mapIdeToDashToolLabel(ide) : 'devtools',
};

return Process.run(executable, arguments, environment: environment);
return Process.run(
executable,
arguments,
workingDirectory: workingDirectory,
environment: environment,
);
}

String _mapIdeToDashToolLabel(String ide) {
Expand Down Expand Up @@ -116,6 +143,7 @@ class DeeplinkManager {

Future<String> _runFlutterCommand(
List<String> arguments, {
required String workingDirectory,
required RegExp outputMatcher,
String? ide,
bool suppressAnalytics = false,
Expand All @@ -124,6 +152,7 @@ class DeeplinkManager {
final result = await runProcess(
flutterPath,
arguments: arguments,
workingDirectory: workingDirectory,
ide: ide,
suppressAnalytics: suppressAnalytics,
);
Expand All @@ -140,10 +169,11 @@ class DeeplinkManager {
}
}

Map<String, Object?> _handleRunFlutterError(
covariant _FlutterProcessError error,
) {
return <String, String?>{kErrorField: error.message};
Map<String, Object?> _handleRunFlutterError(Object error) {
final message = error is _FlutterProcessError
? error.message
: error.toString();
return <String, String?>{kErrorField: message};
}

Future<Map<String, Object?>> _handleReadJsonFile(String filePath) {
Expand All @@ -166,31 +196,62 @@ class DeeplinkManager {
String? ide,
bool suppressAnalytics = false,
}) {
final canonicalPath = path.canonicalize(rootPath);
return _runFlutterCommand(
<String>['analyze', '--android', '--list-build-variants', rootPath],
<String>['analyze', '--android', '--list-build-variants'],
workingDirectory: rootPath,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

convert to use working directory to avoid bad path or non path argument.

outputMatcher: _androidBuildVariantJsonRegex,
ide: ide,
suppressAnalytics: suppressAnalytics,
).then<Map<String, Object?>>(
_handleJsonOutput,
onError: _handleRunFlutterError,
);
).then<Map<String, Object?>>((jsonOutput) {
try {
final variants = (jsonDecode(jsonOutput) as List)
.cast<String>()
.toSet();
_androidBuildVariantsCache[canonicalPath] = variants;
} on Object catch (e) {
return <String, String?>{
kErrorField: 'Failed to parse Android build variants: $e',
};
}
return _handleJsonOutput(jsonOutput);
}, onError: _handleRunFlutterError);
Comment thread
chunhtai marked this conversation as resolved.
}

static const _fileIssueMessage =
'This should not happen; please file an issue at '
'https://github.com/flutter/devtools/issues.';

Future<Map<String, Object?>> getAndroidAppLinkSettings({
required String rootPath,
required String buildVariant,
String? ide,
bool suppressAnalytics = false,
}) {
}) async {
final canonicalPath = path.canonicalize(rootPath);
final validVariants = _androidBuildVariantsCache[canonicalPath];
if (validVariants == null) {
return <String, String?>{
kErrorField:
'Android build variants for "$rootPath" have not been parsed yet. '
'$_fileIssueMessage',
};
}
if (!validVariants.contains(buildVariant)) {
return <String, String?>{
kErrorField:
'Unknown Android build variant "$buildVariant" for "$rootPath". '
'$_fileIssueMessage',
};
}
return _runFlutterCommand(
<String>[
'analyze',
'--android',
'--output-app-link-settings',
'--build-variant=$buildVariant',
rootPath,
],
workingDirectory: rootPath,
outputMatcher: _outputFilePathRegex,
ide: ide,
suppressAnalytics: suppressAnalytics,
Expand All @@ -205,15 +266,25 @@ class DeeplinkManager {
String? ide,
bool suppressAnalytics = false,
}) {
final canonicalPath = path.canonicalize(rootPath);
return _runFlutterCommand(
<String>['analyze', '--ios', '--list-build-options', rootPath],
<String>['analyze', '--ios', '--list-build-options'],
workingDirectory: rootPath,
outputMatcher: _iosBuildOptionsJsonRegex,
ide: ide,
suppressAnalytics: suppressAnalytics,
).then<Map<String, Object?>>(
_handleJsonOutput,
onError: _handleRunFlutterError,
);
).then<Map<String, Object?>>((jsonOutput) {
try {
_iosBuildOptionsCache[canonicalPath] = XcodeBuildOptions.fromJson(
jsonOutput,
);
} on Object catch (e) {
return <String, String?>{
kErrorField: 'Failed to parse iOS build options: $e',
};
}
return _handleJsonOutput(jsonOutput);
}, onError: _handleRunFlutterError);
Comment thread
chunhtai marked this conversation as resolved.
}

Future<Map<String, Object?>> getIosUniversalLinkSettings({
Expand All @@ -222,16 +293,33 @@ class DeeplinkManager {
required String target,
String? ide,
bool suppressAnalytics = false,
}) {
}) async {
final canonicalPath = path.canonicalize(rootPath);
final validOptions = _iosBuildOptionsCache[canonicalPath];
if (validOptions == null) {
return <String, String?>{
kErrorField:
'iOS build options for "$rootPath" have not been parsed yet. '
'$_fileIssueMessage',
};
}
if (!validOptions.configurations.contains(configuration) ||
!validOptions.targets.contains(target)) {
return <String, String?>{
kErrorField:
'Unknown iOS build configuration ($configuration) or target ($target) for "$rootPath". '
'$_fileIssueMessage',
};
}
return _runFlutterCommand(
<String>[
'analyze',
'--ios',
'--output-universal-link-settings',
'--configuration=$configuration',
'--target=$target',
rootPath,
],
workingDirectory: rootPath,
outputMatcher: _outputFilePathRegex,
ide: ide,
suppressAnalytics: suppressAnalytics,
Expand Down
Loading
Loading