implement GUI for NetFtpStorage

store user/password for ftp connection in "path" string for compatiblity with file chooser
This commit is contained in:
Philipp Crocoll 2016-11-18 03:24:15 +01:00
parent fe3da55e0d
commit 113d693f7a
9 changed files with 222 additions and 69 deletions

View File

@ -87,22 +87,45 @@ namespace keepass2android.Io
{ {
public FtpEncryptionMode EncryptionMode {get; set; } public FtpEncryptionMode EncryptionMode {get; set; }
public string Username
{
get;set;
}
public string Password
{
get;
set;
}
public static ConnectionSettings FromIoc(IOConnectionInfo ioc) public static ConnectionSettings FromIoc(IOConnectionInfo ioc)
{ {
string path = ioc.Path; string path = ioc.Path;
int schemeLength = path.IndexOf("://", StringComparison.Ordinal); int schemeLength = path.IndexOf("://", StringComparison.Ordinal);
path = path.Substring(schemeLength + 3); path = path.Substring(schemeLength + 3);
string settings = path.Substring(0, path.IndexOf("/", StringComparison.Ordinal)); string settings = path.Substring(0, path.IndexOf(SettingsPostFix, StringComparison.Ordinal));
if (!settings.StartsWith(SettingsPrefix))
throw new Exception("unexpected settings in path");
settings = settings.Substring(SettingsPrefix.Length);
var tokens = settings.Split(Separator);
return new ConnectionSettings() return new ConnectionSettings()
{ {
EncryptionMode = (FtpEncryptionMode) int.Parse(settings) EncryptionMode = (FtpEncryptionMode) int.Parse(tokens[2]),
Username = tokens[0],
Password = tokens[1]
}; };
} }
public string ToString() public const string SettingsPrefix = "SET";
public const string SettingsPostFix = "%";
public const char Separator = ':';
public override string ToString()
{ {
return ((int) EncryptionMode).ToString(); return SettingsPrefix +
System.Net.WebUtility.UrlEncode(Username) + Separator +
WebUtility.UrlEncode(Password) + Separator +
(int) EncryptionMode;
;
} }
} }
@ -120,7 +143,10 @@ namespace keepass2android.Io
public IEnumerable<string> SupportedProtocols public IEnumerable<string> SupportedProtocols
{ {
get { yield return "ftp"; } get
{
yield return "ftp";
}
} }
public void Delete(IOConnectionInfo ioc) public void Delete(IOConnectionInfo ioc)
@ -160,9 +186,11 @@ namespace keepass2android.Io
internal FtpClient GetClient(IOConnectionInfo ioc, bool enableCloneClient = true) internal FtpClient GetClient(IOConnectionInfo ioc, bool enableCloneClient = true)
{ {
var settings = ConnectionSettings.FromIoc(ioc);
FtpClient client = new RetryConnectFtpClient(); FtpClient client = new RetryConnectFtpClient();
if ((ioc.UserName.Length > 0) || (ioc.Password.Length > 0)) if ((settings.Username.Length > 0) || (settings.Password.Length > 0))
client.Credentials = new NetworkCredential(ioc.UserName, ioc.Password); client.Credentials = new NetworkCredential(settings.Username, settings.Password);
else else
client.Credentials = new NetworkCredential("anonymous", ""); //TODO TEST client.Credentials = new NetworkCredential("anonymous", ""); //TODO TEST
@ -176,7 +204,7 @@ namespace keepass2android.Io
args.Accept = _app.CertificateValidationCallback(control, args.Certificate, args.Chain, args.PolicyErrors); args.Accept = _app.CertificateValidationCallback(control, args.Certificate, args.Chain, args.PolicyErrors);
}; };
client.EncryptionMode = ConnectionSettings.FromIoc(ioc).EncryptionMode; client.EncryptionMode = settings.EncryptionMode;
client.Connect(); client.Connect();
return client; return client;
@ -192,7 +220,7 @@ namespace keepass2android.Io
int schemeLength = path.IndexOf("://", StringComparison.Ordinal); int schemeLength = path.IndexOf("://", StringComparison.Ordinal);
string scheme = path.Substring(0, schemeLength); string scheme = path.Substring(0, schemeLength);
path = path.Substring(schemeLength + 3); path = path.Substring(schemeLength + 3);
string settings = path.Substring(0, path.IndexOf("/", StringComparison.Ordinal)); string settings = path.Substring(0, path.IndexOf(ConnectionSettings.SettingsPostFix, StringComparison.Ordinal));
path = path.Substring(settings.Length + 1); path = path.Substring(settings.Length + 1);
return new Uri(scheme + "://" + path); return new Uri(scheme + "://" + path);
} }
@ -203,10 +231,10 @@ namespace keepass2android.Io
int schemeLength = basePath.IndexOf("://", StringComparison.Ordinal); int schemeLength = basePath.IndexOf("://", StringComparison.Ordinal);
string scheme = basePath.Substring(0, schemeLength); string scheme = basePath.Substring(0, schemeLength);
basePath = basePath.Substring(schemeLength + 3); basePath = basePath.Substring(schemeLength + 3);
string baseSettings = basePath.Substring(0, basePath.IndexOf("/", StringComparison.Ordinal)); string baseSettings = basePath.Substring(0, basePath.IndexOf(ConnectionSettings.SettingsPostFix, StringComparison.Ordinal));
basePath = basePath.Substring(baseSettings.Length+1); basePath = basePath.Substring(baseSettings.Length+1);
string baseHost = basePath.Substring(0, basePath.IndexOf("/", StringComparison.Ordinal)); string baseHost = basePath.Substring(0, basePath.IndexOf("/", StringComparison.Ordinal));
return scheme + "://" + baseSettings + "/" + baseHost + uri.AbsolutePath; //TODO does this contain Query? return scheme + "://" + baseSettings + ConnectionSettings.SettingsPostFix + baseHost + uri.AbsolutePath; //TODO does this contain Query?
} }
@ -261,7 +289,7 @@ namespace keepass2android.Io
public bool RequiresCredentials(IOConnectionInfo ioc) public bool RequiresCredentials(IOConnectionInfo ioc)
{ {
return ioc.CredSaveMode != IOCredSaveMode.SaveCred; return false;
} }
public void CreateDirectory(IOConnectionInfo ioc, string newDirName) public void CreateDirectory(IOConnectionInfo ioc, string newDirName)
@ -340,16 +368,19 @@ namespace keepass2android.Io
var uri = IocPathToUri(ioc.Path); var uri = IocPathToUri(ioc.Path);
string path = uri.PathAndQuery; string path = uri.PathAndQuery;
return new FileDescription() if (!client.FileExists(path) && (!client.DirectoryExists(path)))
throw new FileNotFoundException();
var fileDesc = new FileDescription()
{ {
CanRead = true, CanRead = true,
CanWrite = true, CanWrite = true,
Path = ioc.Path, Path = ioc.Path,
LastModified = client.GetModifiedTime(path), LastModified = client.GetModifiedTime(path),
SizeInBytes = client.GetFileSize(path), SizeInBytes = client.GetFileSize(path),
DisplayName = UrlUtil.GetFileName(path), DisplayName = UrlUtil.GetFileName(path)
IsDirectory = false
}; };
fileDesc.IsDirectory = fileDesc.Path.EndsWith("/");
return fileDesc;
} }
} }
catch (FtpCommandException ex) catch (FtpCommandException ex)
@ -457,6 +488,34 @@ namespace keepass2android.Io
throw ConvertException(ex); throw ConvertException(ex);
} }
} }
public static int GetDefaultPort(FtpEncryptionMode encryption)
{
return new FtpClient() { EncryptionMode = encryption}.Port;
}
public string BuildFullPath(string host, int port, string initialPath, string user, string password, FtpEncryptionMode encryption)
{
var connectionSettings = new ConnectionSettings()
{
EncryptionMode = encryption,
Username = user,
Password = password
};
string scheme = "ftp";
string fullPath = scheme + "://" + connectionSettings.ToString() + ConnectionSettings.SettingsPostFix + host;
if (port != GetDefaultPort(encryption))
fullPath += ":" + port;
if (!initialPath.StartsWith("/"))
initialPath = "/" + initialPath;
fullPath += initialPath;
return fullPath;
}
} }
public class TransactedWrite : IWriteTransaction public class TransactedWrite : IWriteTransaction

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net.FtpClient;
using System.Text; using System.Text;
using Android.App; using Android.App;
@ -62,13 +63,49 @@ namespace keepass2android
#endif #endif
} }
private void ShowFtpDialog(Activity activity, Util.FileSelectedHandler onStartBrowse, Action onCancel)
{
#if !NoNet
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
View dlgContents = activity.LayoutInflater.Inflate(Resource.Layout.ftpcredentials, null);
builder.SetView(dlgContents);
builder.SetPositiveButton(Android.Resource.String.Ok,
(sender, args) =>
{
string host = dlgContents.FindViewById<EditText>(Resource.Id.ftp_host).Text;
string portText = dlgContents.FindViewById<EditText>(Resource.Id.ftp_port).Text;
FtpEncryptionMode encryption =
(FtpEncryptionMode) dlgContents.FindViewById<Spinner>(Resource.Id.ftp_encryption).SelectedItemPosition;
int port = NetFtpFileStorage.GetDefaultPort(encryption);
if (!string.IsNullOrEmpty(portText))
int.TryParse(portText, out port);
string user = dlgContents.FindViewById<EditText>(Resource.Id.ftp_user).Text;
string password = dlgContents.FindViewById<EditText>(Resource.Id.ftp_password).Text;
string initialPath = dlgContents.FindViewById<EditText>(Resource.Id.ftp_initial_dir).Text;
string ftpPath = new NetFtpFileStorage(_activity, App.Kp2a).BuildFullPath(host, port, initialPath, user,
password, encryption);
onStartBrowse(ftpPath);
});
EventHandler<DialogClickEventArgs> evtH = new EventHandler<DialogClickEventArgs>((sender, e) => onCancel());
builder.SetNegativeButton(Android.Resource.String.Cancel, evtH);
builder.SetTitle(activity.GetString(Resource.String.enter_sftp_login_title));
Dialog dialog = builder.Create();
dialog.Show();
#endif
}
public void PerformManualFileSelect(string defaultPath) public void PerformManualFileSelect(string defaultPath)
{ {
if (defaultPath.StartsWith("sftp://")) if (defaultPath.StartsWith("sftp://"))
ShowSftpDialog(_activity, StartFileChooser, ReturnCancel); ShowSftpDialog(_activity, StartFileChooser, ReturnCancel);
else if ((defaultPath.StartsWith("ftp://")) || (defaultPath.StartsWith("ftps://")))
ShowFtpDialog(_activity, StartFileChooser, ReturnCancel);
else else
{ {
Func<string, Dialog, bool> onOpen = (filename, dialog) => OnOpenButton(filename, dialog); Func<string, Dialog, bool> onOpen = OnOpenButton;
Util.ShowFilenameDialog(_activity, Util.ShowFilenameDialog(_activity,
!_isForSave ? onOpen : null, !_isForSave ? onOpen : null,
_isForSave ? onOpen : null, _isForSave ? onOpen : null,
@ -202,7 +239,6 @@ namespace keepass2android
public bool StartFileChooser(string defaultPath) public bool StartFileChooser(string defaultPath)
{ {
#if !EXCLUDE_FILECHOOSER #if !EXCLUDE_FILECHOOSER
Kp2aLog.Log("FSA: defaultPath=" + defaultPath);
string fileProviderAuthority = FileChooserFileProvider.TheAuthority; string fileProviderAuthority = FileChooserFileProvider.TheAuthority;
if (defaultPath.StartsWith("file://")) if (defaultPath.StartsWith("file://"))
{ {

View File

@ -40,46 +40,46 @@ namespace keepass2android
private readonly FileStorageSelectionActivity _context; private readonly FileStorageSelectionActivity _context;
private readonly List<string> _protocolIds = new List<string>(); private readonly List<string> _displayedProtocolIds = new List<string>();
public FileStorageAdapter(FileStorageSelectionActivity context) public FileStorageAdapter(FileStorageSelectionActivity context)
{ {
_context = context; _context = context;
//show all supported protocols: //show all supported protocols:
foreach (IFileStorage fs in App.Kp2a.FileStorages) foreach (IFileStorage fs in App.Kp2a.FileStorages)
_protocolIds.AddRange(fs.SupportedProtocols); _displayedProtocolIds.AddRange(fs.SupportedProtocols);
//special handling for local files: //special handling for local files:
if (!Util.IsKitKatOrLater) if (!Util.IsKitKatOrLater)
{ {
//put file:// to the top //put file:// to the top
_protocolIds.Remove("file"); _displayedProtocolIds.Remove("file");
_protocolIds.Insert(0, "file"); _displayedProtocolIds.Insert(0, "file");
//remove "content" (covered by androidget) //remove "content" (covered by androidget)
//On KitKat, content is handled by AndroidContentStorage taking advantage //On KitKat, content is handled by AndroidContentStorage taking advantage
//of persistable permissions and ACTION_OPEN/CREATE_DOCUMENT //of persistable permissions and ACTION_OPEN/CREATE_DOCUMENT
_protocolIds.Remove("content"); _displayedProtocolIds.Remove("content");
} }
else else
{ {
_protocolIds.Remove("file"); _displayedProtocolIds.Remove("file");
} }
if (context.Intent.GetBooleanExtra(AllowThirdPartyAppGet, false)) if (context.Intent.GetBooleanExtra(AllowThirdPartyAppGet, false))
_protocolIds.Add("androidget"); _displayedProtocolIds.Add("androidget");
if (context.Intent.GetBooleanExtra(AllowThirdPartyAppSend, false)) if (context.Intent.GetBooleanExtra(AllowThirdPartyAppSend, false))
_protocolIds.Add("androidsend"); _displayedProtocolIds.Add("androidsend");
#if NoNet #if NoNet
_protocolIds.Add("kp2a"); _displayedProtocolIds.Add("kp2a");
#endif #endif
} }
public override Object GetItem(int position) public override Object GetItem(int position)
{ {
return _protocolIds[position]; return _displayedProtocolIds[position];
} }
public override long GetItemId(int position) public override long GetItemId(int position)
@ -121,7 +121,7 @@ namespace keepass2android
btn = (Button)convertView; btn = (Button)convertView;
} }
var protocolId = _protocolIds[position]; var protocolId = _displayedProtocolIds[position];
btn.Tag = protocolId; btn.Tag = protocolId;
@ -143,7 +143,7 @@ namespace keepass2android
public override int Count public override int Count
{ {
get { return _protocolIds.Count; } get { return _displayedProtocolIds.Count; }
} }
} }

View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_margin="12dip"
>
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/ftp_host"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:text="85.13.147.153"
android:layout_weight="1"
android:inputType="textNoSuggestions"
android:hint="@string/hint_sftp_host" />
<TextView
android:id="@+id/portsep"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=":" />
<EditText
android:id="@+id/ftp_port"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="4"
android:singleLine="true"
android:inputType="number"
android:text=""
android:hint="@string/hint_sftp_port" />
</LinearLayout>
<Spinner
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:entries="@array/ftp_encryption_modes"
android:id="@+id/ftp_encryption" />
<EditText
android:id="@+id/ftp_user"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:text="f00c530c"
android:hint="@string/hint_username" />
<EditText
android:id="@+id/ftp_password"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:text="2C4vsSt8MhNEaqv5"
android:singleLine="true"
android:hint="@string/hint_pass" />
<TextView android:id="@+id/initial_dir"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="4dip"
android:layout_marginTop="4dip"
android:text="@string/initial_directory" />
<EditText
android:id="@+id/ftp_initial_dir"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:text="/"
/>
</LinearLayout>

View File

@ -944,8 +944,12 @@ Initial public release
</string-array> </string-array>
<string name="design_title">Design</string> <string name="design_title">Design</string>
<string-array name="ftp_encryption_modes">
<string-array name="cred_remember_modes"> <item>No encryption (FTP)</item>
<item>Implicit encryption (FTP over TLS, FTPS)</item>
<item>Explicit encryption (FTP over TLS, FTPS)</item>
</string-array>
<string-array name="cred_remember_modes">
<item>Do not remember username and password</item> <item>Do not remember username and password</item>
<item>Remember username only</item> <item>Remember username only</item>
<item>Remember username and password</item> <item>Remember username and password</item>

View File

@ -319,36 +319,7 @@ namespace keepass2android
public delegate bool FileSelectedHandler(string filename); public delegate bool FileSelectedHandler(string filename);
public static void ShowSftpDialog(Activity activity, FileSelectedHandler onStartBrowse, Action onCancel)
{
#if !EXCLUDE_JAVAFILESTORAGE && !NoNet
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
View dlgContents = activity.LayoutInflater.Inflate(Resource.Layout.sftpcredentials, null);
builder.SetView(dlgContents);
builder.SetPositiveButton(Android.Resource.String.Ok,
(sender, args) =>
{
string host = dlgContents.FindViewById<EditText>(Resource.Id.sftp_host).Text;
string portText = dlgContents.FindViewById<EditText>(Resource.Id.sftp_port).Text;
int port = Keepass2android.Javafilestorage.SftpStorage.DefaultSftpPort;
if (!string.IsNullOrEmpty(portText))
int.TryParse(portText, out port);
string user = dlgContents.FindViewById<EditText>(Resource.Id.sftp_user).Text;
string password = dlgContents.FindViewById<EditText>(Resource.Id.sftp_password).Text;
string initialPath = dlgContents.FindViewById<EditText>(Resource.Id.sftp_initial_dir).Text;
string sftpPath = new Keepass2android.Javafilestorage.SftpStorage().BuildFullPath(host, port, initialPath, user,
password);
onStartBrowse(sftpPath);
});
EventHandler<DialogClickEventArgs> evtH = new EventHandler<DialogClickEventArgs>( (sender, e) => onCancel());
builder.SetNegativeButton(Android.Resource.String.Cancel, evtH);
builder.SetTitle(activity.GetString(Resource.String.enter_sftp_login_title));
Dialog dialog = builder.Create();
dialog.Show();
#endif
}
public class DismissListener: Java.Lang.Object, IDialogInterfaceOnDismissListener public class DismissListener: Java.Lang.Object, IDialogInterfaceOnDismissListener
{ {

View File

@ -511,7 +511,7 @@ namespace keepass2android
new GoogleDriveFileStorage(Application.Context, this), new GoogleDriveFileStorage(Application.Context, this),
new SkyDriveFileStorage(Application.Context, this), new SkyDriveFileStorage(Application.Context, this),
new SftpFileStorage(this), new SftpFileStorage(this),
new NetFtpFileStorage(Application.Context), new NetFtpFileStorage(Application.Context, this),
#endif #endif
#endif #endif
new LocalFileStorage(this) new LocalFileStorage(this)
@ -684,7 +684,7 @@ namespace keepass2android
public void ClearOfflineCache() public void ClearOfflineCache()
{ {
new CachingFileStorage(new BuiltInFileStorage(this), Application.Context.CacheDir.Path, this).ClearCache(); new CachingFileStorage(new LocalFileStorage(this), Application.Context.CacheDir.Path, this).ClearCache();
} }
public IFileStorage GetFileStorage(string protocolId) public IFileStorage GetFileStorage(string protocolId)
@ -700,7 +700,7 @@ namespace keepass2android
{ {
if (iocInfo.IsLocalFile()) if (iocInfo.IsLocalFile())
return new BuiltInFileStorage(this); return new LocalFileStorage(this);
else else
{ {
IFileStorage innerFileStorage = GetCloudFileStorage(iocInfo); IFileStorage innerFileStorage = GetCloudFileStorage(iocInfo);

View File

@ -66,7 +66,6 @@ namespace keepass2android
{ {
try try
{ {
Kp2aLog.Log("Provider.GetFileEntry " + filename);
return ConvertFileDescription(App.Kp2a.GetFileStorage(filename).GetFileDescription(ConvertPathToIoc(filename))); return ConvertFileDescription(App.Kp2a.GetFileStorage(filename).GetFileDescription(ConvertPathToIoc(filename)));
} }
catch (Exception e) catch (Exception e)
@ -80,14 +79,12 @@ namespace keepass2android
protected override void ListFiles(int taskId, string dirName, bool showHiddenFiles, int filterMode, int limit, string positiveRegex, protected override void ListFiles(int taskId, string dirName, bool showHiddenFiles, int filterMode, int limit, string positiveRegex,
string negativeRegex, IList<FileEntry> fileList, bool[] hasMoreFiles) string negativeRegex, IList<FileEntry> fileList, bool[] hasMoreFiles)
{ {
Kp2aLog.Log("Provider.ListFiles " + dirName);
try try
{ {
var dirContents = App.Kp2a.GetFileStorage(dirName).ListContents(ConvertPathToIoc(dirName)); var dirContents = App.Kp2a.GetFileStorage(dirName).ListContents(ConvertPathToIoc(dirName));
foreach (FileDescription e in dirContents) foreach (FileDescription e in dirContents)
{ {
fileList.Add(ConvertFileDescription(e) fileList.Add(ConvertFileDescription(e));
);
} }
} }
catch (Exception e) catch (Exception e)

View File

@ -678,7 +678,9 @@
<AndroidResource Include="Resources\layout\QuickUnlock.xml"> <AndroidResource Include="Resources\layout\QuickUnlock.xml">
<SubType>Designer</SubType> <SubType>Designer</SubType>
</AndroidResource> </AndroidResource>
<AndroidResource Include="Resources\layout\url_credentials.xml" /> <AndroidResource Include="Resources\layout\url_credentials.xml">
<SubType>Designer</SubType>
</AndroidResource>
<AndroidResource Include="Resources\drawable\section_header.xml" /> <AndroidResource Include="Resources\drawable\section_header.xml" />
<AndroidResource Include="Resources\drawable\extra_string_header.xml" /> <AndroidResource Include="Resources\drawable\extra_string_header.xml" />
<AndroidResource Include="Resources\layout\entry_edit_section.xml"> <AndroidResource Include="Resources\layout\entry_edit_section.xml">
@ -771,6 +773,10 @@
<Project>{A8779D4D-7C49-4C2F-82BD-2CDC448391DA}</Project> <Project>{A8779D4D-7C49-4C2F-82BD-2CDC448391DA}</Project>
<Name>Kp2aKeyboardBinding</Name> <Name>Kp2aKeyboardBinding</Name>
</ProjectReference> </ProjectReference>
<ProjectReference Include="..\netftpandroid\System.Net.FtpClient\System.Net.FtpClient.Android.csproj">
<Project>{146FD497-BA03-4740-B6C5-5C84EA8FCDE2}</Project>
<Name>System.Net.FtpClient.Android</Name>
</ProjectReference>
<ProjectReference Include="..\PluginSdkBinding\PluginSdkBinding.csproj"> <ProjectReference Include="..\PluginSdkBinding\PluginSdkBinding.csproj">
<Project>{3DA3911E-36DE-465E-8F15-F1991B6437E5}</Project> <Project>{3DA3911E-36DE-465E-8F15-F1991B6437E5}</Project>
<Name>PluginSdkBinding</Name> <Name>PluginSdkBinding</Name>
@ -1683,6 +1689,11 @@
<ItemGroup> <ItemGroup>
<AndroidResource Include="Resources\drawable-mdpi\ic_fp_40px.png" /> <AndroidResource Include="Resources\drawable-mdpi\ic_fp_40px.png" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\layout\ftpcredentials.xml">
<SubType>Designer</SubType>
</AndroidResource>
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" /> <Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
<Import Project="..\packages\Xamarin.Insights.1.11.3\build\MonoAndroid10\Xamarin.Insights.targets" Condition="Exists('..\packages\Xamarin.Insights.1.11.3\build\MonoAndroid10\Xamarin.Insights.targets')" /> <Import Project="..\packages\Xamarin.Insights.1.11.3\build\MonoAndroid10\Xamarin.Insights.targets" Condition="Exists('..\packages\Xamarin.Insights.1.11.3\build\MonoAndroid10\Xamarin.Insights.targets')" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild"> <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">