Skip to content
This repository has been archived by the owner on May 15, 2024. It is now read-only.

GH-130 & GH-129: Android support for safe shareable file URI’s #416

Merged
merged 47 commits into from
Mar 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
d330940
Android: Support for safe shareable file URI’s
Redth Jul 26, 2018
b377ce5
Fix absolute type naming
Redth Jul 26, 2018
6695cb9
Add a user interaction test for File Provider
Redth Jul 26, 2018
f40ce30
Fix vibration code
Redth Jul 26, 2018
3147070
Reorder using statements
Redth Jul 26, 2018
e2efa96
Fix test attribute
Redth Jul 26, 2018
ef1be9d
Get provider authority properly
Redth Jul 26, 2018
333d0a0
Added external storage permission
Redth Jul 26, 2018
5d2d142
Change file provider path
Redth Jul 26, 2018
4ba6735
Copy file into temp folder instead of file
Redth Jul 26, 2018
3281aaa
Resgen
Redth Jul 26, 2018
9413620
Merge branch 'master' into feature/android-file-provider
Redth Jul 26, 2018
036bbc1
Merge branch 'master' into feature/android-file-provider
Redth Aug 15, 2018
1f6d48d
Merge branch 'master' into feature/android-file-provider
Redth Aug 16, 2018
f3a22cc
Merge branch 'master' into feature/android-file-provider
mattleibow Sep 19, 2018
ec7ca41
Merge branch 'master' into feature/android-file-provider
jamesmontemagno Sep 19, 2018
5c5b3f2
Permissions may need to be checked to control functionality
mattleibow Sep 20, 2018
652c6a7
The Android FileProvider now can detect permissions
mattleibow Sep 20, 2018
8abd65e
Added support for email attachments
mattleibow Sep 20, 2018
f587716
Added attachments to the sample app
mattleibow Sep 20, 2018
8df5c98
Updated the docs with the new types
mattleibow Sep 20, 2018
494d2c6
Some fixes for iOS
mattleibow Sep 21, 2018
ba85b64
Merge branch 'dev/1.1.0' into feature/android-file-provider
mattleibow Dec 18, 2018
fbf0b8f
Fix the mdoc target
mattleibow Dec 18, 2018
8257a18
regen docs
mattleibow Dec 18, 2018
c4c2c96
remove the obsolete armeabi ABI
mattleibow Dec 18, 2018
5c7b54a
Reworked the file logic to try and use public folders first
mattleibow Dec 20, 2018
00a6a6f
Be more specific with the external storage permission name
mattleibow Dec 20, 2018
b557743
Added some more depth to the comments here
Redth Mar 12, 2019
a56c942
Unnecessary else
Redth Mar 12, 2019
b1ed40e
Added base file info class
Redth Mar 12, 2019
a6044b5
EmailAttachment now derives from FileBase
Redth Mar 12, 2019
3561832
Added File Sharing
Redth Mar 12, 2019
f71f741
Keep track of IStorageFile internally
Redth Mar 12, 2019
13f7896
Prefer internal IStorageFile in UWP
Redth Mar 12, 2019
6c6f1e1
Use attachment name properly in UWP
Redth Mar 12, 2019
267ec59
Add ctor to create from existing FileBase
Redth Mar 12, 2019
880ff39
Add ctors for FileBase
Redth Mar 12, 2019
14cea84
Add ctors for ShareFileRequest
Redth Mar 12, 2019
b8037f0
Merge branch 'dev/1.1.0' into feature/android-file-provider
mattleibow Mar 12, 2019
7ceee2e
We can't use N on pre-N platforms
mattleibow Mar 12, 2019
7fc76d6
Updated the docs
mattleibow Mar 12, 2019
4f4ad8f
Update some docs.
Redth Mar 12, 2019
cf38175
Bump
Redth Mar 12, 2019
4af2507
Gate Email/Share files with feature flags
Redth Mar 13, 2019
2f0c268
Add sample for ShareFileRequest
Redth Mar 13, 2019
e5c1671
Added test for share method in netstandard
Redth Mar 13, 2019
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
5 changes: 4 additions & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,7 @@

# Force bash scripts to always use lf line endings so that if a repo is accessed
# in Unix via a file share from Windows, the scripts will work.
*.sh text eol=lf
*.sh text eol=lf

# Force the docs to always use lf line endings
docs/**/*.xml text eol=lf
7 changes: 5 additions & 2 deletions DeviceTests/DeviceTests.Android/DeviceTests.Android.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
<WarningLevel>4</WarningLevel>
<AndroidManagedSymbols>true</AndroidManagedSymbols>
<AndroidUseSharedRuntime>false</AndroidUseSharedRuntime>
<AndroidSupportedAbis>armeabi;armeabi-v7a;x86;x86_64;arm64-v8a</AndroidSupportedAbis>
<AndroidSupportedAbis>armeabi-v7a;x86;x86_64;arm64-v8a</AndroidSupportedAbis>
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
<JavaMaximumHeapSize>1G</JavaMaximumHeapSize>
<AndroidLinkSkip />
Expand All @@ -52,7 +52,7 @@
<WarningLevel>4</WarningLevel>
<AndroidManagedSymbols>true</AndroidManagedSymbols>
<AndroidUseSharedRuntime>false</AndroidUseSharedRuntime>
<AndroidSupportedAbis>armeabi;armeabi-v7a;x86;x86_64;arm64-v8a</AndroidSupportedAbis>
<AndroidSupportedAbis>armeabi-v7a;x86;x86_64;arm64-v8a</AndroidSupportedAbis>
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
<JavaMaximumHeapSize>1G</JavaMaximumHeapSize>
<AndroidLinkSkip />
Expand Down Expand Up @@ -103,6 +103,9 @@
<Compile Include="Resources\Resource.Designer.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<Compile Include="Tests\**\*.cs" />
Copy link
Contributor

Choose a reason for hiding this comment

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

Android has a set of this-platform-only tests, so we just add it to the app

</ItemGroup>
<ItemGroup>
<None Include="Properties\AndroidManifest.xml" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@
<uses-permission android:name="android.permission.FLASHLIGHT" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:theme="@style/MainTheme"></application>
</manifest>
3,910 changes: 2,014 additions & 1,896 deletions DeviceTests/DeviceTests.Android/Resources/Resource.designer.cs

Large diffs are not rendered by default.

238 changes: 238 additions & 0 deletions DeviceTests/DeviceTests.Android/Tests/FileProvider_Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
using System;
using System.IO;
using System.Linq;
using Xamarin.Essentials;
using Xunit;
using AndroidEnvironment = Android.OS.Environment;

namespace DeviceTests.Shared
{
public class Android_FileProvider_Tests
{
[Fact]
[Trait(Traits.InteractionType, Traits.InteractionTypes.Human)]
public void Share_Simple_Text_File_Test()
{
// Save a local cache data directory file
var file = CreateFile(FileSystem.AppDataDirectory, "share-test.txt");

// Make sure it is where we expect it to be
Assert.False(FileProvider.IsFileInPublicLocation(file));

// Actually get a safe shareable file uri
var shareableUri = Platform.GetShareableFileUri(file);

// Launch an intent to let tye user pick where to open this content
var intent = new Android.Content.Intent(Android.Content.Intent.ActionSend);
intent.SetType("text/plain");
intent.PutExtra(Android.Content.Intent.ExtraStream, shareableUri);
intent.PutExtra(Android.Content.Intent.ExtraTitle, "Title Here");
intent.SetFlags(Android.Content.ActivityFlags.GrantReadUriPermission);

var intentChooser = Android.Content.Intent.CreateChooser(intent, "Pick something");

Platform.AppContext.StartActivity(intentChooser);
}

[Theory]
[InlineData(true, FileProviderLocation.Internal)]
[InlineData(true, FileProviderLocation.PreferExternal)]
[InlineData(false, FileProviderLocation.Internal)]
[InlineData(false, FileProviderLocation.PreferExternal)]
public void Get_Shareable_Uri(bool failAccess, FileProviderLocation location)
{
// Always fail to simulate unmounted media
FileProvider.AlwaysFailExternalMediaAccess = failAccess;

try
{
// Save a local cache data directory file
var file = CreateFile(FileSystem.AppDataDirectory);

// Make sure it is where we expect it to be
Assert.False(FileProvider.IsFileInPublicLocation(file));

// Actually get a safe shareable file uri
var shareableUri = GetShareableUri(file, location);

// Determine where the file should be found
var isInternal = (failAccess || location == FileProviderLocation.Internal);
var expectedCache = isInternal ? "internal_cache" : "external_cache";
var expectedCacheDir = isInternal
? Platform.AppContext.CacheDir.AbsolutePath
: Platform.AppContext.ExternalCacheDir.AbsolutePath;

// Make sure the uri is what we expected
Assert.NotNull(shareableUri);
Assert.Equal("content", shareableUri.Scheme);
Assert.Equal("com.xamarin.essentials.devicetests.fileProvider", shareableUri.Authority);
Assert.Equal(4, shareableUri.PathSegments.Count);
Assert.Equal(expectedCache, shareableUri.PathSegments[0]);
Assert.Equal("2203693cc04e0be7f4f024d5f9499e13", shareableUri.PathSegments[1]);
Assert.True(Guid.TryParseExact(shareableUri.PathSegments[2], "N", out var guid));
Assert.Equal(Path.GetFileName(file), shareableUri.PathSegments[3]);

// Make sure the underlying file exists
var realPath = Path.Combine(shareableUri.PathSegments.ToArray())
.Replace(expectedCache, expectedCacheDir);
Assert.True(File.Exists(realPath));
}
finally
{
FileProvider.AlwaysFailExternalMediaAccess = false;
}
}

[Fact]
public void No_Media_Fails_Get_External_Cache_Shareable_Uri()
{
// Always fail to simulate unmounted media
FileProvider.AlwaysFailExternalMediaAccess = true;

try
{
// Save a local cache data directory file
var file = CreateFile(FileSystem.AppDataDirectory);

// Make sure it is where we expect it to be
Assert.False(FileProvider.IsFileInPublicLocation(file));

// try get a uri, but fail as there is no external storage
Assert.Throws<InvalidOperationException>(() => GetShareableUri(file, FileProviderLocation.External));
}
finally
{
FileProvider.AlwaysFailExternalMediaAccess = false;
}
}

[Fact]
public void Get_External_Cache_Shareable_Uri()
{
// Save a local cache data directory file
var file = CreateFile(FileSystem.AppDataDirectory);

// Make sure it is where we expect it to be
Assert.False(FileProvider.IsFileInPublicLocation(file));

// Actually get a safe shareable file uri
var shareableUri = GetShareableUri(file, FileProviderLocation.External);

// Make sure the uri is what we expected
Assert.NotNull(shareableUri);
Assert.Equal("content", shareableUri.Scheme);
Assert.Equal("com.xamarin.essentials.devicetests.fileProvider", shareableUri.Authority);
Assert.Equal(4, shareableUri.PathSegments.Count);
Assert.Equal("external_cache", shareableUri.PathSegments[0]);
Assert.Equal("2203693cc04e0be7f4f024d5f9499e13", shareableUri.PathSegments[1]);
Assert.True(Guid.TryParseExact(shareableUri.PathSegments[2], "N", out var guid));
Assert.Equal(Path.GetFileName(file), shareableUri.PathSegments[3]);

// Make sure the underlying file exists
var realPath = Path.Combine(shareableUri.PathSegments.ToArray())
.Replace("external_cache", Platform.AppContext.ExternalCacheDir.AbsolutePath);
Assert.True(File.Exists(realPath));
}

[Theory]
[InlineData(FileProviderLocation.External)]
[InlineData(FileProviderLocation.Internal)]
[InlineData(FileProviderLocation.PreferExternal)]
public void Get_Existing_Internal_Cache_Shareable_Uri(FileProviderLocation location)
{
// Save a local cache directory file
var file = CreateFile(Platform.AppContext.CacheDir.AbsolutePath);

// Make sure it is where we expect it to be
Assert.True(FileProvider.IsFileInPublicLocation(file));

// Actually get a safe shareable file uri
var shareableUri = GetShareableUri(file, location);

// Make sure the uri is what we expected
Assert.NotNull(shareableUri);
Assert.Equal("content", shareableUri.Scheme);
Assert.Equal("com.xamarin.essentials.devicetests.fileProvider", shareableUri.Authority);
Assert.Equal(new[] { "internal_cache", Path.GetFileName(file) }, shareableUri.PathSegments);
}

[Theory]
[InlineData(FileProviderLocation.External)]
[InlineData(FileProviderLocation.Internal)]
[InlineData(FileProviderLocation.PreferExternal)]
public void Get_Existing_External_Cache_Shareable_Uri(FileProviderLocation location)
{
// Save an external cache directory file
var file = CreateFile(Platform.AppContext.ExternalCacheDir.AbsolutePath);

// Make sure it is where we expect it to be
Assert.True(FileProvider.IsFileInPublicLocation(file));

// Actually get a safe shareable file uri
var shareableUri = GetShareableUri(file, location);

// Make sure the uri is what we expected
Assert.NotNull(shareableUri);
Assert.Equal("content", shareableUri.Scheme);
Assert.Equal("com.xamarin.essentials.devicetests.fileProvider", shareableUri.Authority);
Assert.Equal(new[] { "external_cache", Path.GetFileName(file) }, shareableUri.PathSegments);
}

[Theory]
[InlineData(FileProviderLocation.External)]
[InlineData(FileProviderLocation.Internal)]
[InlineData(FileProviderLocation.PreferExternal)]
public void Get_Existing_External_Shareable_Uri(FileProviderLocation location)
{
// Save an external directory file
var externalRoot = AndroidEnvironment.ExternalStorageDirectory.AbsolutePath;
var root = Platform.AppContext.GetExternalFilesDir(null).AbsolutePath;
var file = CreateFile(root);

// Make sure it is where we expect it to be
Assert.True(FileProvider.IsFileInPublicLocation(file));

// Actually get a safe shareable file uri
var shareableUri = GetShareableUri(file, location);

// Make sure the uri is what we expected
Assert.NotNull(shareableUri);
Assert.Equal("content", shareableUri.Scheme);
Assert.Equal("com.xamarin.essentials.devicetests.fileProvider", shareableUri.Authority);

// replace the real root with the providers "root"
var segements = Path.Combine(root.Replace(externalRoot, "external_files"), Path.GetFileName(file));

Assert.Equal(segements.Split(Path.DirectorySeparatorChar), shareableUri.PathSegments);
}

static string CreateFile(string root, string name = "the-file.txt")
{
var file = Path.Combine(root, name);

if (File.Exists(file))
File.Delete(file);

File.WriteAllText(file, "The file contents.");

return file;
}

static Android.Net.Uri GetShareableUri(string file, FileProviderLocation location)
{
try
{
// use the specific location
FileProvider.TemporaryLocation = location;

// get the uri
return Platform.GetShareableFileUri(file);
}
finally
{
// reset the location
FileProvider.TemporaryLocation = FileProviderLocation.PreferExternal;
}
}
}
}
1 change: 1 addition & 0 deletions DeviceTests/DeviceTests.Shared/DeviceTests.Shared.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
<PackageReference Include="Xamarin.Android.Support.v7.CardView" Version="28.0.0.1" />
<PackageReference Include="Xamarin.Android.Support.v7.MediaRouter" Version="28.0.0.1" />
<PackageReference Include="Xamarin.Android.Support.CustomTabs" Version="28.0.0.1" />
<PackageReference Include="Xamarin.Android.Support.Compat" Version="28.0.0.1" />
<PackageReference Include="Xamarin.Android.Support.Core.Utils" Version="28.0.0.1" />
<Reference Include="System.Numerics" />
<Reference Include="System.Numerics.Vectors" />
Expand Down
28 changes: 27 additions & 1 deletion DeviceTests/DeviceTests.Shared/Email_Tests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Threading.Tasks;
using System;
using System.IO;
using System.Threading.Tasks;
using Xamarin.Essentials;
using Xunit;

Expand Down Expand Up @@ -31,5 +33,29 @@ public Task Compose_With_Message_Shows_New_Window()
return Email.ComposeAsync(email);
});
}

[Fact]
[Trait(Traits.InteractionType, Traits.InteractionTypes.Human)]
public Task Email_Attachments_are_Sent()
{
// Save a local cache data directory file
var file = Path.Combine(FileSystem.AppDataDirectory, "EmailTest.txt");
File.WriteAllText(file, "Attachment contents goes here...");

return Utils.OnMainThread(() =>
{
var email = new EmailMessage
{
Subject = "Hello World!",
Body =
"This is a greeting email." + Environment.NewLine +
"There should be an attachment attached.",
To = { "everyone@example.org" },
Attachments = { new EmailAttachment(file) }
};

return Email.ComposeAsync(email);
});
}
}
}
2 changes: 1 addition & 1 deletion DeviceTests/DeviceTests.iOS/Entitlements.plist
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<dict>
<key>keychain-access-groups</key>
<array>
<string>com.xamarin.essentials.devicetests</string>
<string>$(AppIdentifierPrefix)$(CFBundleIdentifier)</string>
mattleibow marked this conversation as resolved.
Show resolved Hide resolved
</array>
</dict>
</plist>
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,4 @@ Here are some frequently asked questions about Xamarin.Essentials, but be sure t
## License

Please see the [License](LICENSE).

Loading