mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
> [!NOTE] > This PR lands https://github.com/flutter/flutter/pull/158473 (closed due to confusing git-isms). Please see that PR for all comments and discussion on landing this feature. ### Overview Implements setting content sensitivity on Android via a new widget called `SensitiveContent` that currently will only function on Android. ### How it's implemented There are three different content sensitivity levels: [`autoSensitive`](https://developer.android.com/reference/android/view/View#CONTENT_SENSITIVITY_AUTO), [`sensitive`](https://developer.android.com/reference/android/view/View#CONTENT_SENSITIVITY_SENSITIVE), [`notSensitive`](https://developer.android.com/reference/android/view/View#CONTENT_SENSITIVITY_NOT_SENSITIVE). All except `autoSensitive` behave as their Android counterpart level behaves (see links on the levels); for Flutter, `autoSensitive` will behave the same as `notSensitive` though it is implemented as if it works (see https://github.com/flutter/flutter/issues/160879 for more details. In any given Flutter view, each `SensitiveContent` widget sets a content sensitivity level and there may be multiple `SensitiveContent` widgets in the widget tree at a time, but only the most severe content sensitivity level in the tree will be maintained in order to ensure secure content remains obscured during screen projection. This means that even widgets that are not visible may still impact the overall content sensitivity of the entire Flutter view (see https://github.com/flutter/flutter/issues/160051 for more details). Under the hood, the implementation uses method channels. Theoretically, this could cause a delay in setting the content sensitivity level and a Flutter view being obscured during screen projection on time, so we will investigate using JNIgen in the future (see https://github.com/flutter/flutter/issues/160050 for more details). Over the method channel, the framework will send a Flutter view ID and content sensitivity level to the engine to set the expected content sensitivity level for the view. ### Timeline #### Required for feature shipping - Land this PR with the method channel implementation of content sensitivity. #### Short-term follow up tasks - Ensure every `FlutterView` has a unique ID for the native `SensitiveContentPlugin` to identify them by: https://github.com/flutter/flutter/pull/162685 - Add `SensitiveContent` widget to relevant Flutter widgets: https://github.com/flutter/flutter/issues/167302 #### Long-term follow up tasks - Convert this PR to use JNIgen: https://github.com/flutter/flutter/issues/160050. Verify no frames are dropped, revealing sensitive content during media projection: https://github.com/flutter/flutter/issues/164820. #### Stretch goals - Make the `SensitiveContent` widget sensitive to the visibility of child widgets: https://github.com/flutter/flutter/issues/160051 - Implement `autoSensitivity` mode: https://github.com/flutter/flutter/issues/160879 - Investigate backwards compatibility for APIs < 35: https://github.com/flutter/flutter/issues/159208 ### Open questions - ~Is it okay to hard-code the `FlutterActivity` and `FlutterFragment` view IDs? Does this impact add-to-app in any way? I assume we would need extenders of those classes to use a different view ID than the hardcoded one.~ Approach not used. - ~Should we expose `autoSensitive` for now? If so, what behavior should it have?~ Exposing `autoSensitive` as if it works. ### One way to test this PR (rough steps) 1. Create a Flutter app that has some number of `SensitiveContent` widgets. 9. Run the app on an emulator/device that runs API 35 and has the Google meets app downloaded. 10. Start a Google meets meeting and screen share (works with full screen and single app mode). 11. Join the Google meets meeting from another device and witness a blacked out screen if content has been marked sensitive. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
127 lines
4.7 KiB
Dart
127 lines
4.7 KiB
Dart
// Copyright 2014 The Flutter Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
import 'dart:async' show Completer;
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'sensitive_content_utils.dart';
|
|
|
|
void main() {
|
|
const ContentSensitivity defaultContentSensitivitySetting = ContentSensitivity.autoSensitive;
|
|
|
|
tearDown(() {
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
SystemChannels.sensitiveContent,
|
|
null,
|
|
);
|
|
});
|
|
testWidgets(
|
|
'while SensitiveContent widget is being registered, SizedBox.shrink is built initially, then child widget is built upon completion',
|
|
(WidgetTester tester) async {
|
|
final Completer<void> setContentSensitivityCompleter = Completer<void>();
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
SystemChannels.sensitiveContent,
|
|
(MethodCall methodCall) {
|
|
if (methodCall.method == 'SensitiveContent.setContentSensitivity') {
|
|
return setContentSensitivityCompleter.future;
|
|
} else if (methodCall.method == 'SensitiveContent.getContentSensitivity') {
|
|
return Future<int>.value(defaultContentSensitivitySetting.index);
|
|
} else if (methodCall.method == 'SensitiveContent.isSupported') {
|
|
return Future<bool>.value(true);
|
|
} else {
|
|
return null;
|
|
}
|
|
},
|
|
);
|
|
|
|
final Container childWidget = Container();
|
|
|
|
await tester.pumpWidget(
|
|
SensitiveContent(sensitivity: ContentSensitivity.sensitive, child: childWidget),
|
|
);
|
|
|
|
expect(find.byWidget(childWidget), findsNothing);
|
|
final SizedBox shrinkBox = tester.firstWidget(find.byType(SizedBox)) as SizedBox;
|
|
expect(shrinkBox.width, 0);
|
|
expect(shrinkBox.height, 0);
|
|
|
|
setContentSensitivityCompleter.complete();
|
|
|
|
// Two pumps to complete registration, then re-build SensitiveContent widget.
|
|
await tester.pump();
|
|
await tester.pump();
|
|
|
|
expect(find.byWidget(childWidget), findsOne);
|
|
expect(find.byType(SizedBox), findsNothing);
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'when SensitiveContent widget changes sensitivity, SizedBox.shrink is built initially, then child widget is built upon completion',
|
|
(WidgetTester tester) async {
|
|
final Completer<void> setContentSensitivityCompleter = Completer<void>();
|
|
int setContentSensitivityCall = 0;
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
SystemChannels.sensitiveContent,
|
|
(MethodCall methodCall) {
|
|
if (methodCall.method == 'SensitiveContent.setContentSensitivity') {
|
|
setContentSensitivityCall++;
|
|
// Make second call to update content sensitivity awaits the Future for test.
|
|
if (setContentSensitivityCall == 2 && methodCall.arguments == 'autoSensitive') {
|
|
return setContentSensitivityCompleter.future;
|
|
}
|
|
return Future<void>.value();
|
|
} else if (methodCall.method == 'SensitiveContent.getContentSensitivity') {
|
|
return Future<int>.value(defaultContentSensitivitySetting.index);
|
|
} else if (methodCall.method == 'SensitiveContent.isSupported') {
|
|
return Future<bool>.value(true);
|
|
} else {
|
|
return null;
|
|
}
|
|
},
|
|
);
|
|
|
|
const Key scKey = Key('scKey');
|
|
final Container childWidget = Container();
|
|
|
|
await tester.pumpWidget(
|
|
ChangeContentSensitivityTester(
|
|
key: scKey,
|
|
initialContentSensitivity: ContentSensitivity.sensitive,
|
|
child: childWidget,
|
|
),
|
|
);
|
|
await tester.pump();
|
|
|
|
final ChangeContentSensitivityTesterState scState = tester
|
|
.firstState<ChangeContentSensitivityTesterState>(find.byKey(scKey));
|
|
scState.changeContentSensitivityTo(ContentSensitivity.autoSensitive);
|
|
|
|
await tester.pump();
|
|
|
|
expect(find.byWidget(childWidget), findsNothing);
|
|
final SizedBox shrinkBox = tester.firstWidget(find.byType(SizedBox)) as SizedBox;
|
|
expect(shrinkBox.width, 0);
|
|
expect(shrinkBox.height, 0);
|
|
|
|
await tester.pump();
|
|
|
|
setContentSensitivityCompleter.complete();
|
|
|
|
// Two pumps to complete re-registration, then re-build SensitiveContent widget.
|
|
await tester.pump();
|
|
await tester.pump();
|
|
|
|
expect(find.byType(childWidget.runtimeType), findsOne);
|
|
expect(find.byType(SizedBox), findsNothing);
|
|
|
|
// Ensure setContentSensitivity was not called more than once upon re-registration.
|
|
expect(setContentSensitivityCall, 2);
|
|
},
|
|
);
|
|
}
|