I want to get the event of New install or App open/re-open events in my Android App whenever user click on any dynamic link.
Following events are captured in Analytics as per documentation:
dynamic_link_first_open
dynamic_link_app_open
But I can't find any way to get these from sample listener.
I have found solution to my above question. Sharing details here.
Below code is tested from PlayStore also.
You can get mentioned two events through pendingDynamicLinkData callback object received from addOnSuccessListener.
Complete code to get link and associated Dynamic link data here.
FirebaseDynamicLinks.getInstance()
.getDynamicLink(getIntent())
.addOnSuccessListener(this, pendingDynamicLinkData -> {
// Get deep link from result (may be null if no link is found)
try {
Uri deepLink = null;
if (pendingDynamicLinkData != null) {
deepLink = pendingDynamicLinkData.getLink();
sendInstallDetailToAPI(pendingDynamicLinkData.getExtensions());
}
CgUtils.showLog(TAG, "getDynamicLink:onSuccess" + deepLink);
} catch (Exception e) {
CgUtils.showLog(TAG, "getDynamicLink:onFailure" + e);
}
})
.addOnFailureListener(this, e -> CgUtils.showLog(TAG, "getDynamicLink:onFailure" + e));
Below method to send the Dynamic Link data to your backend API if you want.
private void sendInstallDetailToAPI(Bundle deepBundle) {
Bundle deepLinkData = deepBundle.getBundle("scionData");
if (deepLinkData != null) {
Bundle appReOpenBundle = deepLinkData.getBundle("dynamic_link_app_open");
boolean isInstall = false;
String medium = "", source = "", campaign = "", shortLink = "";
if (appReOpenBundle != null) {
medium = appReOpenBundle.getString("medium", "NA");
source = appReOpenBundle.getString("source", "NA");
campaign = appReOpenBundle.getString("campaign", "NA");
shortLink = appReOpenBundle.getString("dynamic_link_link_id", "NA");
}
Bundle appFirstOpenBundle = deepLinkData.getBundle("dynamic_link_first_open");
if (appFirstOpenBundle != null) {
isInstall = true;
medium = appFirstOpenBundle.getString("medium", "NA");
source = appFirstOpenBundle.getString("source", "NA");
campaign = appFirstOpenBundle.getString("campaign", "NA");
shortLink = appFirstOpenBundle.getString("dynamic_link_link_id", "NA");
}
// Send ABOVE detail to your respective APIs
}
}
Now If isInstall flag is true that means it's first time open after install else reopen.
Related
I check pending deferred deeplinks on different devices and android versions
and some devices cant get deferred deeplink.
But when I reset Advertising ID in device settings, device starts get deferred deeplink
void Start()
{
if (FB.IsInitialized)
{
FB.ActivateApp();
}
else
{
FB.Init(() => {
FB.ActivateApp();
FB.Mobile.FetchDeferredAppLinkData(DeferredAppLinkData);
});
}
}
void DeferredAppLinkData(IAppLinkResult result)
{
string targetUrl = string.Empty;
completeLink.text = result.ToString();
if (!String.IsNullOrEmpty(result.TargetUrl))
{
targetUrl = result.TargetUrl;
deepText.text = targetUrl;
}
deepText.text += "'n = " + targetUrl;
}
I create dynamic link and I want to send some specific parameter, like:
"https://mydynamiclink/?link=" + link + "&msgid=" + id + "&apn=myapn".
link field looks like "https://play.google.com/store/apps/details/?id=com.myApp&msgid=myId&apn=myapn"
When I open my app after taping on this link - I receive PendingDynamicLinkData and can get link from it, but not some custom data. (pendingDynamicLinkData.getLink() returns my link without "&msgid=..." - I'm getting string "https://play.google.com/store/apps/details/?id=com.myApp")
How can I add my msgid field and get it after all?
I've found solution
String query = "";
try {
query = URLEncoder.encode(String.format("&%1s=%2s", "msgid", id), "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
final String link = "https://play.google.com/store/apps/details/?id=com.myApp" + query;
After such encoding pendingDynamicLinkData.getLink() returns me https://play.google.com/store/apps/details/?id=com.myApp&msgid=myId
Accepted answer didn't work out fine for me, all i needed to do was check if the link was for a user's profile and not a blog post, so i can redirect to my ProfileActivity instead.
private void generateDynamicLink() {
//build link normally and add queries like a normal href link would
String permLink = getLink() + "?route=profile&name=" + getProfileName()
+ "&category=" + getUserPracticeCategory()
+ "&picture=" + getProfilePicture();
FirebaseDynamicLinks.getInstance().createDynamicLink()
.setLink(Uri.parse(permLink))
.setDynamicLinkDomain(Constants.DYNAMIC_LINK_DOMAIN)
.setAndroidParameters(new
DynamicLink.AndroidParameters.Builder().build())
.setSocialMetaTagParameters(
new DynamicLink.SocialMetaTagParameters.Builder()
.setTitle("Enter Title")
.setDescription("Enter Desc here")
.setImageUrl(Uri.parse(getProfilePicture()))
.build())
.buildShortDynamicLink()
.addOnCompleteListener(this, task -> {
if (task.isSuccessful()) {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_TEXT,task.getResult().getShortLink());
intent.setType("text/plain");
startActivity(intent);
} else {
Utils.snackBar(tvAddress, "Failed to Generate Profile Link, Try
Again");
}
});
}
and when a user navigates into my app using the generated link, it goes to a post detail activity, because i made that activity the only browsable activity in my manifest. i then have to use the route query to determine if the incoming link is a blog post or a shared user profile.
private void retrieveDynamicLink() {
FirebaseDynamicLinks.getInstance().getDynamicLink(getIntent())
.addOnSuccessListener(this, pendingDynamicLinkData -> {
if (pendingDynamicLinkData == null) {
retrieveLocalIntent();
} else {
Toast.makeText(context, "Resolving Link, Please Wait...", Toast.LENGTH_LONG).show();
if (pendingDynamicLinkData.getLink().getQueryParameter("route") != null) {
if (Objects.requireNonNull(pendingDynamicLinkData.getLink().getQueryParameter("route")).equalsIgnoreCase("profile")) {
try {
Uri uri = pendingDynamicLinkData.getLink();
String permLink = uri.toString().split("\\?")[0];
Intent intent = new Intent(this, ProfileActivity.class);
intent.putExtra(ProfileActivity.PROFILE_NAME, uri.getQueryParameter("name"));
intent.putExtra(ProfileActivity.PROFILE_CATEGORY, uri.getQueryParameter("category"));
intent.putExtra(ProfileActivity.PROFILE_PICTURE, uri.getQueryParameter("picture"));
intent.putExtra(Utils.POST_PERMLINK, permLink);
startActivity(intent);
this.finish();
} catch (NullPointerException e) {
Toast.makeText(context, "Unable to View User Profile", Toast.LENGTH_SHORT).show();
}
}
} else {
postHrefLink = pendingDynamicLinkData.getLink().toString();
getPostDetail.getData(postHrefLink);
}
}
})
.addOnFailureListener(this, e ->
retrieveLocalIntent()
);
}
Hope this helps.
1 First Change your Dynamic Link in firebase console from http://exampleandroid/test to http://exampleandroid/test?data
2. You send the query paramter data with this
DynamicLink dynamicLink = FirebaseDynamicLinks.getInstance().createDynamicLink()
// .setLink(dynamicLinkUri)
.setLink(Uri.parse("http://exampleandroid/test?data=dsads"))
.setDomainUriPrefix("https://App_Name.page.link")
// Open links with this app on Android
.setAndroidParameters(new DynamicLink.AndroidParameters.Builder().build())
// Open links with com.example.ios on iOS
.setIosParameters(new DynamicLink.IosParameters.Builder("com.appinventiv.ios").build())
.buildDynamicLink();
dynamicLinkUri = dynamicLink.getUri();
Let's say that You want to create the following URL:
https://www.myawesomesite.com/turtles/types?type=1&sort=relevance#section-name
For this you can do following
Uri.Builder builder = new Uri.Builder();
builder.scheme("https")
.authority("www.myawesomesite.com")
.appendPath("turtles")
.appendPath("types")
.appendQueryParameter("type", "1")
.appendQueryParameter("sort", "relevance")
.fragment("section-name");
String myUrl = builder.build().toString();
I'm following this sample tutorial for https://developer.xamarin.com/guides/xamarin-forms/cloud-services/authentication/oauth/
After download the sample, I follow the instructions and change all my clientID endpoint.I got into the Google Sign In page and browser manage to close. After browser close, it always got into OnAuthError and error message is "Error authenticating : invalid_request"
I couldn't get the OnAuthCompleted fired. Its always got into OnAuthError.
void OnLoginClicked(object sender, EventArgs e)
{
string clientId = null;
string redirectUri = null;
switch (Device.RuntimePlatform)
{
case Device.iOS:
clientId = Constants.iOSClientId;
redirectUri = Constants.iOSRedirectUrl;
break;
case Device.Android:
clientId = Constants.AndroidClientId;
redirectUri = Constants.AndroidRedirectUrl;
break;
}
var authenticator = new OAuth2Authenticator(
clientId,
null,
Constants.Scope,
new Uri(Constants.AuthorizeUrl),
new Uri(redirectUri),
new Uri(Constants.AccessTokenUrl),
null,
true);
authenticator.Completed += OnAuthCompleted;
authenticator.Error += OnAuthError;
AuthenticationState.Authenticator = authenticator;
var presenter = new Xamarin.Auth.Presenters.OAuthLoginPresenter();
presenter.Login(authenticator);
}
async void OnAuthCompleted(object sender, AuthenticatorCompletedEventArgs e)
{
var authenticator = sender as OAuth2Authenticator;
if (authenticator != null)
{
authenticator.Completed -= OnAuthCompleted;
authenticator.Error -= OnAuthError;
}
User user = null;
if (e.IsAuthenticated)
{
// If the user is authenticated, request their basic user data from Google
// UserInfoUrl = https://www.googleapis.com/oauth2/v2/userinfo
var request = new OAuth2Request("GET", new Uri(Constants.UserInfoUrl), null, e.Account);
var response = await request.GetResponseAsync();
if (response != null)
{
// Deserialize the data and store it in the account store
// The users email address will be used to identify data in SimpleDB
string userJson = await response.GetResponseTextAsync();
user = JsonConvert.DeserializeObject<User>(userJson);
}
if (account != null)
{
store.Delete(account, Constants.AppName);
}
await store.SaveAsync(account = e.Account, Constants.AppName);
await DisplayAlert("Email address", user.Email, "OK");
}
}
void OnAuthError(object sender, AuthenticatorErrorEventArgs e)
{
var authenticator = sender as OAuth2Authenticator;
if (authenticator != null)
{
authenticator.Completed -= OnAuthCompleted;
authenticator.Error -= OnAuthError;
}
Debug.WriteLine("Authentication error: " + e.Message);
}
I have read through all the related links. The closest questions I got from SO is this : Xamarin.Forms Google API Authenticating Users with an Identity Provider
but it still failed after configure the setting and update my packages. I'm using Xamarin.Auth 1.5.0.3 (latest stable version)
Any real hero outside can run this tutorial and make it work?
The problem is that I am using Type : "Others" at the Google Console Developer website. When I switch back the Type: "Android". It's working.
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.
I programmed an app that can send a message to twitter with an image attached. It works! I tested it on several devices and asked other people to do the same. It even works for a Direct Message when a twitter friend is selected. However, it does not work when "Direct Message" is selected. This forces the user to select a friend directly instead of selecting him via "Direct Message" (which is really strange) otherwise the picture is not attached. Just have a look at the screenshot:
Here is my Xamarin Android programming code. Let me know how to fix it. Currently, all options work, even selecting my friend but not "Direct Message". I also need to tell that I do not have any issue with the twitter text I expect to see in the tweet.
public bool TweetImage(Bitmap imageToTweet)
{
var messageIntent = context.FindMessageIntent(this.twitterConstants.PackageName);
if (messageIntent == null)
{
return false;
}
string outputFileBMP = SaveBitmap(imageToTweet);
context.Tweet(messageIntent, outputFileBMP, this.twitterConstants.DefaultTwitterText, this.twitterConstants.ChooserMessage);
return true;
}
and
public static Intent FindMessageIntent(this ContextWrapper contextWrapper, params string[] packageNames)
{
Intent wantedIntent = new Intent();
wantedIntent.SetType("text/plain");
var resolveInfos = contextWrapper.PackageManager.QueryIntentActivities(wantedIntent, PackageInfoFlags.MatchDefaultOnly);
var result = (from r in resolveInfos
from p in packageNames
where p == r.ActivityInfo.PackageName
select p).FirstOrDefault();
if (result != null)
{
wantedIntent.SetPackage(result);
return wantedIntent;
}
return null;
}
and
public static void Tweet(this ContextWrapper contextWrapper, Intent messageIntent, string filePath = null, string message = null, string chooserMessage = null)
{
if (filePath != null)
{
using (var file = new Java.IO.File(filePath))
{
messageIntent.PutExtra(Intent.ExtraStream, Android.Net.Uri.FromFile(file));
}
}
if (message != null)
{
messageIntent.PutExtra(Intent.ExtraText, message);
}
if (chooserMessage != null)
{
using (var chooser = Intent.CreateChooser(messageIntent, chooserMessage))
{
contextWrapper.StartActivity(chooser);
}
return;
}
contextWrapper.StartActivity(messageIntent);
}
Please note that I am using Android and need a solution based on Android (intent based).
Sadly, Twitter don't provide API access for uploading images via DM.
If you are able to use Twitter's private API, you should be able to attach a media_id to your DM. But other than that, you're out of luck.
Sorry.