Skip to content
Open
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
@@ -1,22 +1,45 @@
package com.microsoft.maui;

import android.content.Context;
import android.content.res.Resources;
import android.content.pm.ApplicationInfo;
import android.content.res.TypedArray;
import android.os.Bundle;
import android.util.Log;

import androidx.activity.ComponentActivity;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;

import java.lang.reflect.Field;

/**
* Class for batching native method calls within the MauiAppCompatActivity implementation
*/
public class PlatformMauiAppCompatActivity {
private static final String TAG = "MauiAppCompat";

// These are AndroidX saved-instance-state keys. MAUI does not create the bundles stored under
// these keys; it only removes or preserves them before AppCompat restores saved state. AndroidX
// does not expose public constants for these values.
//
// ComponentActivity saves pending ActivityResultRegistry state here. Preserving this bundle
// lets AndroidX replay pending activity results after activity or process recreation.
private static final String ACTIVITY_RESULT_REGISTRY_KEY = "android:support:activity-result";
Comment thread
AdamEssenmacher marked this conversation as resolved.

// AndroidX FragmentManager saved-state key. MAUI removes this when fragment restore is
// disabled because restoring old fragments can conflict with MAUI's own navigation/window
// reconstruction.
private static final String SUPPORT_FRAGMENTS_KEY = "android:support:fragments";

// SavedStateRegistry's top-level bundle key. Older MAUI behavior removed this whole bundle to
// suppress fragment restore side effects, but that also discarded ActivityResultRegistry state.
private static final String SAVED_STATE_REGISTRY_KEY = "androidx.lifecycle.BundlableSavedStateRegistry.key";

private static boolean activityResultRegistryKeyChecked;

public static void onCreate(AppCompatActivity activity, Bundle savedInstanceState, boolean allowFragmentRestore, int splashAttr, int mauiTheme)
{
if (!allowFragmentRestore && savedInstanceState != null) {
savedInstanceState.remove("android:support:fragments");
savedInstanceState.remove("androidx.lifecycle.BundlableSavedStateRegistry.key");
warnIfActivityResultRegistryKeyChanged(activity);
removeFragmentRestoreState(savedInstanceState);
}

boolean mauiSplashAttrValue = false;
Expand All @@ -33,4 +56,54 @@ public static void onCreate(AppCompatActivity activity, Bundle savedInstanceStat
activity.setTheme(mauiTheme);
}
}

private static void removeFragmentRestoreState(Bundle savedInstanceState)
{
// First remove the direct fragment entry that may be present in the activity state.
savedInstanceState.remove(SUPPORT_FRAGMENTS_KEY);

Bundle savedStateRegistry = savedInstanceState.getBundle(SAVED_STATE_REGISTRY_KEY);
if (savedStateRegistry != null) {
// The saved-state registry is a shared AndroidX container. Extract the activity-result
// entry before removing the container so pending activity results are not lost with the
// fragment-related providers.
Bundle activityResultRegistryState = savedStateRegistry.getBundle(ACTIVITY_RESULT_REGISTRY_KEY);

savedInstanceState.remove(SAVED_STATE_REGISTRY_KEY);

if (activityResultRegistryState != null) {
// Keep only the AndroidX ActivityResultRegistry state needed to replay pending
// results after activity/process recreation. Other saved-state providers may
// contain fragment state that MAUI cannot safely restore.
Bundle prunedSavedStateRegistry = new Bundle();
prunedSavedStateRegistry.putBundle(ACTIVITY_RESULT_REGISTRY_KEY, activityResultRegistryState);
savedInstanceState.putBundle(SAVED_STATE_REGISTRY_KEY, prunedSavedStateRegistry);
}
}
}

private static void warnIfActivityResultRegistryKeyChanged(AppCompatActivity activity)
{
if (activityResultRegistryKeyChecked) {
return;
}

activityResultRegistryKeyChecked = true;

if ((activity.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) == 0) {
return;
}

try {
Field field = ComponentActivity.class.getDeclaredField("ACTIVITY_RESULT_TAG");
field.setAccessible(true);
Object value = field.get(null);

if (!ACTIVITY_RESULT_REGISTRY_KEY.equals(value)) {
Log.w(TAG, "AndroidX ActivityResultRegistry saved-state key changed; MediaPicker recovery may be affected.");
}
} catch (Throwable ex) {
Log.w(TAG, "Unable to verify AndroidX ActivityResultRegistry saved-state key.", ex);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using AndroidX.Activity;
using Xunit;
using JavaClass = Java.Lang.Class;

namespace Microsoft.Maui.DeviceTests
{
[Category(TestCategory.Application)]
public class AndroidXActivityResultRegistryTests
{
const string ExpectedActivityResultRegistryKey = "android:support:activity-result";

[Fact]
public void ComponentActivity_ActivityResultSavedStateKey_MatchesMauiRecoveryKey()
{
using var componentActivityClass = JavaClass.FromType(typeof(ComponentActivity));
using var activityResultTagField = componentActivityClass.GetDeclaredField("ACTIVITY_RESULT_TAG");
activityResultTagField.Accessible = true;

var activityResultTag = activityResultTagField.Get(null)?.ToString();

Assert.Equal(ExpectedActivityResultRegistryKey, activityResultTag);
}
}
}
12 changes: 12 additions & 0 deletions src/Essentials/src/FileSystem/FileSystemUtils.android.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using Android.App;
using Android.Provider;
using Android.Webkit;
Expand Down Expand Up @@ -71,6 +72,17 @@ public static string EnsurePhysicalPath(AndroidUri uri, bool requireExtendedAcce
throw new FileNotFoundException($"Unable to resolve absolute path or retrieve contents of URI '{uri}'.");
}

public static Task<string> EnsurePhysicalPathAsync(AndroidUri uri, bool requireExtendedAccess = true)
{
// file:// URIs do not need provider queries or stream copies.
if (string.Equals(uri.Scheme, UriSchemeFile, StringComparison.OrdinalIgnoreCase))
{
return Task.FromResult(uri.Path);
}

return Task.Run(() => EnsurePhysicalPath(uri, requireExtendedAccess));
}

static string ResolvePhysicalPath(AndroidUri uri, bool requireExtendedAccess = true)
{
if (uri.Scheme.Equals(UriSchemeFile, StringComparison.OrdinalIgnoreCase))
Expand Down
Loading
Loading