OAuth1Authenticator of Xamarin.Auth not terminating not completing - android

I am currently trying to use a REST service inside a xamarin.forms app.
To perform the authentication I use this code:
string consumerKey = "consumer_key";
string consumerSecret = "consumer_secret";
var requestTokenUrl = new Uri("https://service/oauth/request_token");
var authorizeUrl = new Uri("https://dservice/oauth/authorize");
var accessTokenUrl = new Uri("https://service/oauth/access_token");
var callbackUrl = new Uri("customprot://oauth1redirect");
authenticator = new Xamarin.Auth.OAuth1Authenticator(consumerKey, consumerSecret, requestTokenUrl, authorizeUrl, accessTokenUrl, callbackUrl, null, true);
authenticator.ShowErrors = true;
authenticator.Completed += Aut_Completed;
var presenter = new Xamarin.Auth.Presenters.OAuthLoginPresenter();
presenter.Completed += Presenter_Completed;
authenticator.Error += Authenticator_Error;
presenter.Login(authenticator);
Now, after authenticating the user will be redirected to customprot://oauth1redirect. To catch this redirection I added a new IntentFilter (for Android) like this:
[Activity(Label = "OAuthLoginUrlSchemeInterceptorActivity", NoHistory = true, LaunchMode = LaunchMode.SingleTop)]
[IntentFilter(
new[] { Intent.ActionView },
Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable },
DataSchemes = new[] { "customprot"},
DataPathPrefix = "/oauth1redirect")]
public class OAuthLoginUrlSchemeInterceptorActivity : Activity
{
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
// Convert Android.Net.Url to Uri
var uri = new Uri(Intent.Data.ToString());
// Load redirectUrl page
Core.Controller.authenticator.OnPageLoading(uri);
Core.Controller.authenticator.OnPageLoaded(uri);
Finish();
}
}
As far as I understood the documentation of xamarin.auth this will trigger the OAuth1Authenticator to parse the resulting url to get the authenticated user's credentials, and ultimatley triggering the Completed or Error event. But suprisingly nothing happens: no event is called or error raised. As this makes debugging harder, I do not really know how to solve this issue. Therefore, I am looking for suggestings about the cause of the issue and possible solutions, too.
Edit: Just to make this clearer: The OnCreate method of the intent is called, but executing the OnPageLoading method does not raise the Completed nor the Error event of the authenticator.
Edit2: here is the code of my callbacks (I created a breakpoint inside each of them, and the debugger does not break at them or raise an exception, so I am quite sure, that the callbacks are not called at all).
private static void Presenter_Completed(object sender, Xamarin.Auth.AuthenticatorCompletedEventArgs e)
{
throw new NotImplementedException();
}
private static void Aut_Completed(object sender, Xamarin.Auth.AuthenticatorCompletedEventArgs e)
{
throw new NotImplementedException();
}

This may only help future people (like me) that stumble on this question but perhaps not answer your particular issue. I was experiencing the same symptoms using the OAuth2Authenticator. I was capturing the redirect, calling OnPageLoading(), but then neither my completed or error events were firing.
The key for me was that it was only happening the 2nd time I called the Authenticator.
After digging through the Xamarin.Auth source, I realized that if HasCompleted is true when the authenticator calls OnSucceeded(), it simply returns without raising any events:
From Authenticator.cs
public void OnSucceeded(Account account)
{
string msg = null;
#if DEBUG
string d = string.Join(" ; ", account.Properties.Select(x => x.Key + "=" + x.Value));
msg = String.Format("Authenticator.OnSucceded {0}", d);
System.Diagnostics.Debug.WriteLine(msg);
#endif
if (HasCompleted)
{
return;
}
HasCompleted = true;
etc...
So, my issue was that I was keeping my authenticator instance around. Since HasCompleted is a private set property, I had to create a new authenticator instance and now it all works as expected.
Maybe I should have posted a new question and answered it. I'm sure the community will let me know.

I have also run into this issue but after managed to get this part working
I create my OAuth2Authenticatoras follows:
App.OAuth2Authenticator = new OAuth2Authenticator(
clientId: OAuthConstants.CLIENT_ID,
clientSecret: null,
scope: OAuthConstants.SCOPE,
authorizeUrl: new Uri(OAuthConstants.AUTHORIZE_URL),
accessTokenUrl: new Uri(OAuthConstants.ACCESS_TOKEN_URL),
redirectUrl: new Uri(OAuthConstants.REDIRECT_URL), //"com.something.myapp:/oauth2redirect" -- note I only have one /
getUsernameAsync: null,
isUsingNativeUI: true);
then in my Interceptor activity:
[Activity(Label = "GoogleAuthInterceptor")]
[IntentFilter
(
actions: new[] { Intent.ActionView },
Categories = new[]
{
Intent.CategoryDefault,
Intent.CategoryBrowsable
},
DataSchemes = new[]
{
// First part of the redirect url (Package name)
"com.something.myapp"
},
DataPaths = new[]
{
// Second part of the redirect url (Path)
"/oauth2redirect"
}
)]
public class GoogleAuthInterceptor: Activity
{
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
// Create your application here
Android.Net.Uri uri_android = Intent.Data;
// Convert Android Url to C#/netxf/BCL System.Uri
Uri uri_netfx = new Uri(uri_android.ToString());
// Send the URI to the Authenticator for continuation
App.OAuth2Authenticator?.OnPageLoading(uri_netfx);
// remove your OnPageLoaded it results in an invalid_grant exception for me
Finish();
}
}
You can try changing your DataPathPrefix = "/oauth1redirect")] to
DataPaths = new[]
{
// Second part of the redirect url (Path)
"/oauth1redirect"
}
This successfully trigger the Completed event on the OAuth2Authenticator and then after that the one on the presenter
private async void OAuth2Authenticator_Completed(object sender, AuthenticatorCompletedEventArgs e)
{
try
{
// UI presented, so it's up to us to dimiss it on Android
// dismiss Activity with WebView or CustomTabs
if(e.IsAuthenticated)
{
App.Account = e.Account;
var oAuthUser = await GetUserDetails();
// Add account to store
AccountStore.Create().Save(App.Account, App.APP_NAME_KEY);
}
else
{
// The user is not authenticated
// Show Alert user not found... or do new signup?
await App.Notify("Invalid user. Please try again");
}
}
catch(Exception ex)
{
throw;
}
}
At this stage I am redirected to the App.
I am currently trying to solve an issue where the presenter is not closed. It runs in the background even though the app is in the foreground and the user already authenticated. But this should hopefully help you solve your issue.

Related

Updating Xamarin.Forms breaks Prism navigation for Android

I have a Xamarin app made up of several pages, and I'm using Prism with AutoFac. I'm unable to update Xamarin.Forms without breaking navigation on the Android project only. It works fine on iOS.
I started with Xamarin.Form 3.1, and I cannot update to anything beyond that. My main page is a login page - when that is successful I navigate to the home page like so:
try
{
await _navigationService.NavigateAsync(new Uri($"/NavigationPage/{nameof(HomePage)}", UriKind.Absolute));
}
catch (Exception e)
{
Log.Error(e);
}
The navigation is not throwing any exceptions, and I'm not picking up any errors anywhere. Release notes for Xamarin 3.2 doesn't provide any clues either. I don't even know if this is a Xamarin or Prism issue. A few days of debugging and I feel no closer to figuring this out.
Has anyone else experienced this? or have any idea what could be going wrong?
Edit 1:
I finally isolated the issue - the fix was to call BeginInvokeOnMainThread when I navigate. But a few things still don't make sense to me:
This should raise an exception, so I must be hiding it somewhere. Is there anything obvious in the code below [This is the first time I've used Async, so seems likely I'm doing something wrong there]?
Why did this work with Xamarin 3.1 on not later versions
My logging confirms that the original navigation code was running on the main thread, but it still failed.
The code:
We are doing client-side google authentication with Azure, if that is successful we navigate to the home page.
First step, we connect to GooglePlay and authenticate the user
public void Login(MobileServiceClient client, Action<string, bool> onLoginComplete)
{
_client = client;
_onLoginComplete = onLoginComplete;
var signInIntent = Auth.GoogleSignInApi.GetSignInIntent(_googleApiClient);
((MainActivity)_context).StartActivityForResult(signInIntent, 1);
_googleApiClient.Connect();
}
The result comes to OnActivityResult in MainActivity.cs:
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
base.OnActivityResult(requestCode, resultCode, data);
if (requestCode == SignInId)
{
Log.Info("Received result from Google sign in");
var result = Auth.GoogleSignInApi.GetSignInResultFromIntent(data);
DroidLoginProvider.Instance.OnAuthCompleted(result);
}
}
Which calls the OnAuthCompleted method. There are a few paths here. If the token is valid we don't re-authenticate with Azure, and just retrieve the saved user details:
public void OnAuthCompleted(GoogleSignInResult result)
{
if (result.IsSuccess)
{
Log.Trace("Native google log in successful");
var signInAccount = result.SignInAccount;
var accounts = _accountStore.FindAccountsForService("monkey_chat");
if (accounts != null)
{
foreach (var acct in accounts)
{
if (acct.Properties.TryGetValue("token", out var azureToken) && acct.Properties.TryGetValue("email", out var email))
{
if (!IsTokenExpired(azureToken))
{
Log.Trace("Auth token is still valid");
_client.CurrentUser = new MobileServiceUser(acct.Username)
{
MobileServiceAuthenticationToken = azureToken
};
_onLoginComplete?.Invoke(email, true);
return;
}
Log.Trace("Auth token no longer valid");
}
}
}
// Authenticate with Azure & get a new token
var token = new JObject
{
["authorization_code"] = signInAccount.ServerAuthCode,
["id_token"] = signInAccount.IdToken
};
try
{
var mobileUser = Task.Run(async () =>
{
try
{
Log.Trace("Authenticating with Azure");
return await client.LoginAsync(MobileServiceAuthenticationProvider.Google, token).ConfigureAwait(false);
}
catch (Exception e)
{
Log.Error(e);
throw;
}
}).GetAwaiter().GetResult();
var account = new Account(_client.CurrentUser.UserId);
account.Properties.Add("token", _client.CurrentUser.MobileServiceAuthenticationToken);
account.Properties.Add("email", signInAccount.Email);
_accountStore.Save(account, "monkey_chat");
_googleUser = new GoogleUser
{
Name = signInAccount.DisplayName,
Email = signInAccount.Email,
Picture = new Uri((signInAccount.PhotoUrl != null
? $"{signInAccount.PhotoUrl}"
: $"https://autisticdating.net/imgs/profile-placeholder.jpg")),
UserId = SidHelper.ExtractUserId(mobileUser?.UserId),
UserToken = mobileUser?.MobileServiceAuthenticationToken
};
_onLoginComplete?.Invoke(signInAccount.Email, true);
}
catch (Exception ex)
{
_onLoginComplete?.Invoke(string.Empty, false);
Log.Error(ex);
}
}
else
{
_onLoginComplete?.Invoke(string.Empty, false);
}
}
My original OnLoginComplete[Not working]:
private async void OnLoginComplete(bool successful, bool isNewUser)
{
if (successful)
{
try
{
Log.Info("Starting navigation to home page");
await _navigationService.NavigateAsync(new Uri($"/NavigationPage/{nameof(HomePage)}", UriKind.Absolute)).GetAwaiter().GetResult();
}
catch (Exception e)
{
Log.Error(e);
}
}
}
New OnLoginComplete[Working]
private void OnLoginComplete(bool successful, bool isNewUser)
{
Device.BeginInvokeOnMainThread(() =>
{
if (successful)
{
try
{
Log.Info("Starting navigation to home page");
_navigationService.NavigateAsync(new Uri($"/NavigationPage/{nameof(HomePage)}", UriKind.Absolute)).GetAwaiter().GetResult();
}
catch (Exception e)
{
Log.Error(e);
}
}
});
}

Xamarin & Android - crash on exiting from method

I don't know what can I tell more.
I have this method:
public async Task<HttpResponseMessage> SendAsyncRequest(string uri, string content, HttpMethod method, bool tryReauthorizeOn401 = true)
{
HttpRequestMessage rm = new HttpRequestMessage(method, uri);
if (!string.IsNullOrWhiteSpace(content))
rm.Content = new StringContent(content, Encoding.UTF8, "application/json");
HttpResponseMessage response = await client.SendAsync(rm);
if (response.StatusCode == HttpStatusCode.Unauthorized && tryReauthorizeOn401)
{
var res = await AuthenticateUser();
if(res.user == null)
return response;
return await SendAsyncRequest(uri, content, method, false);
}
return response;
}
Nothing special.
client.SendAsync(rm) is executed, response.StatusCode is Ok.
Application just crashes when exiting from this method.
Output shows me just this assert:
12-16 20:09:22.025 F/ ( 1683): * Assertion at /Users/builder/jenkins/workspace/xamarin-android-d15-9/xamarin-android/external/mono/mono/mini/debugger-agent.c:4957, condition `is_ok (error)' not met, function:set_set_notification_for_wait_completion_flag, Could not execute the method because the containing type is not fully instantiated. assembly:<unknown assembly> type:<unknown type> member:(null)
12-16 20:09:22.025 F/libc ( 1683): Fatal signal 6 (SIGABRT), code -6 in tid 1683 (omerang.Android)
And nothing more.
client is HttpClient.
I have setting in my Android project: HttpClient Implementation set to Android.
Does anyone have any idea what could be wrong?
edit
SendAsyncRequest is used like that:
public async Task<(HttpResponseMessage response, IEnumerable<T> items)> GetListFromRequest<T>(string uri)
{
HttpResponseMessage response = await SendAsyncRequest(uri, null, HttpMethod.Get);
if (!response.IsSuccessStatusCode)
return (response, null);
string content = await response.Content.ReadAsStringAsync();
var items = JsonConvert.DeserializeObject<IEnumerable<T>>(content);
return (response, new List<T>(items));
}
Based on the provided example project code you provided
protected override async void OnStart()
{
Controller c = new Controller();
TodoItem item = await c.GetTodoItem(1);
TodoItem item2 = await c.GetTodoItem(2);
}
You are calling async void fire and forget on startup.
You wont be able to catch any thrown exceptions, which would explain why the App crashes with no warning.
Reference Async/Await - Best Practices in Asynchronous Programming
async void should only be used with event handlers, so I would suggest adding an event and handler.
based on the provided example, it could look like as follows
public partial class App : Application {
public App() {
InitializeComponent();
MainPage = new MainPage();
}
private even EventHander starting = delegate { };
private async void onStarting(object sender, EventArgs args) {
starting -= onStarting;
try {
var controller = new Controller();
TodoItem item = await controller.GetTodoItem(1);
TodoItem item2 = await controller.GetTodoItem(2);
} catch(Exception ex) {
//...handler error here
}
}
protected override void OnStart() {
starting += onStarting;
starting(this, EventArgs.Empty);
}
//...omitted for brevity
}
With that, you should now at least be able to catch the thrown exception to determine what is failing.
try to update your compileSdkVersion to a higher version and check.
also try following
> Go to: File > Invalidate Caches/Restart and select Invalidate and Restart

Using cached Cognito identity from Xamarin

When I first log into my app, I go through the following code:
auth = new Xamarin.Auth.OAuth2Authenticator(
"my-google-client-id.apps.googleusercontent.com",
string.Empty,
"openid",
new System.Uri("https://accounts.google.com/o/oauth2/v2/auth"),
new System.Uri("com.enigmadream.storyvoque:/oauth2redirect"),
new System.Uri("https://www.googleapis.com/oauth2/v4/token"),
isUsingNativeUI: true);
auth.Completed += Auth_Completed;
StartActivity(auth.GetUI(this));
Which triggers this activity:
[Activity(Label = "GoodleAuthInterceptor")]
[IntentFilter(actions: new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable },
DataSchemes = new[] { "com.enigmadream.storyvoque" }, DataPaths = new[] { "/oauth2redirect" })]
public class GoodleAuthInterceptor : Activity
{
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
Android.Net.Uri uri_android = Intent.Data;
Uri uri_netfx = new Uri(uri_android.ToString());
MainActivity.auth?.OnPageLoading(uri_netfx);
Finish();
}
}
And finally this code to link the account to Cognito:
private void Auth_Completed(object sender, Xamarin.Auth.AuthenticatorCompletedEventArgs e)
{
if (e.IsAuthenticated)
{
var idToken = e.Account.Properties["id_token"];
credentials.AddLogin("accounts.google.com", idToken);
AmazonCognitoIdentityClient cli = new AmazonCognitoIdentityClient(credentials, RegionEndpoint.USEast2);
var req = new Amazon.CognitoIdentity.Model.GetIdRequest();
req.Logins.Add("accounts.google.com", idToken);
req.IdentityPoolId = "us-east-2:79ebf8e1-97de-4d1c-959a-xxxxxxxxxxxx";
cli.GetIdAsync(req).ContinueWith((task) =>
{
if ((task.Status == TaskStatus.RanToCompletion) && (task.Result != null))
{
ShowMessage(string.Format("Identity {0} retrieved", task.Result.IdentityId));
}
else
ShowMessage(task.Exception.InnerException != null ? task.Exception.InnerException.Message : task.Exception.Message);
});
}
else
ShowMessage("Login cancelled");
}
This all works great, and after the login, I am able to use my identity/credentials to retrieve data from DynamoDB. With this object:
Amazon.DynamoDBv2.AmazonDynamoDBClient ddbc = new Amazon.DynamoDBv2.AmazonDynamoDBClient(credentials, RegionEndpoint.USEast2);
The second time I run my app, this code runs:
if (!string.IsNullOrEmpty(credentials.GetCachedIdentityId()) || credentials.CurrentLoginProviders.Length > 0)
{
if (!bDidLogin)
{
var idToken = credentials.GetIdentityId();
ShowMessage(string.Format("I still remember you're {0} ", idToken));
And if I try to use the credentials with DynamoDB (or anything, I assume) at this point, I get errors that I don't have access to the identity. I have to logout (credentials.Clear()) and login again to obtain proper credentials.
I could require that a user go through the whole login process every time my app runs, but that's a real pain because the Google login process requires the user to know how to manually close the web browser to get back to the application after authenticating. Is there something I'm missing about the purpose and usage of cached credentials? When I use most apps, they aren't requiring me to log into my Google account every time and close a web browser just to access their server resources.
It looks like the refresh token needs to be submitted back to the OAuth2 provider to get an updated id token to add to the credentials object. First I added some code to save and load the refresh_token in a config.json file:
private Dictionary<string, string> config;
const string CONFIG_FILE = "config.json";
private void Auth_Completed(object sender, Xamarin.Auth.AuthenticatorCompletedEventArgs e)
{
if (e.IsAuthenticated)
{
var idToken = e.Account.Properties["id_token"];
if (e.Account.Properties.ContainsKey("refresh_token"))
{
if (config == null)
config = new Dictionary<string, string>();
config["refresh_token"] = e.Account.Properties["refresh_token"];
WriteConfig();
}
credentials.AddLogin("accounts.google.com", idToken);
CognitoLogin(idToken).ContinueWith((t) =>
{
try
{
t.Wait();
}
catch (Exception ex)
{
ShowMessage(ex.Message);
}
});
}
else
ShowMessage("Login cancelled");
}
void WriteConfig()
{
using (var configWriter = new System.IO.StreamWriter(
Application.OpenFileOutput(CONFIG_FILE, Android.Content.FileCreationMode.Private)))
{
configWriter.Write(ThirdParty.Json.LitJson.JsonMapper.ToJson(config));
configWriter.Close();
}
}
public void Login()
{
try
{
if (!string.IsNullOrEmpty(credentials.GetCachedIdentityId()) || credentials.CurrentLoginProviders.Length > 0)
{
if (!bDidLogin)
{
var idToken = credentials.GetIdentityId();
if (ReadConfig())
{
LoginRefreshAsync().ContinueWith((t) =>
{
try
{
t.Wait();
if (!t.Result)
FullLogin();
}
catch (Exception ex)
{
ShowMessage(ex.Message);
}
});
}
else
{
credentials.Clear();
FullLogin();
}
}
}
else
FullLogin();
bDidLogin = true;
}
catch(Exception ex)
{
ShowMessage(string.Format("Error logging in: {0}", ex.Message));
}
}
private bool ReadConfig()
{
bool bFound = false;
foreach (string filename in Application.FileList())
if (string.Compare(filename, CONFIG_FILE, true) == 0)
{
bFound = true;
break;
}
if (!bFound)
return false;
using (var configReader = new System.IO.StreamReader(Application.OpenFileInput(CONFIG_FILE)))
{
config = ThirdParty.Json.LitJson.JsonMapper.ToObject<Dictionary<string, string>>(configReader.ReadToEnd());
return true;
}
}
Then refactored the code that initiates the interactive login into a separate function:
public void FullLogin()
{
auth = new Xamarin.Auth.OAuth2Authenticator(CLIENTID_GOOGLE, string.Empty, "openid",
new Uri("https://accounts.google.com/o/oauth2/v2/auth"),
new Uri("com.enigmadream.storyvoque:/oauth2redirect"),
new Uri("https://accounts.google.com/o/oauth2/token"),
isUsingNativeUI: true);
auth.Completed += Auth_Completed;
StartActivity(auth.GetUI(this));
}
Refactored the code that retrieves a Cognito identity into its own function:
private async Task CognitoLogin(string idToken)
{
AmazonCognitoIdentityClient cli = new AmazonCognitoIdentityClient(credentials, RegionEndpoint.USEast2);
var req = new Amazon.CognitoIdentity.Model.GetIdRequest();
req.Logins.Add("accounts.google.com", idToken);
req.IdentityPoolId = ID_POOL;
try
{
var result = await cli.GetIdAsync(req);
ShowMessage(string.Format("Identity {0} retrieved", result.IdentityId));
}
catch (Exception ex)
{
ShowMessage(ex.Message);
}
}
And finally implemented a function that can retrieve a new token based on the refresh token, insert it into the current Cognito credentials, and get an updated Cognito identity.
private async Task<bool> LoginRefreshAsync()
{
string tokenUrl = "https://accounts.google.com/o/oauth2/token";
try
{
using (System.Net.Http.HttpClient client = new System.Net.Http.HttpClient())
{
string contentString = string.Format(
"client_id={0}&grant_type=refresh_token&refresh_token={1}&",
Uri.EscapeDataString(CLIENTID_GOOGLE),
Uri.EscapeDataString(config["refresh_token"]));
System.Net.Http.HttpContent content = new System.Net.Http.ByteArrayContent(
System.Text.Encoding.UTF8.GetBytes(contentString));
content.Headers.Add("content-type", "application/x-www-form-urlencoded");
System.Net.Http.HttpResponseMessage msg = await client.PostAsync(tokenUrl, content);
string result = await msg.Content.ReadAsStringAsync();
string idToken = System.Json.JsonValue.Parse(result)["id_token"];
credentials.AddLogin("accounts.google.com", idToken);
/* EDIT -- discovered this is not necessary! */
// await CognitoLogin(idToken);
return true;
}
}
catch (Exception ex)
{
ShowMessage(ex.Message);
return false;
}
}
I'm not sure if this is optimal or even correct, but it seems to work. I can use the resulting credentials to access DynamoDB without having to prompt the user for permission/credentials again.
There's a very different solution I'm trying to fit with the other answer. But it's so different, I'm adding it as a separate answer.
It appears the problem was not so much related to needing to explicitly use a refresh token to get an updated access token (I think this is done implicitly), but rather needing to remember the identity token. So rather than include all the complexity of manually applying a refresh token, all that's needed is to store the identity token (which can be done in a way similar to how the refresh token was being stored). Then we just need to add that same identity token back to the credentials object when it's missing.
if (!string.IsNullOrEmpty(credentials.GetCachedIdentityId()) || credentials.CurrentLoginProviders.Length > 0)
{
if (config.Read())
{
if (config["id_token"] != null)
credentials.AddLogin(currentProvider.Name, config["id_token"]);
Edit: The problem of needing to use a refresh token does still exist. This code works if the token hasn't expired, but attempting to use these credentials after the token has expired will fail, so there is still some need to use a refresh token somehow in some cases.

Cast control does not appear on the status bar and app lock in V3

I am integratingV3 version in my app.. Notification controls and app lock controls does not appear for the device which initiated the casting . if i connect from the other devices i could see the controls..
My cast provider is as follows
public class CastOptionsProvider implements OptionsProvider {
public static final String CUSTOM_NAMESPACE = "urn:x-cast:com.test.cast.player";
// #Override
// public CastOptions getCastOptions(Context context) {
// List<String> supportedNamespaces = new ArrayList<>();
// supportedNamespaces.add(CUSTOM_NAMESPACE);
// CastOptions castOptions = new CastOptions.Builder()
// .setReceiverApplicationId(context.getString(R.string.app_id))
// .setSupportedNamespaces(supportedNamespaces)
// .build();
// return castOptions;
// }
#Override
public CastOptions getCastOptions(Context context) {
List<String> supportedNamespaces = new ArrayList<>();
supportedNamespaces.add(CUSTOM_NAMESPACE);
NotificationOptions notificationOptions = new NotificationOptions.Builder()
.setActions(Arrays.asList(MediaIntentReceiver.ACTION_SKIP_NEXT,
MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK,
MediaIntentReceiver.ACTION_STOP_CASTING), new int[]{1, 2})
.setTargetActivityClassName(CustomExpandedControlsActivity.class.getName())
.build();
CastMediaOptions mediaOptions = new CastMediaOptions.Builder()
.setImagePicker(new ImagePickerImpl())
.setNotificationOptions(notificationOptions)
.setExpandedControllerActivityClassName(CustomExpandedControlsActivity.class.getName())
.build();
return new CastOptions.Builder()
.setReceiverApplicationId(context.getString(R.string.app_id))
//.setSupportedNamespaces(supportedNamespaces)
.setCastMediaOptions(mediaOptions)
.build();
}
#Override
public List<SessionProvider> getAdditionalSessionProviders(Context appContext) {
return null;
}
private static class ImagePickerImpl extends ImagePicker {
#Override
public WebImage onPickImage(MediaMetadata mediaMetadata, int type) {
if ((mediaMetadata == null) || !mediaMetadata.hasImages()) {
return null;
}
List<WebImage> images = mediaMetadata.getImages();
if (images.size() == 1) {
return images.get(0);
} else {
if (type == ImagePicker.IMAGE_TYPE_MEDIA_ROUTE_CONTROLLER_DIALOG_BACKGROUND) {
return images.get(0);
} else {
return images.get(1);
}
}
}
}
}
Probably a bit late to help you, but I'll answer in case this helps others. I wrestled with this issue for several days. In the end the problem turned out to be in the custom receiver app we developed, not in the Android app. The presence of the playback controls in the sender app (the Android side) depends on receiving exactly the right payload in the messages sent via the message bus in the media namespace (urn:x-cast:com.google.cast.media). So if your receiver app is not providing all of the correct data structures, or sending things in an unexpected sequence, the playback controls will not show up on the Android side. To debug this you'll need to compare the logs from an app that works with the one that doesn't. You can see what messages are coming back to the Android sender by adding a listener for the media namespace channel:
public static final String MEDIA_NAMESPACE = "urn:x-cast:com.google.cast.media";
private Cast.MessageReceivedCallback messageReceivedCallback = new Cast.MessageReceivedCallback() {
#Override
public void onMessageReceived(CastDevice castDevice, String namespace, String message) {
Log.d(TAG, "Received message (" + namespace + "): " + message);
}
};
castSession.setMessageReceivedCallbacks(MEDIA_NAMESPACE, messageReceivedCallback);
In my case there were two problems. The receiver app was not sending the volume in the correct format, and we were not sending out an initial 'IDLE' message that included all of the correct media information. Any deviation from the expected message format can result in a parse error on the sender side that breaks the normal flow. If that happens you won't see any information for the loaded media or the playback controls when you begin casting. The first message sent out always seems to be an 'IDLE' message, and it looks like this in our app:
{
"type":"MEDIA_STATUS",
"status":[
{
"mediaSessionId":1,
"playbackRate":1,
"playerState":"IDLE",
"currentTime":0,
"supportedMediaCommands":15,
"volume":{
"level":1,
"muted":false
},
"media":{
"contentId":"http://your.server/movie.mp4",
"streamType":"BUFFERED",
"contentType":"application/x-mpegurl",
"metadata":{
"metadataType":1,
"images":[
{
"url":"http://your.server/some.jpg",
"width":200,
"height":200
}
],
"title":"The Movie",
"subtitle":"The Thing Worth Watching"
},
"duration":0,
"customData":{
"description":"A very cool movie that you will probably want to see."
}
},
"currentItemId":1,
"extendedStatus":{
"playerState":"LOADING",
"media":{
"contentId":"http://your.server/movie.mp4",
"streamType":"BUFFERED",
"contentType":"application/x-mpegurl",
"metadata":{
"metadataType":1,
"images":[
{
"url":"http://your.server/some.jpg",
"width":200,
"height":200
}
],
"title":"The Movie",
"subtitle":"The Thing Worth Watching"
},
"duration":0,
"customData":{
"description":"A very cool movie that you will probably want to see."
}
}
},
"repeatMode":"REPEAT_OFF"
}
],
"requestId":0
}
Your app might not be providing all of the same data, so your message will look a bit different, but you should make sure that all of the fields that you need are there and have the correct members and data types.

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.

Categories

Resources