Charla de JUnit y testing en Android

Aunque finalmente el hangout de la charla no se podrá subir a Youtube, he colgado las diapositivas que usé para la misma. Pueden descargarse desde el siguiente enlace.

 

Para cualquier consulta, puedes contactarme en mi correo personal.

 

 

 

 

How to use the Twitter API to post from Android

Posting from Android into Twitter is one of the earliest stages of an Android developer. To keep full control over the posting process, we will use prefer primarily a pure OAuth post instead of dealing with Intents, so we can keep full control. So as a user, we could just think and conclude: the most typical way to authenticate is to pop up a window where we can identify with our user and password to give the application access to our account (not the full account though, just to post from the application!) and forget about the rest of the process. This might be a bit tricky process for newbies in Android. And of course, Twitter will eventually change their API or registration method, so we will found sometimes that our old implementation is not working anymore

We first need to register a Twitter App. For that purpose, we will visit the developer website of Twitter. After login in, we will add a new application. There are no special settings to be remembered, but the part of the callback URL has changed very oftenly since Twitter released their API. At the moment, if we are developing one application we only need to provide any random URL.

We need three main classes in our application to make it work.

First, TwitterApp. This will be the controller for the API:


import java.net.MalformedURLException;
import java.net.URLDecoder;

import oauth.signpost.OAuth;
import oauth.signpost.OAuthProvider;
import oauth.signpost.basic.DefaultOAuthProvider;
import oauth.signpost.commonshttp.CommonsHttpOAuthConsumer;
import oauth.signpost.commonshttp.CommonsHttpOAuthProvider;

import twitter4j.Twitter;
import twitter4j.TwitterException;
import twitter4j.TwitterFactory;
import twitter4j.User;

import android.os.Handler;
import android.os.Message;

import android.app.ProgressDialog;
import android.content.Context;
import android.util.Log;
import android.view.Window;

import java.net.URL;

public class TwitterApp {
	private Twitter mTwitter;
	private TwitterSession mSession;
	private twitter4j.auth.AccessToken mAccessToken;
	private CommonsHttpOAuthConsumer mHttpOauthConsumer;
	private OAuthProvider mHttpOauthprovider;
	private String mConsumerKey;
	private String mSecretKey;
	private ProgressDialog mProgressDlg;
	private TwDialogListener mListener;
	private Context context;
	
	public static final String CALLBACK_URL = "twitterapp://connect";
	private static final String TAG = "TwitterApp";
	
	public TwitterApp(Context context, String consumerKey, String secretKey) {
		this.context	= context;
		
		mTwitter 		= new TwitterFactory().getInstance();
		mSession		= new TwitterSession(context);
		mProgressDlg	= new ProgressDialog(context);
		
		mProgressDlg.requestWindowFeature(Window.FEATURE_NO_TITLE);
		
		mConsumerKey 	= consumerKey;
		mSecretKey	 	= secretKey;
	
		mHttpOauthConsumer = new CommonsHttpOAuthConsumer(mConsumerKey, mSecretKey);
		mHttpOauthprovider = new CommonsHttpOAuthProvider("https://api.twitter.com/oauth/request_token",
				 					"https://api.twitter.com/oauth/access_token",
				 					"https://api.twitter.com/oauth/authorize");
		mHttpOauthprovider.setOAuth10a(true);
		mAccessToken	= mSession.getAccessToken();
		
		configureToken();
	}
	
	public void setListener(TwDialogListener listener) {
		mListener = listener;
	}
	
	@SuppressWarnings("deprecation")
	private void configureToken() {
		if (mAccessToken != null) {
			mTwitter.setOAuthConsumer(mConsumerKey, mSecretKey);
			
			mTwitter.setOAuthAccessToken(mAccessToken);
		}
	}
	
	public boolean hasAccessToken() {
		
		return (mAccessToken == null) ? false : true;
	}
	
	public void resetAccessToken() {
		if (mAccessToken != null) {
			mSession.resetAccessToken();
		
			mAccessToken = null;
		}
	}
	
	public String getUsername() {
		return mSession.getUsername();
	}
	
	public void updateStatus(String status) throws Exception {
		try {
			mTwitter.updateStatus(status);
		} catch (TwitterException e) {
			throw e;
		}
	}
	
	public void authorize() {
		mProgressDlg.setMessage("Initializing ...");
		mProgressDlg.show();
		
		new Thread() {
			@Override
			public void run() {
				String authUrl = "";
				int what = 1;
				
				try {
					authUrl = mHttpOauthprovider.retrieveRequestToken(mHttpOauthConsumer, CALLBACK_URL);	
					
					what = 0;
					
					Log.d(TAG, "Request token url " + authUrl);
				} catch (Exception e) {
					Log.d(TAG, "Failed to get request token");
					
					e.printStackTrace();
				}
				
				mHandler.sendMessage(mHandler.obtainMessage(what, 1, 0, authUrl));
			}
		}.start();
	}
	
	public void processToken(String callbackUrl)  {
		mProgressDlg.setMessage("Finalizing ...");
		mProgressDlg.show();
		
		final String verifier = getVerifier(callbackUrl);

		new Thread() {
			@Override
			public void run() {
				int what = 1;
				
				try {
					mHttpOauthprovider.retrieveAccessToken(mHttpOauthConsumer, verifier);
		
					mAccessToken = new twitter4j.auth.AccessToken(mHttpOauthConsumer.getToken(), mHttpOauthConsumer.getTokenSecret());
				
					configureToken();
				
					User user = mTwitter.verifyCredentials();
				
			        mSession.storeAccessToken(mAccessToken, user.getName());
			        
			        what = 0;
				} catch (Exception e){
					Log.d(TAG, "Error getting access token");
					
					e.printStackTrace();
				}
				
				mHandler.sendMessage(mHandler.obtainMessage(what, 2, 0));
			}
		}.start();
	}
	
	private String getVerifier(String callbackUrl) {
		String verifier	 = "";
		
		try {
			callbackUrl = callbackUrl.replace("twitterapp", "http");
			
			URL url 		= new URL(callbackUrl);
			String query 	= url.getQuery();
		
			String array[]	= query.split("&");

			for (String parameter : array) {
	             String v[] = parameter.split("=");
	             
	             if (URLDecoder.decode(v[0]).equals(oauth.signpost.OAuth.OAUTH_VERIFIER)) {
	            	 verifier = URLDecoder.decode(v[1]);
	            	 break;
	             }
	        }
		} catch (MalformedURLException e) {
			e.printStackTrace();
		}
		
		return verifier;
	}
	
	private void showLoginDialog(String url) {
		final TwDialogListener listener = new TwDialogListener() {
			@Override
			public void onComplete(String value) {
				processToken(value);
			}
			
			@Override
			public void onError(String value) {
				mListener.onError("Failed opening authorization page");
			}
		};
		
		new TwitterDialog(context, url, listener).show();
	}
	
	private Handler mHandler = new Handler() {
		@Override
		public void handleMessage(Message msg) {
			mProgressDlg.dismiss();
			
			if (msg.what == 1) {
				if (msg.arg1 == 1)
					mListener.onError("Error getting request token");
				else
					mListener.onError("Error getting access token");
			} else {
				if (msg.arg1 == 1)
					showLoginDialog((String) msg.obj);
				else
					mListener.onComplete("");
			}
		}
	};
	
	public interface TwDialogListener {
		public void onComplete(String value);		
		
		public void onError(String value);
	}
}

The TwitterDialog class is composed by a basic WebView which loads the URL with the authentication fields:


import android.app.Dialog;
import android.app.ProgressDialog;

import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;

import android.os.Bundle;
import android.util.Log;
import android.content.Context;

import android.view.Display;
import android.view.ViewGroup;
import android.view.Window;

import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;


public class TwitterDialog extends Dialog {

    static final float[] DIMENSIONS_LANDSCAPE = {460, 260};
    static final float[] DIMENSIONS_PORTRAIT = {280, 420};
    static final FrameLayout.LayoutParams FILL = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT,
                         						ViewGroup.LayoutParams.FILL_PARENT);
    static final int MARGIN = 4;
    static final int PADDING = 2;

    private String mUrl;
    private TwDialogListener mListener;
    private ProgressDialog mSpinner;
    private WebView mWebView;
    private LinearLayout mContent;
    private TextView mTitle;

    private static final String TAG = "Twitter-WebView";
    
    public TwitterDialog(Context context, String url, TwDialogListener listener) {
        super(context);
        
        mUrl 		= url;
        mListener 	= listener;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mSpinner = new ProgressDialog(getContext());
        
        mSpinner.requestWindowFeature(Window.FEATURE_NO_TITLE);
        mSpinner.setMessage("Loading...");

        mContent = new LinearLayout(getContext());
        
        mContent.setOrientation(LinearLayout.VERTICAL);
        
        setUpTitle();
        setUpWebView();
        
        Display display 	= getWindow().getWindowManager().getDefaultDisplay();
        final float scale 	= getContext().getResources().getDisplayMetrics().density;
        float[] dimensions 	= (display.getWidth() < display.getHeight()) ? DIMENSIONS_PORTRAIT : DIMENSIONS_LANDSCAPE;
        
        addContentView(mContent, new FrameLayout.LayoutParams((int) (dimensions[0] * scale + 0.5f),
        							(int) (dimensions[1] * scale + 0.5f)));
    }

    private void setUpTitle() {
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        
        Drawable icon = getContext().getResources().getDrawable(R.drawable.twitter_icon);
        
        mTitle = new TextView(getContext());
        
        mTitle.setText("Twitter");
        mTitle.setTextColor(Color.WHITE);
        mTitle.setTypeface(Typeface.DEFAULT_BOLD);
        mTitle.setBackgroundColor(0xFFbbd7e9);
        mTitle.setPadding(MARGIN + PADDING, MARGIN, MARGIN, MARGIN);
        mTitle.setCompoundDrawablePadding(MARGIN + PADDING);
        mTitle.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null);
        
        mContent.addView(mTitle);
    }

    private void setUpWebView() {
        mWebView = new WebView(getContext());
        
        mWebView.setVerticalScrollBarEnabled(false);
        mWebView.setHorizontalScrollBarEnabled(false);
        mWebView.setWebViewClient(new TwitterWebViewClient());
        mWebView.getSettings().setJavaScriptEnabled(true);
        mWebView.loadUrl(mUrl);
        mWebView.setLayoutParams(FILL);
        
        mContent.addView(mWebView);
    }

    private class TwitterWebViewClient extends WebViewClient {

        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
        	Log.d(TAG, "Redirecting URL " + url);
        	
        	if (url.startsWith(TwitterApp.CALLBACK_URL)) {
        		mListener.onComplete(url);
        		
        		TwitterDialog.this.dismiss();
        		
        		return true;
        	}  else if (url.startsWith("authorize")) {
        		return false;
        	}
        	
            return true;
        }

        @Override
        public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
        	Log.d(TAG, "Page error: " + description);
        	
            super.onReceivedError(view, errorCode, description, failingUrl);
      
            mListener.onError(description);
            
            TwitterDialog.this.dismiss();
        }

        @Override
        public void onPageStarted(WebView view, String url, Bitmap favicon) {
            Log.d(TAG, "Loading URL: " + url);
            super.onPageStarted(view, url, favicon);
            mSpinner.show();
        }

        @Override
        public void onPageFinished(WebView view, String url) {
            super.onPageFinished(view, url);
            String title = mWebView.getTitle();
            if (title != null && title.length() > 0) {
                mTitle.setText(title);
            }
            mSpinner.dismiss();
        }

    }
}

And as we can expect, TwitterSession manages the twitter session:

import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.Context;

public class TwitterSession {
	private SharedPreferences sharedPref;
	private Editor editor;
	
	private static final String TWEET_AUTH_KEY = "auth_key";
	private static final String TWEET_AUTH_SECRET_KEY = "auth_secret_key";
	private static final String TWEET_USER_NAME = "user_name";
	private static final String SHARED = "Twitter_Preferences";
	
	public TwitterSession(Context context) {
		sharedPref 	  = context.getSharedPreferences(SHARED, Context.MODE_PRIVATE);
		
		editor 		  = sharedPref.edit();
	}
	
	public void storeAccessToken(twitter4j.auth.AccessToken accessToken, String username) {
		editor.putString(TWEET_AUTH_KEY, accessToken.getToken());
		editor.putString(TWEET_AUTH_SECRET_KEY, accessToken.getTokenSecret());
		editor.putString(TWEET_USER_NAME, username);
		
		editor.commit();
	}
	
	public void resetAccessToken() {
		editor.putString(TWEET_AUTH_KEY, null);
		editor.putString(TWEET_AUTH_SECRET_KEY, null);
		editor.putString(TWEET_USER_NAME, null);
		
		editor.commit();
	}
	
	public String getUsername() {
		return sharedPref.getString(TWEET_USER_NAME, "");
	}
	
	public twitter4j.auth.AccessToken getAccessToken() {
		String token 		= sharedPref.getString(TWEET_AUTH_KEY, null);
		String tokenSecret 	= sharedPref.getString(TWEET_AUTH_SECRET_KEY, null);
		
		if (token != null && tokenSecret != null) 
			return new twitter4j.auth.AccessToken(token, tokenSecret);
		else
			return null;
	}
}

Now let’s see how this works. In this application we will need to insert our consumer and private keys in the upper variables. The rest of the code is easy to understand: we will initialize the Twitter object, and we will post!


public class ProceedActivity extends Activity {
	
	private TwitterApp mTwitter;

	private static final String twitter_consumer_key = "key";
	private static final String twitter_secret_key = "key";
	private String username = "";
	Dialog dialog ;
	
	 private final TwDialogListener mTwLoginDialogListener = new TwDialogListener() {
	        @Override
	        public void onComplete(String value) {
	            username    = mTwitter.getUsername();
	            username    = (username.equals("")) ? "No Name" : username;
	            Toast.makeText(getActivity(), "Connected to Twitter as " + username, Toast.LENGTH_LONG).show();
	            postToTwitter(String.valueOf("texto"));
	        }
	 
	        @Override
	        public void onError(String value) {
	        	Toast.makeText(getBaseContext(), "Twitter connection failed", Toast.LENGTH_LONG).show();
	        }
	    };
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.proceed);
		
		mTwitter = new TwitterApp(this, twitter_consumer_key,twitter_secret_key);
		   
		mTwitter.setListener(mTwLoginDialogListener);
	 
		dialog = new Dialog(this);
		dialog.setContentView(R.layout.custom_dialog);
		
	      
					ImageView imageTwitter = (ImageView) dialog.findViewById(R.id.imageTwitter);
					
					 imageTwitter.setOnClickListener(new OnClickListener() {

						@Override
						public void onClick(View v) {
							ProgressDialog dialog = ProgressDialog.show(getBaseContext(), "", 
			                        "Loading. Please wait...", true);
							if (mTwitter.hasAccessToken()) {
								postToTwitter(String.valueOf("texto"));
							} else {
								mTwitter.authorize();
							}
							dialog.dismiss();
						}
						 
					 });
					
					
			
	}

	
	
	private void postToTwitter(final String review) {
    	
        new Thread() {
            @Override
            public void run() {
                int what = 0;
 
                try {
                    mTwitter.updateStatus(getResources().getString(R.string.finalTextShare)+" "+review);
                } catch (Exception e) {
                    what = 1;
                }
 
                mHandler.sendMessage(mHandler.obtainMessage(what));
            }
        }.start();
    }
 
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            String text = (msg.what == 0) ? "Posted to Twitter" : "Post to Twitter failed";
            dialog.dismiss();
            Toast.makeText(getBaseContext(), text, Toast.LENGTH_SHORT).show();
        }
    };
    
    
    public Activity getActivity () {
    	return this;
    }
	
	
}

Enrique López-Mañas

Push: Client (Android based) and server

As part of the training and pushing the boundaries in my department, we recently experimented with Push technologies and their application to mobile development. Whereas iPhone seems to support natively push messaging, we soon realize that Android was not perfect in this direction. Surprisingly they haven’t yet considered that a native push support is a technology worth to embedded within their official SDK, and this is a completely setback for developers aiming to develop and create their own ideas.

 

Let’s explain this is in shortly: push is the name given to the fact that messages can be sent to the receiver, instead of being requested from the client. Although the concept might sounds easy to understand, the explanation on why the implementation is hard is a bit more complex: we can summarize it by saying that the protocol running under the modern Internet communications (TCP/IP) was not designed for Push technologies. For more information on this topic, we recommend to read the following article.

 

So there are three general solutions widely accepted to implement our own Push notifications:

  • Google C2DM: recently released by Google,
  • Poll: although it is not a real push request, the effect looks the same: we periodically poll the server looking for new data. The more often we poll the closer we get to the real-time push. But obviously, it will never be on real-time. Plus the battery will die very quickly
  • Persistent TCP/IP connection: the device initiates a long-lived mostly idle TCP/IP connection by playing with the feature of “keep-alive” (sending occasionally messages to the devices). Whenever something new is on the server, the phone initiates a fully TCP connection to acquire the new data. This is the underlying technology for Google C2DM, but you might want to have full control on the entire process until C2DM provides a reliable service.

 

The Google C2DM API is well described on the official service, but we might need to know a bit more about how to set up the server part. In short, we will need three parts: the C2DM server itself, a client implementation of Push (i.e., in your Android device) and a third party application service. This last component might be a bit tricky to understand, but a short summary of the functionalities performed are:

 

  • Is able to communicate with the client.
  • Will fire HTTP requests to the C2DM server.
  • Handle requests algorithm (for instance, we could design it for performing an exponential back off).
  • Storages the authentication tokens. They are need to handle applications with a little bit of complexity

 

As stated before, is easy to set up all the parts, but we might spend more time thinking about the third party server implementation. And that’s the sugar of this article.

 

According to the source site of C2DM, the implementation of the Android is quite trivial. We first need to declare in our Manifest the required permissions for the application, which are com.google.android.c2dm.permission.RECEIVE, com.example.myapp.permission.C2D_MESSAGE and of course android.permission.INTERNET.  We also need to declare our receivers. For further information, check out the official link.
The next step is to register on our application, what can be done with the following code. Typically, we will add it into an onCreate method, or when the application needs to prepare itself for the push.

 

Intent registrationIntent = new Intent("com.google.android.c2dm.intent.REGISTER");
registrationIntent.putExtra("app", PendingIntent.getBroadcast(this, 0, new Intent(), 0)); // boilerplate
registrationIntent.putExtra("sender", emailOfSender);
startService(registrationIntent);

 
Unregistering is also trivial (again, we might want to add this into the onDestroy event):
 

Intent unregIntent = new Intent("com.google.android.c2dm.intent.UNREGISTER");
unregIntent.putExtra("app", PendingIntent.getBroadcast(this, 0, new Intent(), 0));
startService(unregIntent);

 
The basic part for handling the message needs a bit more of explanation. When the method onReceive of our BroadcastReceiver is triggered, we might need to check if we are dealing with the registration or if we are just receiving a push notification. We provide the code for the first case. For the second one, we might need to create our own method based on our design. We will be able to receive a message through the Intent. And that’s the part we need to handle on the server side.
 

public void onReceive(Context context, Intent intent) {
   if (intent.getAction().equals("com.google.android.c2dm.intent.REGISTRATION")) {
      handleRegistration(context, intent);
   } else if (intent.getAction().equals("com.google.android.c2dm.intent.RECEIVE")) {
      handleMessage(context, intent);
   }
}

private void handleRegistration(Context context, Intent intent) {
   String registration = intent.getStringExtra("registration_id");
   if (intent.getStringExtra("error") != null) {
      // Registration failed, should try again later.
   } else if (intent.getStringExtra("unregistered") != null) {
      // unregistration done, new messages from the authorized sender will be rejected
   } else if (registration != null) {
      // Send the registration ID to the 3rd party site that is sending the messages.
      // This should be done in a separate thread.
      // When done, remember that all registration is done.
   }
}

 

So this is the core of the server application. This method needs to receive an auth code, and the device registration ID. The device registration ID will be provided when our phone is registered in the previous step. The authcode is the authentication through a Google account. For instance, we could create a dummy account, and send it along with the push notification:

 

googleAuthenticate("mydummyaccount@gmail.com","mydummypassword")

 

Afterwards, we can send a message type and the content itself. This allows us to have more flexibility in our notifications. For instance, we might provide a message type which corresponds with an error, and on the content we can send the complete error text.
 

function sendMessageToPhone($authCode, $deviceRegistrationId, $msgType, $messageText) {
    $headers = array('Authorization: GoogleLogin auth=' . $authCode);
    $data = array(
      'registration_id' => $deviceRegistrationId,
      'collapse_key' => $msgType,
      'data.message' => $messageText //TODO Add more params with just simple data instead
   );

   $ch = curl_init();

   curl_setopt($ch, CURLOPT_URL, "https://android.apis.google.com/c2dm/send");
   if ($headers)
      curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
      curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
      curl_setopt($ch, CURLOPT_POST, true);
      curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
      curl_setopt($ch, CURLOPT_POSTFIELDS, $data);

      $response = curl_exec($ch);

      curl_close($ch);

      return $response;
   }

function googleAuthenticate($username, $password, $source="Company-AppName-Version", $service="ac2dm") {

   if( isset($_SESSION['google_auth_id']) && $_SESSION['google_auth_id'] != null) {
   return $_SESSION['google_auth_id'];
}

// get an authorization token
$ch = curl_init();
if(!ch){
   return false;
}

curl_setopt($ch, CURLOPT_URL, "https://www.google.com/accounts/ClientLogin");
$post_fields = "accountType=" . urlencode('HOSTED_OR_GOOGLE')

. "&Email=" . urlencode($username)
. "&Passwd=" . urlencode($password)
. "&source=" . urlencode($source)
. "&service=" . urlencode($service);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_fields);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FRESH_CONNECT, true);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);

// for debugging the request
//curl_setopt($ch, CURLINFO_HEADER_OUT, true); // for debugging the request

$response = curl_exec($ch);

//var_dump(curl_getinfo($ch)); //for debugging the request
//var_dump($response);

//var_dump(curl_getinfo($ch)); //for debugging the request
//var_dump($response);

curl_close($ch);
if (strpos($response, '200 OK') === false) {
   return false;
}

// find the auth code
preg_match("/(Auth=)([\w|-]+)/", $response, $matches);

if (!$matches[2]) {
   return false;
}
$_SESSION['google_auth_id'] = $matches[2];

return $matches[2];
}

 

I hope you enjoyed the tutorial. For any further questions or inquiries, you can drop me a line into my personal email.

 

Enrique López-Mañas

New automatic language translation tool for Android

Since I have been working in Barcelona, I got so much in touch with Android and mobile developers than in Germany, since my work there was a little bit more theoretical rather than applied.

Although I don’t use on a regular basis Google Translate or any engine to translate my applications (this is a problem of quality vs. quantity, where I bet quality should prevail), I realized that many developers of successful applications are using Google Translate to generate new string files of their projects. And it is, in fact, much more widespread of what I thought. When I have been talking with those developers, they confess after one or two beers that they use web powered engines (i.e., Google Translate) and they usually don’t perform a pair check or a professional review of their generated tokens. Without judging this behavior, I was interested in the procedure, and the first question to arise was: have you automatized the process? The most recurrent answer was a “No”, while they began to stare at the floor.

I thought it could be interesting for some projects, so I thought I could expand the translation tool I published a few months ago. Why couldn’t we just perform the translation systematically? And that’s why I’m presenting the second version of the tool.

The Google Translate API is not free, and since I don’t want to offer my API key and see a huge bill at the end of the month in my bank account, I’m offering two different versions of the tool: the first version allows you to insert your own key, and perform the translation. And if you don’t want to deal with the registration and Google Checkout, you can get a paid version which uses my own API key for only 5$. So if you think this tool can be helpful for you, those 5$ will make you save a lot of time and I will get a few extra beers and some support to develop projects on my free time. As shown on the following screen, you just need to type your API key on the following edit text.

There are still some upcoming tasks to be performed, but this can be considered as a working alpha version.

This tool will not be released as GPL software as the previous one. However, if you need the source code for any purpose you always can send me an email in order to convince me.

The free version can be download from here (version 2.0). If you want the paid version, feel free to drop me a line or contact me via Twitter or Facebook

cvBlob library for Android

I recently moved to Barcelona to start working in here. Although I’m not working in any computer-vision based project, I still keep a high interest in this field, trying to conduct as many personal projects as possible. My work team is highly motivated and full of professionals, and we all keep our personal projects besides our work.

Recently I met Fegabe in Barcelona, who’s a member of the GTUG Barcelona core team and an Android Developer. We decided to start together a Sudoku Solver for Android. Although there are already many of them published in the Market, we just wanted to do it for fun and to get a bit deeper into computer vision and pattern recognition. There is an OpenCV port for Android, but we decided to keep our implementation pure Java.

The following steps are applied in order to capture, detect and solve the Sudoku:

-The user must take a picture of the Sudoku using the camera of the device.

-After the picture is taken, a Threshold is applied to the image, so we can get a black and white version. Of course, there is some preprocessing involved. Things like smoothing out noise, some morphological operations, etc.

-When the picture has been taken, we apply a blob detection in order to detect the biggest segment of the image. We work under the assumption that this segment will be the outer line of the Sudoku table.

-Afterwards, we can use the Hough transform to get lines in the image. It returns lines in mathematical terms. So after this step, we’ll know exactly where a lines lies… and not just the pixels where it lies.

-When the lines has been detected, it will be easier to locate each individual cell. The number inside will be recognized using a neural network.

-The last step is the easiest: we just solve the sudoku by using any of all the known algorithms

The Sudoku Solver is still to be finished, but the cvBlob library is working fine, so I decided to publish it. The implementation is based on Dr. Andrew Greensted suggestion for Blob Detection.

I shared the project with a GPL license in Google Code. All the technical information can be found there, besides the source code and a binary file for Android. If you wanna try out, I would highly appreciate any feedback

 

blob detection