One assumed using the Tumblr API to upload images would be easy. It isn't. (EDIT It is now, see Edit 2 at the end of this entry)
My app is supposed to upload an image to tumblr
. I would prefer doing that from a service but for now I use an activity that closes itself as soon as its done uploading. In OnCreate()
the user is authenticated:
consumer = new CommonsHttpOAuthConsumer(CONSUMER_KEY, CONSUMER_SECRET);
// It uses this signature by default
// consumer.setMessageSigner(new HmacSha1MessageSigner());
provider = new CommonsHttpOAuthProvider(REQUEST_TOKEN_URL,ACCESS_TOKEN_URL,AUTH_URL);
String authUrl;
try
{
authUrl = provider.retrieveRequestToken(consumer, CALLBACK_URL);
Log.d(TAG, "Auth url:" + authUrl);
startActivity(new Intent("android.intent.action.VIEW", Uri.parse(authUrl)));
}
This opens a browser activity where the user can add username and passoword and then the app returns to the activity (this is also why I have to use an activity, I don't know how to do this from a service)
Returning from the browser the data is extracted:
Uri uri = context.getIntent().getData();
if (uri != null && uri.toString().startsWith(CALLBACK_URL))
{
Log.d(TAG, "uri!=null");
String verifier = uri.getQueryParameter("oauth_verifier");
Log.d(TAG, "verifier"+verifier);
try
{
provider.setOAuth10a(true);
provider.retrieveAccessToken(consumer, verifier);
Log.d(TAG, "try");
}
catch (Exception e)
{
Log.e(TAG, e.toString());
e.printStackTrace();
}
OAUTH_TOKEN = consumer.getToken();
OAUTH_SECRET = consumer.getTokenSecret();
Most of these two snippets I got from here and they work well.
With these tokens I can now try putting data on tumblr. When I try to add Text this works fine using this method:
private void createText()
{
if(!OAUTH_TOKEN.equals(""))
{
HttpContext context = new BasicHttpContext();
HttpPost request = new HttpPost("http://api.tumblr.com/v2/blog/" + blogname + ".tumblr.com/post");
List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(2);
nameValuePairs.add(new BasicNameValuePair("type", "text"));
nameValuePairs.add(new BasicNameValuePair("body", "this is just a test"));
try
{
request.setEntity(new UrlEncodedFormEntity(nameValuePairs));
}
catch (UnsupportedEncodingException e1)
{
Log.e(TAG, e1.toString());
e1.printStackTrace();
}
if (consumer == null)
{
consumer = new CommonsHttpOAuthConsumer(OAuthConstants.TUMBR_CONSUMERKEY, OAuthConstants.TUMBR_SECRETKEY);
}
if (OAUTH_TOKEN == null || OAUTH_SECRET == null)
{
Log.e(TAG, "Not logged in error");
}
consumer.setTokenWithSecret(OAUTH_TOKEN, OAUTH_SECRET);
try
{
consumer.sign(request);
}
catch (OAuthMessageSignerException e)
{
}
catch (OAuthExpectationFailedException e)
{
}
catch (OAuthCommunicationException e)
{
}
HttpClient client = new DefaultHttpClient();
//finally execute this request
try
{
HttpResponse response = client.execute(request, context);
HttpEntity responseEntity = response.getEntity();
if (responseEntity != null)
{
Log.d(TAG, "responseEntety!=null");
try
{
Log.d(TAG, EntityUtils.toString(responseEntity));
}
catch (ParseException e)
{
e.printStackTrace();
Log.e(TAG, e.toString());
}
catch (IOException e)
{
e.printStackTrace();
Log.e(TAG, e.toString());
} // gives me {"meta":{"status":401,"msg":"Not Authorized"},"response":[]} when I try to upload a photo
}
else
{
Log.d(TAG, "responseEntety==null");
}
}
catch (ClientProtocolException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
catch (IOException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
}
PostToTumblr.this.finish();
}
As you can see here http://www.tumblr.com/blog/snapnowandroid (at least as of this time) the text "this is just a test" is posted.
However, when I try to post images, it gets strange. Now I have checked around and apparently this is a well known issue with the tumblr API, which has excessively been discussed here and some have solved it in other programming languages (for example here) but I have been unable to repeat those successes.
The method (in its entirety below) has the exact same structure to the above method (that works), the nameValuePairs are just different
The method is given a Bitmap variable called photo:
private void uploadToTumblr(Bitmap photo)
This bitmap is converted into an array:
ByteArrayOutputStream stream = new ByteArrayOutputStream();
photo.compress(Bitmap.CompressFormat.PNG, 100, stream);
byte[] bytes = stream.toByteArray();
The nameValuePairs are filled as follows:
nameValuePairs.add(new BasicNameValuePair(URLEncoder.encode("type", enc), URLEncoder.encode("photo", enc)));
nameValuePairs.add(new BasicNameValuePair(URLEncoder.encode("caption", enc), URLEncoder.encode(text, enc)));
nameValuePairs.add(new BasicNameValuePair("data", Base64.encodeToString(bytes, Base64.URL_SAFE)));
The result is a {"meta":{"status":400,"msg":"Bad Request"},"response":{"errors":["Error uploading photo."]}}
from the tumblr api.
I have tries encoding the picture differently as discribed in this article but without any changes.
//http://www.coderanch.com/t/526487/java/java/Java-Byte-Hex-String
final char[] hexArray = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
char[] hexChars = new char[bytes.length * 3];
int v;
for ( int j = 0; j < bytes.length; j++ )
{
v = bytes[j] & 0xFF;
hexChars[j * 3] = '%';
hexChars[j * 3 + 1] = hexArray[v >>> 4];
hexChars[j * 3 + 2] = hexArray[v & 0x0F];
}
String s = new String(hexChars);
s = URLEncoder.encode(s, enc);
nameValuePairs.add(new BasicNameValuePair(URLEncoder.encode("data", enc), s));
Here the entire method (without the hex encoding):
private void uploadToTumblr(Bitmap photo)
{
if(!OAUTH_TOKEN.equals(""))
{
ByteArrayOutputStream stream = new ByteArrayOutputStream();
photo.compress(Bitmap.CompressFormat.PNG, 100, stream);
byte[] bytes = stream.toByteArray();
String text ="SNAP";
HttpContext context = new BasicHttpContext();
HttpPost request = new HttpPost("http://api.tumblr.com/v2/blog/" + blogname + ".tumblr.com/post");
List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(2);
String enc = "UTF-8";
try
{
nameValuePairs.add(new BasicNameValuePair(URLEncoder.encode("type", enc), URLEncoder.encode("photo", enc)));
nameValuePairs.add(new BasicNameValuePair(URLEncoder.encode("caption", enc), URLEncoder.encode(text, enc)));
nameValuePairs.add(new BasicNameValuePair("data", Base64.encodeToString(bytes, Base64.URL_SAFE)));
}
catch (UnsupportedEncodingException e2)
{
Log.e(TAG, e2.toString());
e2.printStackTrace();
}
try
{
request.setEntity(new UrlEncodedFormEntity(nameValuePairs));
}
catch (UnsupportedEncodingException e1)
{
Log.e(TAG, e1.toString());
e1.printStackTrace();
}
if (consumer == null)
{
consumer = new CommonsHttpOAuthConsumer(OAuthConstants.TUMBR_CONSUMERKEY, OAuthConstants.TUMBR_SECRETKEY);
}
if (OAUTH_TOKEN == null || OAUTH_SECRET == null)
{
//throw new LoginErrorException(LoginErrorException.NOT_LOGGED_IN);
Log.e(TAG, "Not logged in error");
}
consumer.setTokenWithSecret(OAUTH_TOKEN, OAUTH_SECRET);
try
{
consumer.sign(request);
}
catch (OAuthMessageSignerException e)
{
}
catch (OAuthExpectationFailedException e)
{
}
catch (OAuthCommunicationException e)
{
}
HttpClient client = new DefaultHttpClient();
//finally execute this request
try
{
HttpResponse response = client.execute(request, context);
HttpEntity responseEntity = response.getEntity();
if (responseEntity != null)
{
Log.d(TAG, "responseEntety!=null");
try
{
Log.d(TAG, EntityUtils.toString(responseEntity));
}
catch (ParseException e)
{
e.printStackTrace();
Log.e(TAG, e.toString());
}
catch (IOException e)
{
e.printStackTrace();
Log.e(TAG, e.toString());
}
}
else
{
Log.d(TAG, "responseEntety==null");
}
}
catch (ClientProtocolException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
else
{
Log.d(TAG, "upload imposble... Toklen not set");
}
PostToTumblr.this.finish();
}
Now, while there are several things I am unhappy with (for example that this is done using an activity instead of a service) the big issue here is clearly the problem of uploading images. I am by no means the first to have this problem, so has anyone been able to get this done in java?
Edit 1
Have not made any progress with the problem at hand but created a workaround that might be nice for people who have the same issue. Tumblr offers posting via mail and you can programm android to send emails in the background as shown here. This works very well but you need to ask users to provide their mail account data and the Tumblr-mail Adress to post.
Edit 2
Years have pased and using email is no longer the easy way to do it. With jumblr there is finally a good API for Java that will work on android. OAuth-Authentication is no fun (it never is) but once you get past this, its fantastic.
Now, technically the question of how to do the authentication does not belong here but It's my overly long question, so I'll just paste some code here and if it's not interesting to you just skip it.
This uses a jar called jumblr-0.0.10-jar-with-dependencies.jar
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.util.Log;
import com.tumblr.jumblr.JumblrClient;
import com.tumblr.jumblr.request.RequestBuilder;
import com.tumblr.jumblr.types.Blog;
import com.tumblr.jumblr.types.User;
import org.scribe.builder.ServiceBuilder;
import org.scribe.builder.api.TumblrApi;
import org.scribe.model.Token;
import org.scribe.model.Verifier;
import org.scribe.oauth.OAuthService;
import java.io.File;
public class Tumblr
{
private static final String PROTECTED_RESOURCE_URL = "http://api.tumblr.com/v2/user/info";
static OAuthService service;
static Token requestToken=null;
public static void share(final Activity ctx, File file)
{
Thread tt = new Thread(new Runnable()
{
@Override
public void run()
{
JumblrClient client = new JumblrClient(Tumblr_Constants.CONSUMER_KEY, Tumblr_Constants.CONSUMER_SECRET);
RequestBuilder requestBuilder = client.getRequestBuilder();
requestBuilder.setConsumer(Tumblr_Constants.CONSUMER_KEY, Tumblr_Constants.CONSUMER_SECRET);
SharedPreferences settings = ctx.getSharedPreferences("TumblrData", 0);
String oauthToken=settings.getString("OauthToken", "");
String oauthTokenSecret=settings.getString("OauthSecret", "");
if(oauthToken.equals("") || oauthTokenSecret.equals(""))
{
authenticate(ctx);
while(WebViewFragment.verifier.equals(""))
{
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
String v = WebViewFragment.verifier;
Token accessToken = authenticatefurther(v);
SharedPreferences.Editor edit = settings.edit();
edit.putString("OauthToken", accessToken.getToken());
edit.putString("OauthSecret", accessToken.getSecret());
edit.commit();
oauthToken=settings.getString("OauthToken", "");
oauthTokenSecret=settings.getString("OauthSecret", "");
}
if(!oauthToken.equals("") && !oauthTokenSecret.equals(""))
{
client.setToken(oauthToken, oauthTokenSecret);
User user = client.user();
System.out.println(user.getName());
for (Blog blog : user.getBlogs()) {
Log.d("TUMBLR", blog.getTitle());
}
}
}
});
tt.start();
}
private static void authenticate(Context ctx) {
service = new ServiceBuilder()
.provider( TumblrApi.class )
.apiKey(Tumblr_Constants.CONSUMER_KEY)
.apiSecret(Tumblr_Constants.CONSUMER_SECRET)
.callback("snapnao://snapnao.de/ok") // OOB forbidden. We need an url and the better is on the tumblr website !
.build();
Log.d("TUMBLR", "=== Tumblr's OAuth Workflow ===" );
System.out.println();
// Obtain the Request Token
Log.d("TUMBLR", "Fetching the Request Token...");
requestToken = service.getRequestToken();
Log.d("TUMBLR", "Got the Request Token!");
Log.d("TUMBLR", "");
Log.d("TUMBLR", "Now go and authorize Scribe here:" );
Log.d("TUMBLR", service.getAuthorizationUrl( requestToken ) );
String url = service.getAuthorizationUrl(requestToken);
Intent i = new Intent(ctx, WebViewFragment.class);
i.putExtra("url", url);
ctx.startActivity(i);
}
private static Token authenticatefurther(String v)
{
Token accessToken = null;
Log.d("TUMBLR", "And paste the verifier here");
Log.d("TUMBLR", ">>");
Verifier verifier = new Verifier( v);
Log.d("TUMBLR", "");
// Trade the Request Token and Verfier for the Access Token
Log.d("TUMBLR", "Trading the Request Token for an Access Token...");
accessToken = service.getAccessToken( requestToken ,
verifier );
Log.d("TUMBLR", "Got the Access Token!");
Log.d("TUMBLR", "(if your curious it looks like this: " + accessToken + " )");
Log.d("TUMBLR", "");
return accessToken;
}
}
The WebViewFragement looks like this:
import android.app.Activity;
import android.graphics.Bitmap;
import android.net.http.SslError;
import android.os.Bundle;
import android.util.Log;
import android.webkit.SslErrorHandler;
import android.webkit.WebView;
import android.webkit.WebViewClient;
public class WebViewFragment extends Activity
{
public static String verifier="";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.webviewfragment);
String url = getIntent().getStringExtra("url");
Log.d("TUMBLR", "webview-> "+url);
WebView view = (WebView) findViewById(R.id.webView);
view.setWebViewClient(
new SSLTolerentWebViewClient()
);
view.getSettings().setJavaScriptEnabled(true);
view.loadUrl(url);
}
// SSL Error Tolerant Web View Client
private class SSLTolerentWebViewClient extends WebViewClient {
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
handler.proceed(); // Ignore SSL certificate errors
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
Log.d("TUMBLR", "+++++"+url);
if(url.contains("oauth_verifier="))
{
String[] x = url.split("oauth_verifier=");
verifier=x[1].replace("#_=_", "");
WebViewFragment.this.finish();
}
}
}
}
data
input name in the name-value pairs like you aretype
andcaption
? – Crepusculardata
notsource
but you're sending it a string not an array. Try usingsource
with an encoded string ordata
as an array with one encoded string element if you're just uploading one photo. – Impulse