Skip to content

Commit 3ba56a3

Browse files
author
Copilot CI
committed
Address Android activity review feedback
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d00747b7-96f3-4e7a-8dfb-e3a48db04b2d
1 parent 9dee565 commit 3ba56a3

4 files changed

Lines changed: 105 additions & 13 deletions

File tree

src/Controls/tests/TestCases.HostApp/Issues/Issue37706.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
namespace Maui.Controls.Sample.Issues;
2+
13
[Issue(
24
IssueTracker.Github,
35
37706,
@@ -33,7 +35,7 @@ public Issue37706()
3335
protected override bool OnBackButtonPressed()
3436
{
3537
_backPressCount++;
36-
_statusLabel.Text = $"OnBackButtonPressed called {_backPressCount} time";
38+
_statusLabel.Text = $"OnBackButtonPressed called {_backPressCount} {(_backPressCount == 1 ? "time" : "times")}";
3739
return true;
3840
}
3941
}

src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue37706.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public Issue37706(TestDevice device) : base(device)
1515

1616
[Test]
1717
[Category(UITestCategories.Navigation)]
18-
public void RootPageBackButtonOverrideIsInvokedOnce()
18+
public void RootPageBackButtonOverrideIsInvokedForEachPress()
1919
{
2020
App.WaitForElement("RootPageLabel");
2121

@@ -26,6 +26,14 @@ public void RootPageBackButtonOverrideIsInvokedOnce()
2626
Is.True,
2727
"OnBackButtonPressed should be called exactly once.");
2828
App.WaitForElement("RootPageLabel");
29+
30+
App.Back();
31+
32+
Assert.That(
33+
App.WaitForTextToBePresentInElement("BackButtonPressedStatus", "OnBackButtonPressed called 2 times"),
34+
Is.True,
35+
"OnBackButtonPressed should be called once for each back press.");
36+
App.WaitForElement("RootPageLabel");
2937
}
3038
}
3139
#endif

src/Essentials/src/Platform/ActivityForResultRequest.android.cs

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -178,13 +178,27 @@ internal void ActivityDestroyed(ComponentActivity componentActivity)
178178

179179
if (componentActivity.IsFinishing
180180
|| !_savedRequestOwners.TryGetValue(componentActivity, out _)
181-
|| componentActivity.GetSystemService(global::Android.Content.Context.ActivityService) is not global::Android.App.ActivityManager activityManager
182-
|| !IsTaskPresent(activityManager, componentActivity.TaskId))
181+
|| componentActivity.GetSystemService(global::Android.Content.Context.ActivityService) is not global::Android.App.ActivityManager activityManager)
183182
{
184183
CancelPendingRequest(componentActivity);
185184
return;
186185
}
187186

187+
try
188+
{
189+
if (!IsTaskPresent(activityManager, componentActivity.TaskId))
190+
{
191+
CancelPendingRequest(componentActivity);
192+
return;
193+
}
194+
}
195+
catch (global::Java.Lang.SecurityException ex)
196+
{
197+
if (_requestState.TrySetException(requestOwner, ex))
198+
StopTaskRemovalMonitor(requestOwner);
199+
return;
200+
}
201+
188202
// A saved owner can be adopted by a replacement activity, but task removal may
189203
// happen later without another callback for this already-destroyed instance.
190204
StartTaskRemovalMonitor(requestOwner, activityManager, componentActivity.TaskId);
@@ -202,34 +216,36 @@ void StartTaskRemovalMonitor(
202216
int taskId)
203217
{
204218
var cancellation = new System.Threading.CancellationTokenSource();
219+
var cancellationToken = cancellation.Token;
205220
if (!_taskRemovalMonitors.TryAdd(requestOwner, cancellation))
206221
{
207222
cancellation.Dispose();
208223
return;
209224
}
210225

211-
_ = MonitorTaskPresenceAsync(requestOwner, activityManager, taskId, cancellation);
226+
_ = MonitorTaskPresenceAsync(requestOwner, activityManager, taskId, cancellation, cancellationToken);
212227
}
213228

214229
async Task MonitorTaskPresenceAsync(
215230
string requestOwner,
216231
global::Android.App.ActivityManager activityManager,
217232
int taskId,
218-
System.Threading.CancellationTokenSource cancellation)
233+
System.Threading.CancellationTokenSource cancellation,
234+
System.Threading.CancellationToken cancellationToken)
219235
{
220236
try
221237
{
222238
while (_requestState.HasPendingRequest(requestOwner))
223239
{
224-
await Task.Delay(TaskPresencePollInterval, cancellation.Token).ConfigureAwait(false);
240+
await Task.Delay(TaskPresencePollInterval, cancellationToken).ConfigureAwait(false);
225241
if (!IsTaskPresent(activityManager, taskId))
226242
{
227243
_requestState.TrySetCanceled(requestOwner);
228244
break;
229245
}
230246
}
231247
}
232-
catch (OperationCanceledException) when (cancellation.IsCancellationRequested)
248+
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
233249
{
234250
}
235251
catch (global::Java.Lang.SecurityException ex)
@@ -238,21 +254,30 @@ async Task MonitorTaskPresenceAsync(
238254
}
239255
finally
240256
{
241-
_taskRemovalMonitors.TryRemove(
242-
new KeyValuePair<string, System.Threading.CancellationTokenSource>(requestOwner, cancellation));
243-
cancellation.Dispose();
257+
if (_taskRemovalMonitors.TryRemove(
258+
new KeyValuePair<string, System.Threading.CancellationTokenSource>(requestOwner, cancellation)))
259+
{
260+
cancellation.Dispose();
261+
}
244262
}
245263
}
246264

247265
void StopTaskRemovalMonitor(string requestOwner)
248266
{
249267
if (_taskRemovalMonitors.TryRemove(requestOwner, out var cancellation))
250268
{
251-
cancellation.Cancel();
269+
try
270+
{
271+
cancellation.Cancel();
272+
}
273+
finally
274+
{
275+
cancellation.Dispose();
276+
}
252277
}
253278
}
254279

255-
static bool IsTaskPresent(global::Android.App.ActivityManager activityManager, int taskId)
280+
protected virtual bool IsTaskPresent(global::Android.App.ActivityManager activityManager, int taskId)
256281
{
257282
foreach (var appTask in activityManager.AppTasks ?? [])
258283
{

src/Essentials/test/DeviceTests/Tests/ActivityStateManagerRecreation_Tests.Android.cs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,56 @@ await MainThread.InvokeOnMainThreadAsync(() =>
8686
}
8787
}
8888

89+
[Fact]
90+
public async Task PendingRequest_FaultsWhenTaskPresenceCheckIsDenied()
91+
{
92+
var request = new RecreationActivityForResultRequest();
93+
var manager = new ActivityStateManagerImplementation(request);
94+
ActivityResultRecreationState.Begin(manager);
95+
96+
ActivityResultRecreationActivity? activity = null;
97+
98+
try
99+
{
100+
var hostActivity = MauiPlatform.CurrentActivity
101+
?? throw new InvalidOperationException("The device-test host activity is unavailable.");
102+
103+
await MainThread.InvokeOnMainThreadAsync(() =>
104+
hostActivity.StartActivity(new Intent(hostActivity, typeof(ActivityResultRecreationActivity))));
105+
activity = await ActivityResultRecreationState.FirstActivity
106+
.WaitAsync(TimeSpan.FromSeconds(15));
107+
108+
Task<JavaString>? pendingResult = null;
109+
using var input = new JavaString("security-exception");
110+
using var savedState = new Bundle();
111+
await MainThread.InvokeOnMainThreadAsync(() =>
112+
{
113+
pendingResult = request.Launch(activity, input);
114+
request.SaveInstanceState(activity, savedState);
115+
request.DenyTaskPresenceCheck = true;
116+
request.ActivityDestroyed(activity);
117+
});
118+
var pending = pendingResult
119+
?? throw new InvalidOperationException("The activity-result request was not started.");
120+
121+
var exception = await Assert.ThrowsAsync<global::Java.Lang.SecurityException>(
122+
() => pending.WaitAsync(TimeSpan.FromSeconds(15)));
123+
Assert.Contains("task presence denied", exception.Message, StringComparison.Ordinal);
124+
}
125+
finally
126+
{
127+
activity ??= manager.GetCurrentActivity() as ActivityResultRecreationActivity;
128+
await MainThread.InvokeOnMainThreadAsync(() =>
129+
{
130+
if (activity is { IsDestroyed: false, IsFinishing: false })
131+
activity.Finish();
132+
});
133+
134+
manager.Dispose();
135+
ActivityResultRecreationState.Reset();
136+
}
137+
}
138+
89139
[Fact]
90140
public async Task PendingRequest_IsCanceledWhenOwningTaskIsRemoved()
91141
{
@@ -157,6 +207,8 @@ await MainThread.InvokeOnMainThreadAsync(() =>
157207
sealed class RecreationActivityForResultRequest
158208
: ActivityForResultRequest<RecreationActivityResultContract, JavaString>
159209
{
210+
internal bool DenyTaskPresenceCheck { get; set; }
211+
160212
protected override ActivityResultLauncher RegisterForActivityResult(
161213
ComponentActivity componentActivity,
162214
RecreationActivityResultContract contract,
@@ -165,6 +217,11 @@ protected override ActivityResultLauncher RegisterForActivityResult(
165217
var activity = Assert.IsType<ActivityResultRecreationActivity>(componentActivity);
166218
return activity.RegisterForActivityResult(contract, activity.ResultRegistry, callback);
167219
}
220+
221+
protected override bool IsTaskPresent(ActivityManager activityManager, int taskId) =>
222+
DenyTaskPresenceCheck
223+
? throw new global::Java.Lang.SecurityException("task presence denied")
224+
: base.IsTaskPresent(activityManager, taskId);
168225
}
169226

170227
sealed class RecreationActivityResultContract : ActivityResultContract

0 commit comments

Comments
 (0)