1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:math' show max;
6
7import 'package:flutter/services.dart'
8 show ContentSensitivity, PlatformException, SensitiveContentService;
9
10import '../foundation/assertions.dart' show FlutterErrorDetails;
11import 'async.dart' show AsyncSnapshot, ConnectionState, FutureBuilder;
12import 'basic.dart' show SizedBox;
13import 'framework.dart';
14
15/// Data structure used to track the [SensitiveContent] widgets in the
16/// widget tree.
17class _ContentSensitivitySetting {
18 /// Creates a [_ContentSensitivitySetting].
19 _ContentSensitivitySetting();
20
21 /// The number of [SensitiveContent] widgets that have sensitivity [ContentSensitivity.sensitive].
22 int _sensitiveWidgetCount = 0;
23
24 /// The number of [SensitiveContent] widgets that have sensitivity [ContentSensitivity.autoSensitive].
25 int _autoSensitiveWidgetCount = 0;
26
27 /// The number of [SensitiveContent] widgets that have sensitivity [ContentSensitivity.notSensitive].
28 int _notSensitiveWigetCount = 0;
29
30 static void _reportUnknownContentSensitivityDetected(ContentSensitivity sensitivity) {
31 FlutterError.reportError(
32 FlutterErrorDetails(
33 exception: FlutterError(
34 'SensitiveContent widgets with ContentSensitivity $sensitivity is unsupported by _ContentSensitivitySetting',
35 ),
36 library: 'widget library',
37 stack: StackTrace.current,
38 ),
39 );
40 }
41
42 /// Increases the count of [SensitiveContent] widgets with [sensitivity] set.
43 void addWidgetWithContentSensitivity(ContentSensitivity sensitivity) {
44 switch (sensitivity) {
45 case ContentSensitivity.sensitive:
46 _sensitiveWidgetCount++;
47 case ContentSensitivity.autoSensitive:
48 _autoSensitiveWidgetCount++;
49 case ContentSensitivity.notSensitive:
50 _notSensitiveWigetCount++;
51 // ignore is safe because it protects this setting from tracking SensitiveContent
52 // widgets with an _unknown ContentSensitivity. _unknown is private to avoid
53 // developers using it as a SensitiveContent sensitivity.
54 // ignore: no_default_cases
55 default:
56 _reportUnknownContentSensitivityDetected(sensitivity);
57 }
58 }
59
60 static String _getNegativeWidgetCountErrorMessage(ContentSensitivity sensitivity, int count) {
61 return 'A negative amount ($count) of $sensitivity SensitiveContent widgets have been detected, which is not expected. Please file an issue.';
62 }
63
64 /// Decreases the count of [SensitiveContent] widgets with [sensitivity] set.
65 void removeWidgetWithContentSensitivity(ContentSensitivity sensitivity) {
66 switch (sensitivity) {
67 case ContentSensitivity.sensitive:
68 _sensitiveWidgetCount--;
69 assert(
70 _sensitiveWidgetCount >= 0,
71 _getNegativeWidgetCountErrorMessage(sensitivity, _sensitiveWidgetCount),
72 );
73 case ContentSensitivity.autoSensitive:
74 _autoSensitiveWidgetCount--;
75 assert(
76 _autoSensitiveWidgetCount >= 0,
77 _getNegativeWidgetCountErrorMessage(sensitivity, _autoSensitiveWidgetCount),
78 );
79 case ContentSensitivity.notSensitive:
80 _notSensitiveWigetCount--;
81 assert(
82 _notSensitiveWigetCount >= 0,
83 _getNegativeWidgetCountErrorMessage(sensitivity, _notSensitiveWigetCount),
84 );
85 // ignore is safe because it protects this setting from tracking SensitiveContent
86 // widgets with an _unknown ContentSensitivity. _unknown is private to avoid
87 // developers using it as a SensitiveContent sensitivity.
88 // ignore: no_default_cases
89 default:
90 _reportUnknownContentSensitivityDetected(sensitivity);
91 }
92 }
93
94 /// Returns true if this class is currently tracking at least one [SensitiveContent] widget.
95 bool get hasWidgets =>
96 max(0, _sensitiveWidgetCount) +
97 max(0, _autoSensitiveWidgetCount) +
98 max(0, _notSensitiveWigetCount) >
99 0;
100
101 /// Returns the highest prioritized [ContentSensitivity] of the [SensitiveContent] widgets
102 /// that this setting tracks.
103 ContentSensitivity? get contentSensitivityBasedOnWidgetCounts {
104 if (_sensitiveWidgetCount > 0) {
105 return ContentSensitivity.sensitive;
106 }
107 if (_autoSensitiveWidgetCount > 0) {
108 return ContentSensitivity.autoSensitive;
109 }
110 if (_notSensitiveWigetCount > 0) {
111 return ContentSensitivity.notSensitive;
112 }
113 return null;
114 }
115}
116
117/// Host of the current content sensitivity for the widget tree that contains
118/// some number [SensitiveContent] widgets.
119///
120/// This is not ready for production.
121// TODO(camsim99): Fix `SensitiveContent` implementation to prevent revealing sensitive
122// content during media projection. Then, export this file to make the widget available
123// for use. See https://github.com/flutter/flutter/issues/160050 and
124// https://github.com/flutter/flutter/issues/164820.
125@visibleForTesting
126class SensitiveContentHost {
127 SensitiveContentHost._();
128
129 bool? _contentSenstivityIsSupported;
130 late final _ContentSensitivitySetting _contentSensitivitySetting = _ContentSensitivitySetting();
131 ContentSensitivity? _fallbackContentSensitivitySetting;
132
133 final SensitiveContentService _sensitiveContentService = SensitiveContentService();
134
135 /// [SensitiveContentHost] instance for the widget tree.
136 @visibleForTesting
137 static final SensitiveContentHost instance = SensitiveContentHost._();
138
139 /// Returns the current content sensitivity as tracked by [_contentSensitivitySetting].
140 @visibleForTesting
141 ContentSensitivity? get calculatedContentSensitivity =>
142 _contentSensitivitySetting.contentSensitivityBasedOnWidgetCounts;
143
144 /// Registers a [SensitiveContent] widget that will help determine the
145 /// [ContentSensitivity] for the widget tree.
146 static Future<void> register(ContentSensitivity desiredSensitivity) {
147 return instance._register(desiredSensitivity);
148 }
149
150 Future<void> _register(ContentSensitivity desiredSensitivity) async {
151 try {
152 _contentSenstivityIsSupported ??= await _sensitiveContentService.isSupported();
153 } on PlatformException catch (e) {
154 _contentSenstivityIsSupported = false;
155 FlutterError.reportError(
156 FlutterErrorDetails(
157 exception: FlutterError(
158 'Call to check if setting content sensitivity is supported on the current platform failed unexpectedly, so it is assumed to be unsupported: $e}',
159 ),
160 library: 'widget library',
161 stack: e.stacktrace == null ? StackTrace.current : StackTrace.fromString(e.stacktrace!),
162 ),
163 );
164 }
165 if (!_contentSenstivityIsSupported!) {
166 // Setting content sensitivity is not supported on this device.
167 return;
168 }
169 // When the first `SensitiveContent` widget is registered, determine the content sensitivity
170 // we should fallback to if/when no `SensitiveContent` widgets remain in the tree.
171 // For Android API 35, this will be auto sensitive if it is otherwise unset by the developer.
172 if (_fallbackContentSensitivitySetting == null) {
173 try {
174 _fallbackContentSensitivitySetting = await _sensitiveContentService.getContentSensitivity();
175 } on UnsupportedError catch (e) {
176 // Unknown ContentSensitivity detected; fallback to not sensitive mode since we
177 // cannot determine the desired behavior of the current mode and log error to user.
178 _fallbackContentSensitivitySetting = ContentSensitivity.notSensitive;
179 FlutterError.reportError(
180 FlutterErrorDetails(
181 exception: FlutterError(
182 'Unknown content sensitivity set in the Android embedding or by default: $e}',
183 ),
184 library: 'widget library',
185 stack: e.stackTrace,
186 ),
187 );
188 }
189 }
190
191 // Check current calculated content sensitivity (or the fallback if it has not been set yet).
192 // If this sensitivity is different from the one that accounts for the newly registered
193 // desiredSensitivity, then update the sensitivity on the platform side.
194 final ContentSensitivity? contentSensitivityBasedOnWidgetCountsBeforeRegister =
195 _contentSensitivitySetting.contentSensitivityBasedOnWidgetCounts ??
196 _fallbackContentSensitivitySetting;
197
198 // Update content sensitivity setting to account for adding the desiredSensitivity SensitiveContent
199 // widget to the tree.
200 _contentSensitivitySetting.addWidgetWithContentSensitivity(desiredSensitivity);
201
202 // Verify that desiredSensitivity should be set in order for sensitive
203 // content to remain obscured.
204 if (contentSensitivityBasedOnWidgetCountsBeforeRegister ==
205 _contentSensitivitySetting.contentSensitivityBasedOnWidgetCounts) {
206 return;
207 }
208
209 // Set content sensitivity as desiredSensitivity.
210 try {
211 await _sensitiveContentService.setContentSensitivity(
212 _contentSensitivitySetting.contentSensitivityBasedOnWidgetCounts!,
213 );
214 } on PlatformException catch (e) {
215 FlutterError.reportError(
216 FlutterErrorDetails(
217 exception: FlutterError('Attempt to set $desiredSensitivity sensitivity failed: $e}'),
218 library: 'widget library',
219 stack: e.stacktrace == null ? StackTrace.current : StackTrace.fromString(e.stacktrace!),
220 ),
221 );
222 }
223 }
224
225 /// Unregisters a [SensitiveContent] widget from the [_ContentSensitivitySetting] tracking
226 /// the content sensitivity of the widget tree.
227 static Future<void> unregister(ContentSensitivity widgetSensitivity) async {
228 return instance._unregister(widgetSensitivity);
229 }
230
231 Future<void> _unregister(ContentSensitivity widgetSensitivity) async {
232 assert(
233 _contentSenstivityIsSupported != null,
234 'SensitiveContentHost.register must be called before SensitiveContentHost.unregister',
235 );
236
237 if (!_contentSenstivityIsSupported!) {
238 // Setting content sensitivity is not supported on this device.
239 return;
240 }
241
242 // Check current calculated content sensitivity. Use this to determine whether or not
243 // a new content sensitivity needs to be set based on which sensitiivty needs to be
244 // restored to accurately reflect the SensitiveContent widgets in the tree.
245 final ContentSensitivity contentSensitivityBasedOnWidgetCountsBeforeUnregister =
246 _contentSensitivitySetting.contentSensitivityBasedOnWidgetCounts!;
247
248 // Update the content sensitivity estting to account for removing a SensitiveContent
249 // widget with sensitivity widgetSensitivity from the tree.
250 _contentSensitivitySetting.removeWidgetWithContentSensitivity(widgetSensitivity);
251
252 if (!_contentSensitivitySetting.hasWidgets) {
253 // Restore fallback content sensitivity setting if there are no more SensitiveContent
254 // widgets in the tree.
255 if (contentSensitivityBasedOnWidgetCountsBeforeUnregister ==
256 _fallbackContentSensitivitySetting) {
257 return;
258 }
259
260 try {
261 await _sensitiveContentService.setContentSensitivity(_fallbackContentSensitivitySetting!);
262 } on PlatformException catch (e) {
263 FlutterError.reportError(
264 FlutterErrorDetails(
265 exception: FlutterError(
266 'Attempted to set $_fallbackContentSensitivitySetting sensitivity failed: $e}',
267 ),
268 library: 'widget library',
269 stack: e.stacktrace == null ? StackTrace.current : StackTrace.fromString(e.stacktrace!),
270 ),
271 );
272 }
273 return;
274 }
275
276 // Determine if another sensitivity needs to be restored. The null check should be
277 // safe because contentSensitivityBasedOnWidgetCounts should always be non-null as long
278 // as there are still SensitiveContent widgets in the tree.
279 final ContentSensitivity contentSensitivityToRestore =
280 _contentSensitivitySetting.contentSensitivityBasedOnWidgetCounts!;
281 if (contentSensitivityToRestore != contentSensitivityBasedOnWidgetCountsBeforeUnregister) {
282 // Set content sensitivity as contentSensitivityToRestore.
283 try {
284 await _sensitiveContentService.setContentSensitivity(contentSensitivityToRestore);
285 } on PlatformException catch (e) {
286 FlutterError.reportError(
287 FlutterErrorDetails(
288 exception: FlutterError(
289 'Attempted to set $_fallbackContentSensitivitySetting sensitivity failed: $e}',
290 ),
291 library: 'widget library',
292 stack: e.stacktrace == null ? StackTrace.current : StackTrace.fromString(e.stacktrace!),
293 ),
294 );
295 }
296 }
297 }
298}
299
300/// Widget to set the [ContentSensitivity] of content in the widget
301/// tree.
302///
303/// The [sensitivity] of the widget in conjunction with the other
304/// [SensitiveContent] widgets in the tree will determine whether or not the
305/// screen will be obscured during media projection, e.g. screen sharing.
306///
307/// {@macro flutter.services.ContentSensitivity}
308///
309/// Currently, this widget is only supported on Android API 35+. On all lower Android
310/// versions and non-Android platforms, this does nothing; the screen will never be
311/// obscured regardless of the [sensitivity] set. To programmatically check if
312/// a device supports this widget, call [SensitiveContentService.isSupported].
313///
314/// It is possible for a frame to be projected before the screen is updated to match
315/// the widget's `sensitivityLevel`, potentially revealing sensitive information during
316/// that frame. For example, when navigating from a page with no `SensitiveContent` to a
317/// new page in an app using a `Navigator.of(context).pushReplacement` to push a new
318/// `PageRouteBuilder` with (1) a `pageBuilder` that includes a [SensitiveContent] widget
319/// with `sensitivity` [ContentSensitivity.sensitive] and (2)
320/// `transitionDuration: Duration.zero`, one frame showing the app content is projected
321/// before the screen is obscured. See https://github.com/flutter/flutter/issues/164820 for
322/// for a discussion on known vulnerabilities or to report encountered vulnerabilities.
323///
324/// See also:
325///
326/// * [ContentSensitivity], which are the different content sensitivity that a
327/// [SensitiveContent] widget can set.
328// TODO(camsim99): Fix `SensitiveContent` implementation to prevent revealing sensitive
329// content during media projection. See https://github.com/flutter/flutter/issues/160050
330// and https://github.com/flutter/flutter/issues/164820.
331class SensitiveContent extends StatefulWidget {
332 /// Creates a [SensitiveContent] widget.
333 const SensitiveContent({super.key, required this.sensitivity, required this.child});
334
335 /// The sensitivity that the [SensitiveContent] widget should sets for the
336 /// Android native `View` hosting the widget tree.
337 final ContentSensitivity sensitivity;
338
339 /// The child widget of this [SensitiveContent].
340 ///
341 /// If the [sensitivity] is set to [ContentSensitivity.sensitive], then
342 /// the entire screen will be obscured when the screen is projected irrespective
343 /// to the parent/child widgets.
344 ///
345 /// {@macro flutter.widgets.ProxyWidget.child}
346 final Widget child;
347
348 @override
349 State<SensitiveContent> createState() => _SensitiveContentState();
350}
351
352class _SensitiveContentState extends State<SensitiveContent> {
353 Future<void> _sensitiveContentRegistrationFuture = Future<void>.value();
354
355 @override
356 void initState() {
357 super.initState();
358 _sensitiveContentRegistrationFuture = SensitiveContentHost.register(widget.sensitivity);
359 }
360
361 @override
362 void dispose() {
363 SensitiveContentHost.unregister(widget.sensitivity);
364 super.dispose();
365 }
366
367 Future<void> _reregisterWidget(
368 ContentSensitivity oldSensitivity,
369 ContentSensitivity newSensitivty,
370 ) async {
371 SensitiveContentHost.register(newSensitivty);
372 SensitiveContentHost.unregister(oldSensitivity);
373 }
374
375 @override
376 void didUpdateWidget(SensitiveContent oldWidget) {
377 super.didUpdateWidget(oldWidget);
378
379 if (widget.sensitivity == oldWidget.sensitivity) {
380 return;
381 }
382
383 // Re-register SensitiveContent widget if the sensitivity changed.
384 _sensitiveContentRegistrationFuture = _reregisterWidget(
385 oldWidget.sensitivity,
386 widget.sensitivity,
387 );
388 }
389
390 @override
391 Widget build(BuildContext context) {
392 return FutureBuilder<void>(
393 future: _sensitiveContentRegistrationFuture,
394 builder: (BuildContext context, AsyncSnapshot<void> snapshot) {
395 if (snapshot.connectionState == ConnectionState.done) {
396 return widget.child;
397 }
398 return const SizedBox.shrink();
399 },
400 );
401 }
402}
403