Cancel
Showing results for 
Show  only  | Search instead for 
Did you mean: 

Redirect url - need URGENT advice..

ANSWERED

 

Hello, I have an issue with the redirect_url on Android.. 

User is directed to the Authorization Page in Chrome Custom Tabs, when I click "agree", user is not redirected back to application.. 

 

What are the rules and expectancies regarding redirect_url for FitBit API?

I find the documentation on the website very unclear.. 

 

I have already set callback_uri in many different ways, with different outcomes:

 

Local Page with Auth Code, no redirect because not specified:

http://locallhost.com/

 

Error Not Found:

- http://locallhost.com/callback

- http://locallhost.com/redirect

 

What should I use to test locally? 

 

Oops it it not you, it is us.. 

- https://www.fitbit.com

 

Setting in the Application Page are correct. 

Settings in the Manifest are the following, adapted per redirect.. 

 

<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:host="www.locallhost.com/redirect"
android:scheme="http"/>
</intent-filter>

 

Best Answer
0 Votes
65 REPLIES 65

No, nothing..

Best Answer
0 Votes

Since you're not even getting an error its like onNewIntent() isn't even getting called, which is also strange.

 

Could you send me your whole <activity></activity> block and the associated public class? - PM me it if you dont want it public

Ionic & Aria, Blaze (retired), Alta (retired), Surge (retired), Charge HR (retired), One (retired), Classic (retired) | Microsoft Surface | Google Pixel 2XL Android FitBit App
Best Answer
0 Votes

The Manifest, Callback will be to the MainActivity.. This is a demo for OAuth2 purposes, so MainActivity is the only activity in here. For now, I have not included anything regarding getting the Token or Refreshing the Token yet. This will be covered by Scribe Library. 

 

The Manifest:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.gebruiker.scribetest">

<uses-permission android:name="android.permission.INTERNET" />

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity"
android:launchMode="singleInstance">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:host="fitbitcallback"
android:scheme="niels"/>
</intent-filter>
</activity>
</application>

</manifest>

 

The MainActivity 


public class MainActivity extends AppCompatActivity {

private String clientid = "xxxxx";
private String client_secret = "xxxxxxxxxxxx";
private String AUTHCODE_URL = "https://www.fitbit.com/oauth2/authorize?response_type=code&client_id=22CJ69&scope=activity%20heartrate%20location%20profile%20sleep";
private static final String TAG = "MyActivity";
String string;
Uri uri;

//here you create an instance of the FitBit20Service Object and of the FitBitApi that is used to initiate the authentication
private OAuth20Service service = new ServiceBuilder(clientid)
.apiSecret(client_secret)
.apiKey(clientid)
.scope("activity heartrate location profile sleep")
.build(FitBitApi.instance());

//create an instance of Scanner so you can read the output from the FitBit servers
private Scanner in = new Scanner(System.in);

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

//this code you use in the Sign Up Button/ Activity
LaunchTabs_getCode();

//retrieve parameter from the Intent
onNewIntent(getIntent());
uri = Uri.parse(string);
Set<String> args = uri.getQueryParameterNames();
String returnedCode = uri.getQueryParameter("code");
System.out.println(returnedCode);

//use this code as a parameter in retrieving the accesstoken and refreshtoken
//this needs to be done on a separate thread, not on the UI, otherwise you get an error
//store the accesstoken and the refreshtoken in FireBase for this user
//new OAuthTask(code).execute();
}

@Override
protected void onNewIntent(Intent intent) {

super.onNewIntent(intent);
Log.d(TAG, intent.toString());
string = intent.getDataString();
}

public void LaunchTabs_getCode(){

String authorizationUrl = service.getAuthorizationUrl();
System.out.println(authorizationUrl);

final CustomTabsIntent intent = new CustomTabsIntent.Builder().build();
final String url = AUTHCODE_URL;
intent.launchUrl(this,Uri.parse(url));

}
}
Best Answer
0 Votes

Okay, can you try splitting up your classes? Instead of having one class that both gets and tries to process the returned code.

 

Instead, have one class that sends the user off to Fibit for authorisation and a second class that take the input from Chrome Tab. I'm thinking the problem is once your users are finished in the Chrome Tab android is reusing the MainActivity class and never calling onNewIntent() - since the class has already been initiated.

 

I'm thinking something like this:

 

The Manifest:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.gebruiker.scribetest">

    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        
        <activity android:name=".MainActivity"
            android:launchMode="singleInstance">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        
        <activity
	        android:name=".RedirectActivity"
	        android:label="RedirectActivity"
	        android:theme="@style/AppTheme.NoActionBar">
            <intent-filter>
                <action android:name="android.intent.action.VIEW"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="android.intent.category.BROWSABLE"/>
                <data android:host="fitbitcallback"
                    android:scheme="niels"/>
            </intent-filter>
        </activity>
    </application>

</manifest>

The MainActivity:

public class MainActivity extends AppCompatActivity {

    private String clientid = "xxxxx";
    private String client_secret = "xxxxxxxxxxxx";
    private String AUTHCODE_URL = "https://www.fitbit.com/oauth2/authorize?response_type=code&client_id=22CJ69&scope=activity%20heartrate%20location%20profile%20sleep";
    private static final String TAG = "MyActivity";

    //here you create an instance of the FitBit20Service Object and of the FitBitApi that is used to initiate the authentication
    private OAuth20Service service = new ServiceBuilder(clientid)
            .apiSecret(client_secret)
            .apiKey(clientid)
            .scope("activity heartrate location profile sleep")
            .build(FitBitApi.instance());

    //create an instance of Scanner so you can read the output from the FitBit servers
    private Scanner in = new Scanner(System.in);
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //this code you use in the Sign Up Button/ Activity
        LaunchTabs_getCode();
    }

    public void LaunchTabs_getCode(){

        String authorizationUrl = service.getAuthorizationUrl();
        System.out.println(authorizationUrl);

        final CustomTabsIntent intent = new CustomTabsIntent.Builder().build();
        final String url = AUTHCODE_URL;
        intent.launchUrl(this,Uri.parse(url));

    }
}

The (new) RedirectActivity:

public class RedirectActivity extends AppCompatActivity {

    private static final String TAG = "MyActivity";
    String string;
    Uri uri;

    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        Log.d(TAG, intent.toString());
        string = intent.getDataString();
    }
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //retrieve parameter from the Intent
        onNewIntent(getIntent());
        
        uri = Uri.parse(string);
        Set<String> args = uri.getQueryParameterNames();
        String returnedCode = uri.getQueryParameter("code");
        System.out.println(returnedCode);
    }
}
Ionic & Aria, Blaze (retired), Alta (retired), Surge (retired), Charge HR (retired), One (retired), Classic (retired) | Microsoft Surface | Google Pixel 2XL Android FitBit App
Best Answer

I get directed to the Authorization Page and when clicking "Agree" to the RedirectActivity. 

Still no "returnedCode" though.. 

In LogCat I find "E/MultiWindowProxy: getServiceInstance failed!"

I don't know what it means, only thing in red I can find.. 

Best Answer
0 Votes

I just remembered how much I love debuging Java code. The compiler give you such detailed error messages!

 

I'm assuming your log level is set to DEBUG, so I've updated the RedirectActivity class with a bunch of log lines so we can trace whats going on. Can you run this and let me see what appears in the log message - See message bellow

Ionic & Aria, Blaze (retired), Alta (retired), Surge (retired), Charge HR (retired), One (retired), Classic (retired) | Microsoft Surface | Google Pixel 2XL Android FitBit App
Best Answer

I must be having a bad day!

 

public class RedirectActivity extends AppCompatActivity {

    private static final String TAG = "MyActivity";
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
	Log.d(TAG, "FINDLINE 1");
        super.onCreate(savedInstanceState);
	Log.d(TAG, "FINDLINE 2");
        setContentView(R.layout.activity_main); // For now this will work, but once everthing is fine change this to what ever layout your using once a user has completed authorisation
	Log.d(TAG, "FINDLINE 3");
        
        //retrieve parameter from the Intent
	Log.d(TAG, "FINDLINE 4");
        List<String> params = getIntent().getData().getPathSegments();
	Log.d(TAG, "FINDLINE 5");
        
        final int size = params.size();
	Log.d(TAG, "FINDLINE 6 There are " + size + " segments");
	for (int i = 0; i < size; i++) {
	    object = params.get(i);
	    Log.d(TAG, "FINDLINE 6." + i + " = " + object);
	}
    }
}
Ionic & Aria, Blaze (retired), Alta (retired), Surge (retired), Charge HR (retired), One (retired), Classic (retired) | Microsoft Surface | Google Pixel 2XL Android FitBit App
Best Answer

I don't really understand what you are doing here.. 

 

Have adapted a few things:

- changed layout to layout of activity we land on 

- added "onNewIntent(getIntent()); to have something to work with 

- declared object as a String

- changed "alist" to "params", I think you meant that.. 

 

For the moment this is not returning anything in the "LogCat Debug"

public class RedirectActivity extends AppCompatActivity {

    private static final String TAG = "MyActivity";
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
	Log.d(TAG, "FINDLINE 1");
        super.onCreate(savedInstanceState);
	Log.d(TAG, "FINDLINE 2");
        setContentView(R.layout.activity_redirect); // For now this will work, but once everthing is fine change this to what ever layout your using once a user has completed authorisation
	Log.d(TAG, "FINDLINE 3");
        
onNewIntent(getIntent()); //retrieve parameter from the Intent Log.d(TAG, "FINDLINE 4"); List<String> params = getIntent().getData().getPathSegments(); Log.d(TAG, "FINDLINE 5"); final int size = params.size(); Log.d(TAG, "FINDLINE 6 There are " + size + " segments"); for (int i = 0; i < size; i++) { String object = params.get(i); Log.d(TAG, "FINDLINE 6." + i + " = " + object); } } }

 

Best Answer
0 Votes

Thank you so much for the help.. I don't understand why they make it so hard to get to an API.. 

The value for me is in using the API, making catching visualizations with good UX.. My functional design is ready, just this OAuth2.. Pff.. 🙂

Best Answer
0 Votes

Basically I'm trying to get it to print a each line into LogCat. That way if it does spit out an error we can see whats causing it by checking which FINDLINE's its coming between.

 

We shouldnt need the onNewIntent() function anymore since params directly from getIntent(), and all onNewIntent() was doing was storing the getIntent value as a string anyway.

 

Under FINDLINE 4 I'm trying to get all the parameters from the intent path and putting them in a List.

 

FINDLINE 6 should tell us how many parameters are being sent back by Chrome Tab and the for loop is supposed to just tell us what parameters are being returned.

 

I'm really not sure why nothing is bring printed in LogCat though, I wonder if its worth replacing all Log.d() function with System.out.println().

 

Your app is going to Fibit and authorising users, Chrome tab is then sending users back to the correct class. There is just something not right about how we're getting the returned code from the intent and I'm wondering if Chrome tab is even returning a code or not. So I'm trying to get everthing returned from the intent and print it out to logs so we can actually inspect whats being sent back

Ionic & Aria, Blaze (retired), Alta (retired), Surge (retired), Charge HR (retired), One (retired), Classic (retired) | Microsoft Surface | Google Pixel 2XL Android FitBit App
Best Answer

I get nothing back in the Debug Log in Logcat. Not when using Log.d, not when using System.out.println(). Left onNewIntent() out.. 

 

My second activity now is//

 

public class RedirectActivity extends AppCompatActivity {

private static final String TAG = "MyActivity";

@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d(TAG, "FINDLINE 1");
super.onCreate(savedInstanceState);
Log.d(TAG, "FINDLINE 2");
setContentView(R.layout.activity_main); // For now this will work, but once everthing is fine change this to what ever layout your using once a user has completed authorisation
Log.d(TAG, "FINDLINE 3");

//retrieve parameter from the Intent
Log.d(TAG, "FINDLINE 4");
List<String> params = getIntent().getData().getPathSegments();
Log.d(TAG, "FINDLINE 5");

final int size = params.size();
Log.d(TAG, "FINDLINE 6 There are " + size + " segments");
for (int i = 0; i < size; i++) {
String object = params.get(i);
Log.d(TAG, "FINDLINE 6." + i + " = " + object);


}
}
}
Best Answer
0 Votes

Okay, i'm now setting up my own Android build enviroment. this may take some time - I will be back...

Ionic & Aria, Blaze (retired), Alta (retired), Surge (retired), Charge HR (retired), One (retired), Classic (retired) | Microsoft Surface | Google Pixel 2XL Android FitBit App
Best Answer

ok, great, thanks! 

Best Answer
0 Votes

Sorry about the wait, I forgot just how long it takes to setup a working development enviroment on Windows - and how slow the emulator was!

 

It turns out I made two mistakes in my previous code.

  • Uri.parse() - is not required as getIntent().getData() already returns a Uri object
  • getPathSegments() - was also wrong, it should have been getQueryParameter() already

Here is a working example:

 

MainActivity:

 

public class MainActivity extends AppCompatActivity {

    private String clientid = "XXXXXX";
    private static final String TAG = "MyActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //this code you use in the Sign Up Button/ Activity
        getApiCodeFromFitbit();
    }

    public void getApiCodeFromFitbit() {
        String url = "https://www.fitbit.com/oauth2/authorize?" +
                "response_type=code" +
                "&client_id=" + clientid +
                "&scope=activity"+
                "&redirect_uri=niels://fitbitcallback";

        CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
        builder.setToolbarColor(ContextCompat.getColor(this, R.color.colorAccent));
        // set toolbar color and/or setting custom actions before invoking build()
        // Once ready, call CustomTabsIntent.Builder.build() to create a CustomTabsIntent
        CustomTabsIntent customTabsIntent = builder.build();
        // and launch the desired Url with CustomTabsIntent.launchUrl()
        customTabsIntent.launchUrl(this, Uri.parse(url));
    }
}

RedirectActivity:

 

public class RedirectActivity extends AppCompatActivity {

    private static final String TAG = "MyActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main); // For now this will work, but once everthing is fine change this to what ever layout your using once a user has completed authorisation

        //retrieve parameter from the Intent
        Uri returnUrl = getIntent().getData();
        if (returnUrl != null) {
            String apiCode = returnUrl.getQueryParameter("code");
            Log.d(TAG, "API Code returned from Fitbit " + apiCode);
        } else {
            Log.d(TAG, "Something is wrong with the return value from Fitbit. getIntent().getData() is NULL?");
        }

    }
}

In all my testing apiCode is correctly populated with the OAuth2 code from Fitbit. I've uploaded the full project code to my Git server

 

Let me know how you get on with this code

Ionic & Aria, Blaze (retired), Alta (retired), Surge (retired), Charge HR (retired), One (retired), Classic (retired) | Microsoft Surface | Google Pixel 2XL Android FitBit App
Best Answer

Hi man, 

 

Thank you very much for your help.. 

 

This indeed functions and I get the code back in LogCat. 

I see you use a very different approach from what was proposed in the Scribe library I used. 

This is a much more straightforward way of doing it. 

 

Seeing this I would want to lose the dependency on Scribe, and also use the direct approach for the token request, the refresh and the revoke flow. For these three requests, I need custom Headers though. Is it possible to add custom headers to a url? 

 

Furthermore, two of them are POST requests.. 

Also, this does not involve going to an Authorization page anymore, they are direct requests.

So Chrome Custom Tabs is not necessary anymore, but can/ should I keep using it? 

 

What would you do? 

Best Answer
0 Votes

Yeah I tried setting up my example to use the Scribe library but in the end it was all getting too confusing.

 

I need to pop out for a bit, but let me have a think about how I would do it and I'll get back to you.

 

I think I might create a new class to handle the resquests, that how I would solve it in my web app so the principle should work for an Android app too but I'll have a play about with my example code and see if I can come up with something eligent

Ionic & Aria, Blaze (retired), Alta (retired), Surge (retired), Charge HR (retired), One (retired), Classic (retired) | Microsoft Surface | Google Pixel 2XL Android FitBit App
Best Answer

Thanks for your time and patience..

You are of great help to me, you cannot imagine.. 🙂

Best Answer
0 Votes

@NielsWearablesI just want to check, you said before you were writing to API level 17. Is that definitly the API level you want your app to be aimed at?

 

I'm running into a few things I'm trying to do that require higher API level, but if you need it to be 17 I can find other ways to do the same things - I just wanted to check your requirments

Ionic & Aria, Blaze (retired), Alta (retired), Surge (retired), Charge HR (retired), One (retired), Classic (retired) | Microsoft Surface | Google Pixel 2XL Android FitBit App
Best Answer
0 Votes

Hi, 

 

I would say I would like to target at least 90% of available Android devices out there.. That means API 19 or less. That is the target I would like to have for percentage of devices.

 

With regards to the libraries used, it is a very positive thing to avoid library dependencies. The fact that you can find an approach that avoids Scribe is another plus. 

 

@DavidSFitbit, I have been getting super advice from stuart since a few days. Maybe you can work out an integrated OAuth2 approach for Android and other platforms with him. I have to say, it has been a pleasure conversating with him, been a great help. I am into UX, Data Visualization, IoT and this sort of stuff, not a Backend guy.. It could be a great added value for FitBit to have a simplified onboarding procedure regarding OAuth2 for different platforms. 

 

@stuartma, thank you again for all the help! 

Best Answer
0 Votes

Right its done. In the end I ended up rethinking most of it and rewrote everything else. I’ve updated the Git repo for you to see a working example.

 

In the end I went back to ScribeJava, anything else was just reinventing the wheel, but this time I’ve wrapped it all in a helper class (NxFitbitHelper) tokens to make things simpler, this is where you setup your ClientID and access.

 

MainActivity.java – now just calls a static method of the helper class to get the users authenticated

 

RedirectActivity.java – creates a instance of the helper class and sets up the access token .requestAccessTokenFromIntent(). Everything bellow that line is just examples of how to get a query information. You’ll probably use the last example most by making direct calls to the API, but if you find your calling the same end point several times setting up helper methods like I have for Profile might be better for you so you dont hit any rate limits

 

NxFitbitHelper.java is the helper class but inorder to get Scribe to work you'll need to include FitbitApi20.java and Fitbit20ServiceImpl.java so we can setup the Authorization header - the non-standard OAuth element to the API.

 

You shoud be able to seriablise the helper class and pass it around but I've not tested that - there are limits to my android programming skills.

 

I think this is about the limit of help I can give you without actual seeing your source code! But if you need anything else added to the helper class, or help pulling data out of the returned JSON I should be able to do that.

 

Let me know how you get on, I'm intrigued to see what you come up with now

Ionic & Aria, Blaze (retired), Alta (retired), Surge (retired), Charge HR (retired), One (retired), Classic (retired) | Microsoft Surface | Google Pixel 2XL Android FitBit App
Best Answer
0 Votes