Accessing Web API from Hybrid Mobile App, Blazor Mobile - android

I'm working on a Blazor Hybrid App and currently trying to access a .NET Web API from my phone.
I have deployed a .NET Web Application to IIS. API returns just a WeatherForecast data (for those who're not familiar, data type is already defined in project and comes with the template) in JSON.
API Response is something like this:
[
{"date":"2020-09-18T15:55:27.4724752+03:00","temperatureC":-6,"temperatureF":22,"summary":"Hot"},
{"date":"2020-09-19T15:55:27.4725087+03:00","temperatureC":27,"temperatureF":80,"summary":"Bracing"},
{"date":"2020-09-20T15:55:27.4725155+03:00","temperatureC":54,"temperatureF":129,"summary":"Bracing"},
{"date":"2020-09-21T15:55:27.4725221+03:00","temperatureC":1,"temperatureF":33,"summary":"Scorching"},
{"date":"2020-09-22T15:55:27.4725302+03:00","temperatureC":-3,"temperatureF":27,"summary":"Chilly"}
]
I deployed it to my localhost at port 3004. So both in my PC's browser and my mobile phone's browser I can successfully reach to this address and get this response.
However in my mobile application there is a method responsible for retrieving the data, defined as:
AppState.cs:
public async Task<List<WeatherForecast>> GetForecastAsync()
{
return await _http.GetFromJsonAsync<List<WeatherForecast>>("http://192.168.1.22:3004/weatherforecast");
}
and this is called from Index.razor:
#inject AppState appState
#if(todos == null){
<p> Loading </p>
}
else {
// loop todos in foreach
}
#code {
List<Todo> todos;
protected override async Task OnInitializedAsync()
{
todos = await appState.GetForecastAsync();
appState.OnChange += UpdateState;
}
This GET Request returns null. I've tried it with JSON placeholder from https://jsonplaceholder.typicode.com/todos/ (Changed the WeatherForecast to Todo of course) there was no problem!
My Attemps
For possible solutions I've tried to
change my local IP to 10.0.2.2:3004 since I'm on the android phone but no use.
I've tried with both http:// and https:// but still no use.
Configure CORS to allow any origins in the API :
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options => options.AddDefaultPolicy(builder => builder.AllowAnyOrigin()));
services.AddControllers();
}
//...
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
//...
app.UseHttpsRedirection();
app.UseRouting();
app.UseCors();
app.UseAuthorization();
//...
}
How can I reach the API from the mobile app?

After few hours of researching and trial and error I found it.
So I'll explain the flow step by step:
First when the page we want opens, we want to fetch the data from a remote database. In my case I call it from Index.razor, my home page:
Index.razor
#page "/"
#inject AppState appState
#implements IDisposable
#if (forecasts== null)
{
<p>Null</p>
}
else
{
#foreach (var forecast in forecasts)
{
<p>#forecast .Date</p>
<p>#forecast .TemperatureC</p>
<p>#forecast .TemperatureF</p>
<p>#forecast .Summary</p>
}
}
#code {
List<WeatherForecast> forecasts;
protected override async Task OnInitializedAsync()
{
forecasts = await appState.GetForecastAsync(); // We call the Get Forecast Async via appState
appState.OnChange += UpdateState;
}
public void Dispose() => appState.OnChange -= UpdateState;
private async Task UpdateState()
{
StateHasChanged();
}
public class WeatherForecast
{
public DateTime Date { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
public string Summary { get; set; }
}
}
We need the AppState because we need to keep track of state, so it's like a state manager.
After that we jump to AppState.cs:
class AppState
{
HttpClient _http;
public AppState()
{
_http = new HttpClient() { BaseAddress = new Uri("http://192.168.1.22:3000/") };
public async Task<List<WeatherForecast>> GetForecastAsync()
{
try
{
return await _http.GetFromJsonAsync<List<WeatherForecast>>("weatherforecast");
}
catch (Exception ex)
{
Debug.WriteLine(#"\tERROR {0}", ex.Message);
}
return new List<WeatherForecast>();
}
//There is also OnChange method here but it's irrelevant.
}
I've downloaded ISSexpress-proxy and proxied the api to another port because I was having "Invalid hostname" error.
When initializing HttpClient I gave the baseAddress as my local IP, not 10.0.2.2 where you use to access to localhost on Android.
I was still getting errors on HTTP communication. In AndroidManifest.xml file I added android:targetSandboxVersion="1" to <manifest> and android:usesCleartextTraffic="true" to <application>. Thanks to this answer I found how to solve it.
Final Result

Related

Xamarin Android CookieManager unable to remove cookies

Xamarin Android CookieManager.hasCookies() returns false, indicating there are no cookies.
public static async Task<string> SignOut(){
IEnumerable<IAccount> accounts = await App.IdentityClientApp.GetAccountsAsync();
foreach (IAccount account in accounts)
{
await App.IdentityClientApp.RemoveAsync(account);
}
DependencyService.Get<ILogoutHelper>().LogoutSSO();
graphClient = null;
TokenForUser = null;
return "";
}
public class LogoutHelper : ILogoutHelper
{
public void LogoutSSO()
{
CookieManager.Instance.RemoveAllCookies(null);
CookieManager.Instance.RemoveAllCookie();
CookieManager.Instance.RemoveSessionCookie();
CookieManager.Instance.Flush();
Device.BeginInvokeOnMainThread(() => new Android.Webkit.WebView(null).ClearCache(true));
}
}
I want to clear cookies on the browser of the OS so that MSAL login credentials are not cached.
Fixed.
Switching from Chrome Custom Tabs to Embedded (WebView?) allows you to manage cookies through CookieManager.
App.UiParent = new UIParent(Xamarin.Forms.Forms.Context as Activity, true);
https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/MSAL.NET-uses-web-browser#choosing-between-embedded-web-browser-or-system-browser-on-xamarinandroid

.net or Xamarin Internet availability check when the WIFI network has no internet connection

i know its a big discussion how to check if there is a internet connection available for the device.
I tried Ping, WebClient and HTTPClient.
I also use Xamarin Essentials and the Connectivity Plugin.
All this things are working, just make a request to google or the server of your choice and you will get back the answer or not.
You can also set a timeout for 2 seconds and so on.
But now i came to the situation where i'm connected to the WIFI BUT the WIFI itself has no active internet connection.
So all the things i wrote about was not working anymore. The problem is that the timeout will be ignored somehow. Maybe a bug in .net? I don't know.
Now i found one last thing:
try
{
var request = (HttpWebRequest)WebRequest.Create("https://www.google.com");
request.KeepAlive = false;
request.Timeout = 2000;
using (var response = (HttpWebResponse)await request.GetResponseAsync())
{
if (response.StatusCode == HttpStatusCode.OK)
{
//Connection to internet available
return true;
}
else
{
//Connection to internet not available
return false;
}
}
}
catch (WebException webEx)
{
if (webEx.Status == WebExceptionStatus.Timeout)
{
return false;
}
}
catch (Exception e)
{
return false;
}
This was the only solution where i got the WebException when the 2 seconds timeout was reached.
In all other solutions i stuck more than 1 minute till the timeout was reached. Also when i set it to 500ms or something.
Did anybody know the reason why the given timeout is not reached for example with other methodes about?
Solution:
You can use DependencyServiceto implement it .Refer the following code.
in Forms ,create an interface
using System;
namespace app1
{
public interface INetworkAvailable
{
bool IsNetworkAvailable();
}
}
in iOS project
using System;
using Xamarin.Forms;
using Foundation;
[assembly: Dependency(typeof(IsNetworkAvailableImplement))]
namespace xxx.iOS
{
public class IsNetworkAvailableImplement:INetworkAvailable
{
public IsNetworkAvailableImplement()
{
}
bool INetworkAvailable.IsNetworkAvailable()
{
NSString urlString = new NSString("http://captive.apple.com");
NSUrl url = new NSUrl(urlString);
NSUrlRequest request = new NSUrlRequest(url, NSUrlRequestCachePolicy.ReloadIgnoringCacheData, 3);
NSData data = NSUrlConnection.SendSynchronousRequest(request, out NSUrlResponse response, out NSError error);
NSString result = NSString.FromData(data,NSStringEncoding.UTF8);
if(result.Contains(new NSString("Success")))
{
return true;
}
else
{
return false;
}
}
}
}
Don't forget to allow HTTP access .add the following code in info.plist
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
in Android project
using System;
using Java.Lang;
using Xamarin.Forms;
[assembly: Dependency(typeof(IsNetworkAvailableImplement))]
namespace xxx.Droid
{
public class IsNetworkAvailableImplement:INetworkAvailable
{
public IsNetworkAvailableImplement()
{
}
public bool IsNetworkAvailable()
{
Runtime runtime = Runtime.GetRuntime();
Process process = runtime.Exec("ping -c 3 www.google.com");
int result = process.WaitFor();
if(result==0)
{
return true;
}
else
{
return false;
}
}
}
}
Now you can call it in forms ,just like
bool isAvailable= DependencyService.Get<INetworkAvailable>().IsNetworkAvailable();
if(isAvailable)
{
Console.WriteLine("network is available");
}
else
{
Console.WriteLine("network is unavailable");
}
Edited: Xamarin Essentials is the best solution as it offers more options, this is a sample of how to use it
after setup Xamarin Essential NuGet in your project
your code we be like the following
if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.Internet)
{
//your Code if there is an internet
}
else
{
//your Code if there isn't an internet
}
look at this:
https://github.com/xamarin/Essentials

System.Net.Http.HttpClient with AutomaticDecompression and GetAsync (timeout) vs GetStringAsync (working

I have the following code to make requests to a REST API, using Xamarin and an Android device:
public class ApiBase
{
HttpClient m_HttpClient;
public ApiBase(string baseAddress, string username, string password)
{
if (!baseAddress.EndsWith("/"))
{
baseAddress += "/";
}
var handler = new HttpClientHandler();
if (handler.SupportsAutomaticDecompression)
{
handler.AutomaticDecompression = DecompressionMethods.GZip;
}
m_HttpClient = new HttpClient(handler);
m_HttpClient.BaseAddress = new Uri(baseAddress);
var credentialsString = Convert.ToBase64String(Encoding.UTF8.GetBytes(username + ":" + password));
m_HttpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", credentialsString);
m_HttpClient.Timeout = new TimeSpan(0, 0, 30);
}
protected async Task<XElement> HttpGetAsync(string method)
{
try
{
HttpResponseMessage response = await m_HttpClient.GetAsync(method);
if (response.IsSuccessStatusCode)
{
// the request was successful, parse the returned string as xml and return the XElement
var xml = await response.Content.ReadAsAsync<XElement>();
return xml;
}
// the request was not successful -> return null
else
{
return null;
}
}
// some exception occured -> return null
catch (Exception)
{
return null;
}
}
}
If i have it like this, the first and the second call to HttpGetAsync work perfectly, but from the 3rd on the GetAsyncstalls and eventually throws an exception due to the timeout. I send these calls consecutively, there are not 2 of them running simultaneously since the results of the previous call are needed to decide the next call.
I tried using the app Packet Capture to look at the requests and responses to find out if i'm sending an incorrect request. But it looks like the request which fails in the end is never even sent.
Through experimentation i found out that everything works fine if don't set the AutomaticDecompression.
It also works fine if i change the HttpGetAsync method to this:
protected async Task<XElement> HttpGetAsync(string method)
{
try
{
// send the request
var response = await m_HttpClient.GetStringAsync(method);
if (string.IsNullOrEmpty(response))
{
return null;
}
var xml = XElement.Parse(response);
return xml;
}
// some exception occured -> return null
catch (Exception)
{
return null;
}
}
So basically using i'm m_HttpClient.GetStringAsync instead of m_HttpClient.GetAsync and then change the fluff around it to work with the different return type. If i do it like this, everything works without any problems.
Does anyone have an idea why GetAsync doesn't work properly (doesn't seem to send the 3rd request) with AutomaticDecompression, where as GetStringAsync works flawlessly?
There are bug reports about this exact issue:
https://bugzilla.xamarin.com/show_bug.cgi?id=21477
The bug is marked as RESOLVED FIXED and the recomended action is to update to the latest stable build. But there are other (newer) bugreports that indicate the same thing that are still open, ex:
https://bugzilla.xamarin.com/show_bug.cgi?id=34747
I made a workaround by implementing my own HttpHandler like so:
public class DecompressionHttpClientHandler : HttpClientHandler
{
protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
request.Headers.AcceptEncoding.Add(new System.Net.Http.Headers.StringWithQualityHeaderValue("gzip"));
var msg = await base.SendAsync(request, cancellationToken);
if (msg.Content.Headers.ContentEncoding.Contains("gzip"))
{
var compressedStream = await msg.Content.ReadAsStreamAsync();
var uncompresedStream = new System.IO.Compression.GZipStream(compressedStream, System.IO.Compression.CompressionMode.Decompress);
msg.Content = new StreamContent(uncompresedStream);
}
return msg;
}
}
Note that the code above is just an example and not a final solution. For example the request will not be compressed and all headers will be striped from the result. But you get the idea.

Couchbase facebook pull authenticator

I am using couchbase mobile for an application and I want to use facebook for authentication. As per documentation, couchbase offers it's own implementation for authentication, the only required thing would be the token which I retrieve from the android facebook login flow.
The code for Synchronize class looks something like this:
public class Synchronize {
public Replication pullReplication;
public Replication pushReplication;
public static class Builder {
public Replication pullReplication;
public Replication pushReplication;
public Builder(Database database, String url, Boolean continuousPull) {
if (pullReplication == null && pushReplication == null) {
URL syncUrl;
try {
syncUrl = new URL(url);
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
pullReplication = database.createPullReplication(syncUrl);
pullReplication.setContinuous(true);
pushReplication = database.createPushReplication(syncUrl);
pushReplication.setContinuous(true);
}
}
public Builder facebookAuth(String token) {
if (!TextUtils.isEmpty(token)) {
Authenticator facebookAuthenticator = AuthenticatorFactory.createFacebookAuthenticator(token);
pullReplication.setAuthenticator(facebookAuthenticator);
pushReplication.setAuthenticator(facebookAuthenticator);
}
return this;
}
public Builder basicAuth(String username, String password) {
Authenticator basicAuthenticator = AuthenticatorFactory.createBasicAuthenticator(username, password);
pullReplication.setAuthenticator(basicAuthenticator);
pushReplication.setAuthenticator(basicAuthenticator);
return this;
}
public Builder addChangeListener(Replication.ChangeListener changeListener) {
pullReplication.addChangeListener(changeListener);
pushReplication.addChangeListener(changeListener);
return this;
}
public Synchronize build() {
return new Synchronize(this);
}
}
private Synchronize(Builder builder) {
pullReplication = builder.pullReplication;
pushReplication = builder.pushReplication;
}
public void start() {
pullReplication.start();
pushReplication.start();
}
public void destroyReplications() {
if (pullReplication != null && pushReplication != null) {
pullReplication.stop();
pushReplication.stop();
pullReplication.deleteCookie("SyncGatewaySession");
pushReplication.deleteCookie("SyncGatewaySession");
pullReplication = null;
pushReplication = null;
}
}
}
And I use it like this:
...
public void startReplicationSync(String facebookAccessToken) {
if (sync != null) {
sync.destroyReplications();
}
final String url = BuildConfig.URL_HOST + ":" + BuildConfig.URL_PORT + "/" + DATABASE_NAME;
sync = new Synchronize.Builder(databaseManager.getDatabase(), url, true)
.facebookAuth(facebookAccessToken)
.addChangeListener(getReplicationChangeListener())
.build();
sync.start();
}
...
My sync gateway json config file:
{
"interface":":4984",
"adminInterface":":4985",
"log":["REST"],
"facebook":{
"register" : true
},
"databases":{
"sync_gateway":{
"server":"http://localhost:8091",
"bucket":"sync_gateway",
"users": {
"GUEST": {"disabled": false}
},
"sync":`function(doc) {channel(doc.channels);}`
}
}
}
I also tried with "GUEST": {"disabled": true}, no luck
My problem is that if I do this
pullReplication.setAuthenticator(facebookAuthenticator);
pushReplication.setAuthenticator(facebookAuthenticator);
Nothing will ever get replicated/pulled from the server. However if I don't set an authenticator, everything is pulled. Is it something I am doing wrong? I really need to use the authenticator in order to prevent some documents to not being replicated for non-authenticated users.
Note! The token is good, as if I am looking in the users section of sync gateway admin, I can see the right profile id of the logged in user token I passed to the couchbase facebook authenticator.
In the Sync Gateway config you provided, the Sync Function is function(doc, oldDoc) {channel(doc.channels);} which means that if the document processed by Sync Gateway contains a string(s) under the channels field, the document will be mapped to this/these channel(s). Let's assume the following config file:
{
"log": ["CRUD"],
"databases": {
"db": {
"server": "walrus:",
"users": {
"GUEST": {"disabled": false, "admin_channels": ["*"]}
},
"sync": `
function sync(doc, oldDoc) {
channel(doc.channels);
}
`
}
}
}
If the channels field doesn't exist then the document will be mapped to a channel called undefined. But the GUEST account has access to the * channel (a placeholder to represent all channels). So, all unauthenticated replications will pull all documents. Let's now introduce the facebook login field in the config file. This time, replications authenticated with a facebook token represent a new user which has only access to the ! channel by default (watch this screencast to understand the ! channel, a.k.a the public channel https://www.youtube.com/watch?v=DKmb5mj9pMI). To give a user access to other channels, you must use the access API call in the Sync Function (read more about all Sync Function API calls here).
In the case of facebook authentication, the user's facebook ID is used to represent the user name. Supposing that the document has a property holding the user's facebook ID (user_id: FACEBOOK_ID), you can map the document to a channel and give the user access to it. The new Sync Function would look like this:
function(doc, oldDoc) {
channel(doc._id);
access(doc.user_id, doc._id);
}
You can retrieve the user's facebook ID with the Facebook Android SDK and save on a document field.

Error using MvxMultiPartFormRestRequest to post image/data to API controller

I am building an Android app which has a form that user can post image for an item.
So the post data is an int field and an image.
I use MvvmCross Network plugin to post and got below error. I am a beginner and I do not know where I did wrong: mobile app code or API controller code?
error = {System.Net.WebException: The remote server returned an error: (500) Internal Server Error.
at System.Net.HttpWebRequest.CheckFinalStatus (System.Net.WebAsyncResult result) [0x00000] in <filename unknown>:0
at System.Net.HttpWebRequest.SetResponseData ...
This is mobile app code:
This is select image code:
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
if ((requestCode == PickImageId) && (resultCode == Result.Ok) && (data != null))
{
_imgUri = data.Data;
_imgPath = GetPathToImage(_imgUri);
_contentType = ContentResolver.GetType(_imgUri);
}
}
Then click Submit button
private void btnSubmit_Click(object sender, EventArgs e)
{
MemoryStream stream = new MemoryStream();
ContentResolver.OpenInputStream(_imgUri).CopyTo(stream);
_vm.Submit(_imgPath, _contentType, stream);
}
This is Submit function:
public void Submit(string fileName, string contentType, MemoryStream stream) {
//Post data
int itemId = 1;
List<MvxMultiPartFormRestRequest.IStreamForUpload> streams = new List<MvxMultiPartFormRestRequest.IStreamForUpload>();
streams.Add(new MvxMultiPartFormRestRequest.MemoryStreamForUpload("userFile", fileName, contentType, stream));
var client = Mvx.Resolve<IMvxJsonRestClient>();
var r = new MvxMultiPartFormRestRequest("https://.../api/ItemUserImage");
r.FieldsToSend.Add("itemId", itemId.ToString());
r.StreamsToSend.AddRange(streams);
client.MakeRequestFor<MyResponse>(r, (result) =>
{
Mvx.Resolve<IUserInteraction>().Alert(result.Result.ResponseText, null, TitleInformation);
}, (error) =>
{
//I met error here
});
This is my API controller:
public class ItemUserImageController : ApiController
{
public async Task<HttpResponseMessage> PostFormData()
{
Response response = new Response();
response.ResponseCode = 1;
response.ResponseText = "step0-";
// Check if the request contains multipart/form-data.
if (!Request.Content.IsMimeMultipartContent())
{
response.ResponseText += "step1-";
throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
}
response.ResponseText += "step2-";
string root = HttpContext.Current.Server.MapPath("~/App_Data");
var provider = new MultipartFormDataStreamProvider(root);
response.ResponseText += "step3-";
try
{
// Read the form data.
await Request.Content.ReadAsMultipartAsync(provider);
response.ResponseText += "step4-";
foreach (var key in provider.FormData.AllKeys)
{
foreach (var val in provider.FormData.GetValues(key))
{
response.ResponseText += string.Format("{0}: {1}-", key, val);
}
}
response.ResponseText += "step5-";
// This illustrates how to get the file names.
foreach (MultipartFileData file in provider.FileData)
{
response.ResponseText += string.Format("{0} - Server file path: {1}-", file.Headers.ContentDisposition.FileName, file.LocalFileName);
}
return Request.CreateResponse(HttpStatusCode.OK, response);
}
catch (System.Exception e)
{
return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, response.ResponseText ,e);
}
}
}
Please help. Thank you.
This "bug" could be lots of things. Really the best way to resolve it is to get in there with some debugging tools, to set breakpoints in both the client and the ASP.Net app and to see what the communication is.
To set breakpoints in the client app, use Visual or Xamarin Studio.
To see the raw HTTP traffic between the client app and the server, use Fiddler - see https://stackoverflow.com/a/25412339/373321 (this assumes you have a 4.4 or later Android device)
To set breakpoints in the server app, use Visual Studio and try exposing the website from your development box beyond localhost using IISExpress settings - see http://johan.driessen.se/posts/Accessing-an-IIS-Express-site-from-a-remote-computer
Once you start debugging this, I'm sure you'll quickly
Beyond that, the only "spider sense tingle" I got looking through your client code was a slight concern that you might need to reset the current position in your MemoryStream back to the start (but I haven't thought this fully through).

Categories

Resources