Skip to content

Commit

Permalink
[Xamarin.Android.Build.Tasks] Code-Behind, Take 2! (#1465)
Browse files Browse the repository at this point in the history
The original code-behind approach in 7c31899 and d09b86a was deemed
as "bad", for two primary reasons:

 1. The "mirroring" of the `id` hierarchy into C# would result in
    "brittle" code, as moving elements in Layout `.axml` could result
    in semantic C# changes.

 2. We thought that "code behind" shouldn't be *required*; rather,
    there should be a way to make use of the "code behind" logic
    and infrastructure without requiring partial classes.

Rework things, allowing one to find and access layout elements
(widgets) without having to call the `FindViewById()` or
`FindFragment()` APIs, and instead access a C# property which will do
all the work of finding and returning the right widget.

Two approaches are available:

 1. Bindings
 2. Code Behind

For both approaches, assume a Layout `.axml` file such as:

	<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android">
	  <Button android:id="@+id/myButton" />
	  <fragment
	      xamarin:managedType="CommonSampleLibrary.LogFragment"
	      android:name="commonsamplelibrary.LogFragment"
	      android:id="@+id/log_fragment"
	  />
	  <fragment
	      android:name="CommonSampleLibrary.LogFragment"
	      android:id="@+id/secondary_log_fragment"
	  />
	</LinearLayout>


Bindings
========

Bindings involve processing the `.axml` files and generating a
stand-alone class with properties to access the widgets. All the
generated classes are placed into the global `Binding` namespace and
their names follow the base name of the layout being bound.
Therefore, a class for layout named `Main.axml` will be named
`Binding.Main` and a class for layout `my_other_layout.xml` will be
named `Binding.my_other_layout`.

The generated file will contain the API:

	// Generated code:
	namespace Binding {

	  sealed partial class Main : global::Xamarin.Android.Design.LayoutBinding {

	    public override int ResourceLayoutID {get;}

	    public Main (global::Xamarin.Android.Design.ILayoutBindingClient client);

	    public Button                              myButton                  {get;}
	    public CommonSampleLibrary.LogFragment     log_fragment              {get;}
	    public global::Android.App.Fragment        secondary_log_fragment    {get;}
	  }
	}

Note that each `//*/@android:id` within the Layout `.axml` file
results in a property of the same name within the Binding class.

The main advantage of this approach is that no modifications to
layout files are necessary.

The Binding class can be instantiated in a number of ways:

  * By passing instance of an activity *after* setting the activity
    layout in the usual way:

        SetContentView (Resource.Layout.Main); // as normal
        var binding = new Binding.Main (this)

  * By passing instance of a *View* to the constructor after loading
    the correct layout into the view:

        var binding = new Binding.my_other_layout (some_view);

  * By changing the `Activity` layout setting code to call a new
    overload of the `SetContentView()` method:

        var binding = SetContentView<Binding.Main> ();

After the binding is instantiated, one can find the widgets by simply
accessing the correct property:

	Button btn = binding.myButton;

`Xamarin.Android.Design.LayoutBinding` is a new class added to make
it possible to instantiate bindings for both Activities and Views.


Code Behind
===========

The new Code Behind approach now uses the above Bindings types to
work. It is similar in spirit to the previous approach in that it
generates a `partial` activity class which defines a number of
properties, within the Activity, to support accessing the layout
widgets. In order for this to work, it is first necessary to modify
the associated layout by adding two attributes to the root element of
the layout:

  * XML namespace declaration:

        xmlns:xamarin="http://schemas.xamarin.com/android/xamarin/tools"

  * Specification of *full* type names for activities which will use
    the generated code (a semicolon-separated list; at least one type
    is required):

        xamarin:classes="Xamarin.Android.Tests.CodeBehindBuildTests.MainActivity;Xamarin.Android.Tests.CodeBehindBuildTests.AnotherMainActivity"

This would result in a `.axml` file such as:

	<LinearLayout
	    xmlns:android="http://schemas.android.com/apk/res/android"
	    xmlns:xamarin="http://schemas.xamarin.com/android/xamarin/tools"
	    xamarin:classes="Xamarin.Android.Tests.CodeBehindBuildTests.MainActivity">
	  <Button android:id="@+id/myButton" />
	  <fragment
	      xamarin:managedType="CommonSampleLibrary.LogFragment"
	      android:name="commonsamplelibrary.LogFragment"
	      android:id="@+id/log_fragment"
	  />
	  <fragment
	      android:name="CommonSampleLibrary.LogFragment"
	      android:id="@+id/secondary_log_fragment"
	  />
	</LinearLayout>

(Note changes to the previous `<LinearLayout/>` example, far above.)

With the added XML attributes in place, all the activities mentioned
in the `/*/@xamarin:classes` attribute have to be modified by adding
the `partial` modifier to the class declaration, and the build system
will generate `partial` classes for each specified type, providing
C# property access to each element with a `//*/@android:id` value:

	// Generated code:
	namespace Xamarin.Android.Tests.CodeBehindBuildTests {
	  partial class MainActivity {
	    public override void SetContentView (global::Android.Views.View view);
	    public override void SetContentView (global::Android.Views.View view, global::Android.Views.ViewGroup.LayoutParams @params);
	    public override void SetContentView (int layoutResID);

	    partial void OnSetContentView (global::Android.Views.View view, ref bool callBaseAfterReturn);
	    partial void OnSetContentView (global::Android.Views.View view, global::Android.Views.ViewGroup.LayoutParams @params, ref bool callBaseAfterReturn);
	    partial void OnSetContentView (int layoutResID, ref bool callBaseAfterReturn);

	    public Button                          myButton                {get;}
	    public CommonSampleLibrary.LogFragment log_fragment            {get;}
	    public global::Android.App.Fragment    secondary_log_fragment  {get;}
	  }
	}

User-written code would continue to use `SetContentView()`, and could
access the Widget properties after `SetContentView()` is called:

	// User code
	namespace Xamarin.Android.Tests.CodeBehindBuildTests {
	  partial class MainActivity {
	    protected override void OnCreate(Bundle state)
	    {
	      SetContentView (Resource.Layout.Main);
	      myButton.Click += delegate {
	          myButton.Text = $"{count++} clicks!";
	      };
	    }
	  }
	}

The `OnSetContentView()` partial methods may be implemented in your
code to customize `SetContentView()` behavior since it's no longer
possible to override them in your "main" activity code.

The current implementation of the generated code-behind class is
similar to:

	namespace Xamarin.Android.Tests.CodeBehindBuildTests {
	  partial class MainActivity {

	    Binding.Main __layout_binding;
	
	    public override void SetContentView (global::Android.Views.View view) {
	      __layout_binding = new global::Binding.Main (view);
	      bool callBase = true;
	      OnSetContentView (view, ref callBase);
	      if (callBase)
	        base.SetContentView (view);
	    }
	
	    public override void SetContentView (global::Android.Views.View view, global::Android.Views.ViewGroup.LayoutParams @params) {
	      // Code similar to above
	    }
	
	    public override void SetContentView (int layoutResID) {
	      // Code similar to above
	    }
	
	    partial void OnSetContentView (global::Android.Views.View view, ref bool callBaseAfterReturn);
	    partial void OnSetContentView (global::Android.Views.View view, global::Android.Views.ViewGroup.LayoutParams @params, ref bool callBaseAfterReturn);
	    partial void OnSetContentView (int layoutResID, ref bool callBaseAfterReturn);
	
	    public Button myButton => __layout_binding?.myButton;
	    public CommonSampleLibrary.LogFragment log_fragment => __layout_binding?.log_fragment;
	    public global::Android.App.Fragment secondary_log_fragment => __layout_binding?.secondary_log_fragment;
	  }
	}
  • Loading branch information
grendello authored and jonpryor committed Apr 27, 2018
1 parent 43ea750 commit ab3773c
Show file tree
Hide file tree
Showing 86 changed files with 4,432 additions and 300 deletions.
14 changes: 13 additions & 1 deletion Documentation/guides/BuildProcess.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,9 @@ an [MSBuild PropertyGroup element](http://msdn.microsoft.com/en-us/library/t4w15
`DebugSymbols` property controls whether or not th Application is
debuggable.


- **AndroidGenerateLayoutBindings** &ndash; Enables generation of [layout code-behind](LayoutCodeBehind.md)
if set to `true` (the default) or disables it completely if set to `false`

### Install Properties

Install properties control the behavior of the `Install` and
Expand Down Expand Up @@ -890,6 +892,16 @@ distinct resource names.
</ItemGroup>
```

### AndroidBoundLayout

Indicates that the layout file is to have code-behind generated for it in case when
the `AndroidGenerateLayoutBindings` property is set to `false`. In all other aspects
it is identical to `AndroidResource` described above. This action can be used **only**
with layout files:

```xml
<AndroidBoundLayout Include="Resources\layout\Main.axml" />
```

### AndroidNativeLibrary

Expand Down
430 changes: 255 additions & 175 deletions Documentation/guides/LayoutCodeBehind.md

Large diffs are not rendered by default.

30 changes: 30 additions & 0 deletions Xamarin.Android-Tests.sln
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestRunner.xUnit", "tests\T
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LocalTests.NUnit", "tests\BCL-Tests\LocalTests.NUnit\LocalTests.NUnit.csproj", "{910DCD88-D50A-4AAD-BA7A-CD51AB8532BF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeBehindBuildTests", "tests\CodeBehind\BuildTests\CodeBehindBuildTests.csproj", "{95012FA9-ED51-4004-8F36-91DB361C892B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommonSampleLibrary", "tests\CodeBehind\CommonSampleLibrary\CommonSampleLibrary.csproj", "{7A5FB23C-6B26-461A-8BBD-02392DCE3C11}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CodeBehind", "CodeBehind", "{9B63992C-2201-4BB0-BD00-D637B481A995}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeBehindUnitTests", "tests\CodeBehind\UnitTests\CodeBehindUnitTests.csproj", "{F4DAFD78-BE76-46C9-A1AD-85D8C91CD77B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Xamarin.ProjectTools", "src\Xamarin.Android.Build.Tasks\Tests\Xamarin.ProjectTools\Xamarin.ProjectTools.csproj", "{2DD1EE75-6D8D-4653-A800-0A24367F7F38}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -155,6 +165,22 @@ Global
{910DCD88-D50A-4AAD-BA7A-CD51AB8532BF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{910DCD88-D50A-4AAD-BA7A-CD51AB8532BF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{910DCD88-D50A-4AAD-BA7A-CD51AB8532BF}.Release|Any CPU.Build.0 = Release|Any CPU
{95012FA9-ED51-4004-8F36-91DB361C892B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{95012FA9-ED51-4004-8F36-91DB361C892B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{95012FA9-ED51-4004-8F36-91DB361C892B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{95012FA9-ED51-4004-8F36-91DB361C892B}.Release|Any CPU.Build.0 = Release|Any CPU
{7A5FB23C-6B26-461A-8BBD-02392DCE3C11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7A5FB23C-6B26-461A-8BBD-02392DCE3C11}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7A5FB23C-6B26-461A-8BBD-02392DCE3C11}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7A5FB23C-6B26-461A-8BBD-02392DCE3C11}.Release|Any CPU.Build.0 = Release|Any CPU
{F4DAFD78-BE76-46C9-A1AD-85D8C91CD77B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F4DAFD78-BE76-46C9-A1AD-85D8C91CD77B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F4DAFD78-BE76-46C9-A1AD-85D8C91CD77B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F4DAFD78-BE76-46C9-A1AD-85D8C91CD77B}.Release|Any CPU.Build.0 = Release|Any CPU
{2DD1EE75-6D8D-4653-A800-0A24367F7F38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2DD1EE75-6D8D-4653-A800-0A24367F7F38}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2DD1EE75-6D8D-4653-A800-0A24367F7F38}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2DD1EE75-6D8D-4653-A800-0A24367F7F38}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{2305B00D-DE81-4744-B0DA-357835CAFE5A} = {43A4FB09-279A-4138-8027-EC1E1CED2E8A}
Expand All @@ -178,5 +204,9 @@ Global
{CB2335CB-0050-4020-8A05-E9614EDAA05E} = {6C86878D-9EBF-45B3-9368-C0EA4026F706}
{57DC8529-2628-40C4-B27E-BAC1AE44A706} = {6C86878D-9EBF-45B3-9368-C0EA4026F706}
{910DCD88-D50A-4AAD-BA7A-CD51AB8532BF} = {6C86878D-9EBF-45B3-9368-C0EA4026F706}
{95012FA9-ED51-4004-8F36-91DB361C892B} = {9B63992C-2201-4BB0-BD00-D637B481A995}
{7A5FB23C-6B26-461A-8BBD-02392DCE3C11} = {9B63992C-2201-4BB0-BD00-D637B481A995}
{F4DAFD78-BE76-46C9-A1AD-85D8C91CD77B} = {9B63992C-2201-4BB0-BD00-D637B481A995}
{2DD1EE75-6D8D-4653-A800-0A24367F7F38} = {9B63992C-2201-4BB0-BD00-D637B481A995}
EndGlobalSection
EndGlobal
4 changes: 3 additions & 1 deletion build-tools/scripts/RunTests.targets
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
</PropertyGroup>
<ItemGroup>
<_TestAssembly Include="$(_TopDir)\bin\Test$(Configuration)\Xamarin.Android.Build.Tests.dll" />
<_TestAssembly Include="$(_TopDir)\bin\Test$(Configuration)\CodeBehind\CodeBehindUnitTests.dll" />
<_ApkTestProject Include="$(_TopDir)\src\Mono.Android\Test\Mono.Android-Tests.csproj" />
<_ApkTestProject Include="$(_TopDir)\tests\CodeGen-Binding\Xamarin.Android.JcwGen-Tests\Xamarin.Android.JcwGen-Tests.csproj" />
<_ApkTestProject Include="$(_TopDir)\tests\CodeGen-MkBundle\Xamarin.Android.MakeBundle-Tests\Xamarin.Android.MakeBundle-Tests.csproj" />
Expand Down Expand Up @@ -96,4 +97,5 @@
Targets="@(_RunTestTarget)"
/>
</Target>
</Project>
</Project>

28 changes: 27 additions & 1 deletion src/Mono.Android/Android.App/Activity.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
using System;
using System.Reflection;

using Android.Content;
using Android.Runtime;
using Android.Views;
using Xamarin.Android.Design;

namespace Android.App {

partial class Activity {
partial class Activity : ILayoutBindingClient {

Context ILayoutBindingClient.Context => this;

public T FindViewById<T> (int id)
where T : Android.Views.View
Expand All @@ -22,6 +28,26 @@ public void RunOnUiThread (Action action)
{
RunOnUiThread (new Java.Lang.Thread.RunnableImplementor (action));
}

protected T SetContentView <T> () where T: LayoutBinding
{
T items = (T)Activator.CreateInstance (
type: typeof (T),
bindingAttr: BindingFlags.CreateInstance | BindingFlags.DeclaredOnly | BindingFlags.NonPublic | BindingFlags.Public,
binder: null,
args: new Object[] {this}, // ILayoutBindingClient constructor should be looked up
culture: null
);

SetContentView (items.ResourceLayoutID);
return items;
}

public virtual void OnLayoutViewNotFound <T> (int resourceId, ref T view) where T: View
{}

public virtual void OnLayoutFragmentNotFound <T> (int resourceId, ref T fragment) where T: Fragment
{}
}
}

Expand Down
16 changes: 15 additions & 1 deletion src/Mono.Android/Android.Views/View.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Android.AccessibilityServices;
using Android.OS;
using Android.Runtime;
using Xamarin.Android.Design;

namespace Android.Views {

Expand All @@ -11,7 +12,9 @@ public enum SystemUiFlags {
}
#endif

public partial class View {
public partial class View : ILayoutBindingClient {

global::Android.Content.Context ILayoutBindingClient.Context => ((View)this).Context;

#if ANDROID_16
[Obsolete ("This method uses wrong enum type. Please use PerformAccessibilityAction(Action) instead.")]
Expand Down Expand Up @@ -75,5 +78,16 @@ public bool FitsSystemWindows ()
return InvokeFitsSystemWindows ();
}
#endif
public virtual void OnLayoutViewNotFound <T> (int resourceId, ref T view) where T: View
{
var activity = Context as global::Android.App.Activity;
activity?.OnLayoutViewNotFound <T> (resourceId, ref view);
}

public virtual void OnLayoutFragmentNotFound <T> (int resourceId, ref T fragment) where T: global::Android.App.Fragment
{
var activity = Context as global::Android.App.Activity;
activity?.OnLayoutFragmentNotFound <T> (resourceId, ref fragment);
}
}
}
5 changes: 5 additions & 0 deletions src/Mono.Android/Mono.Android.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@
<Compile Include="..\..\external\Java.Interop\src\Java.Interop.Tools.TypeNameMappings\Java.Interop.Tools.TypeNameMappings\JavaNativeTypeManager.cs">
<Link>JavaNativeTypeManager.cs</Link>
</Compile>
<Compile Include="Xamarin.Android.Design\LayoutBinding.cs" />
<Compile Include="Xamarin.Android.Design\ILayoutBindingClient.cs" />
</ItemGroup>
<Import Project="..\Xamarin.Android.NamingCustomAttributes\Xamarin.Android.NamingCustomAttributes.projitems" Label="Shared" Condition="Exists('..\Xamarin.Android.NamingCustomAttributes\Xamarin.Android.NamingCustomAttributes.projitems')" />
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
Expand Down Expand Up @@ -367,4 +369,7 @@
<ReferenceOutputAssembly>False</ReferenceOutputAssembly>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Folder Include="Xamarin.Android.Design\" />
</ItemGroup>
</Project>
3 changes: 2 additions & 1 deletion src/Mono.Android/Test/Resources/layout/FragmentFixup.axml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:xamarin="http://schemas.xamarin.com/android/xamarin/tools"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
Expand All @@ -12,6 +12,7 @@
/>
<fragment
android:name="xamarin.android.runtimetests.MyFragment"
xamarin:managedType="Xamarin.Android.RuntimeTests.MyFragment"
android:id="@+id/csharp_legacy_fragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
Expand Down
5 changes: 2 additions & 3 deletions src/Mono.Android/Test/Resources/layout/Main.axml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
xmlns:tools="http://schemas.xamarin.com/android/tools"
tools:class="Xamarin.Android.RuntimeTests.MainActivity"
>
xmlns:xamarin="http://schemas.xamarin.com/android/xamarin/tools"
xamarin:classes="Xamarin.Android.RuntimeTests.MainActivity">
<TextView
android:id="@+id/first_text_view"
android:layout_width="wrap_content"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ protected override void OnCreate (Bundle bundle)
// ignore
};

my_scroll_view.Widget.FillViewport = true;
my_scroll_view.FillViewport = true;

my_scroll_view.first_text_view.Click += delegate {
first_text_view.Click += delegate {
// ignore
};

my_scroll_view.second_text_view.Click += delegate {
second_text_view.Click += delegate {
// ignore
};

Expand Down
17 changes: 17 additions & 0 deletions src/Mono.Android/Xamarin.Android.Design/ILayoutBindingClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;

using Android.App;
using Android.Content;
using Android.Views;

namespace Xamarin.Android.Design
{
public interface ILayoutBindingClient
{
Context Context { get; }

T FindViewById<T> (int value) where T : View;
void OnLayoutViewNotFound <T> (int resourceId, ref T view) where T: View;
void OnLayoutFragmentNotFound <T> (int resourceId, ref T fragment) where T: Fragment;
}
}
55 changes: 55 additions & 0 deletions src/Mono.Android/Xamarin.Android.Design/LayoutBinding.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System;

using Android.App;
using Android.Views;

namespace Xamarin.Android.Design
{
public abstract class LayoutBinding
{
ILayoutBindingClient client;

public abstract int ResourceLayoutID { get; }

protected LayoutBinding (ILayoutBindingClient client)
{
this.client = client ?? throw new ArgumentNullException (nameof (client));
}

protected T FindView <T> (int resourceId, ref T cachedField) where T: View
{
if (cachedField != null)
return cachedField;

T ret = client.FindViewById <T> (resourceId);
if (ret == null)
client.OnLayoutViewNotFound <T> (resourceId, ref ret);

if (ret == null)
throw new global::System.InvalidOperationException ($"View not found (Client: {client}; Layout ID: {ResourceLayoutID}; Resource ID: {resourceId})");

cachedField = ret;
return ret;
}

protected T FindFragment <T> (int resourceId, ref T cachedField) where T: Fragment
{
if (cachedField != null)
return cachedField;

var activity = client.Context as Activity;
if (activity == null)
throw new InvalidOperationException ("Finding fragments is supported only for Activity instances");

T ret = activity.FragmentManager.FindFragmentById<T> (resourceId);
if (ret == null)
client.OnLayoutFragmentNotFound (resourceId, ref ret);

if (ret == null)
throw new InvalidOperationException ($"Fragment not found (ID: {resourceId})");

cachedField = ret;
return ret;
}
}
}
2 changes: 1 addition & 1 deletion src/Xamarin.Android.Build.Tasks/Tasks/Aapt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ void DoExecute ()

protected string GenerateCommandLineCommands (string ManifestFile, string currentAbi, string currentResourceOutputFile)
{
// For creating Resource.Designer.cs:
// For creating Resource.designer.cs:
// Running command: C:\Program Files (x86)\Android\android-sdk-windows\platform-tools\aapt
// "package"
// "-M" "C:\Users\Jonathan\AppData\Local\Temp\ryob4gaw.way\AndroidManifest.xml"
Expand Down
Loading

0 comments on commit ab3773c

Please sign in to comment.