Эх сурвалжийг харах

Login page updated to get OAuth2 access token

David A. Velasco 12 жил өмнө
parent
commit
cab55a8edd

+ 5 - 1
AndroidManifest.xml

@@ -150,7 +150,11 @@
                 <action android:name="android.intent.action.BOOT_COMPLETED"/>
             </intent-filter>
         </receiver>
-        <service android:name=".files.services.FileObserverService"/>
+        <service android:name=".files.services.FileObserverService"/>
+        
+        <service android:name=".authenticator.oauth2.services.OAuth2GetTokenService" >
+        </service>
+        
     </application>
 
 </manifest>

+ 44 - 0
res/layout-land/account_setup.xml

@@ -100,6 +100,15 @@
                         android:visibility="invisible" />
                 </LinearLayout>
 
+                <CheckBox
+                    android:id="@+id/oauth_onOff_check"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:checked="false"
+                    android:onClick="onOff_check_Click"
+                    android:text="@string/oauth_check_onoff"
+                    android:textAppearance="?android:attr/textAppearanceSmall" />
+
                 <TextView
                     android:id="@+id/textView2"
                     android:layout_width="wrap_content"
@@ -108,6 +117,41 @@
                     android:text="@string/auth_login_details"
                     android:textAppearance="?android:attr/textAppearanceSmall" />
 
+                <EditText
+                    android:id="@+id/oAuth_URL"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1"
+                    android:ems="10"
+                    android:hint="@string/oauth_host_url"
+                    android:singleLine="true"
+                    android:visibility="gone" >
+
+                    <requestFocus />
+                </EditText>
+                
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1" >
+
+                    <ImageView
+                        android:id="@+id/auth2_action_indicator"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_marginLeft="5dp"
+                        android:layout_marginRight="5dp"
+                        android:src="@android:drawable/stat_notify_sync"
+                        android:visibility="gone" />
+
+                    <TextView
+                        android:id="@+id/oauth2_status_text"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="TextView"
+                        android:visibility="gone" />
+                </LinearLayout>
+                
                 <EditText
                     android:id="@+id/account_username"
                     android:layout_width="match_parent"

+ 43 - 1
res/layout/account_setup.xml

@@ -94,9 +94,17 @@
                     android:layout_height="wrap_content"
                     android:text="TextView"
                     android:visibility="invisible" />
-
             </LinearLayout>
 
+            <CheckBox
+                android:id="@+id/oauth_onOff_check"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:checked="false"
+                android:onClick="onOff_check_Click"
+                android:text="@string/oauth_check_onoff"
+                android:textAppearance="?android:attr/textAppearanceSmall" />
+
             <TextView
                 android:id="@+id/textView2"
                 android:layout_width="wrap_content"
@@ -104,7 +112,41 @@
                 android:layout_weight="1"
                 android:text="@string/auth_login_details"
                 android:textAppearance="?android:attr/textAppearanceSmall" />
+            
+            <EditText
+                android:id="@+id/oAuth_URL"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:ems="10"
+                android:hint="@string/oauth_host_url"
+                android:singleLine="true"
+                android:visibility="gone" >
 
+                <requestFocus />
+            </EditText>            
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_weight="1" >
+
+                <ImageView
+                    android:id="@+id/auth2_action_indicator"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginLeft="5dp"
+                    android:layout_marginRight="5dp"
+                    android:src="@android:drawable/stat_notify_sync"
+                    android:visibility="gone" />
+
+                <TextView
+                    android:id="@+id/oauth2_status_text"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="TextView"
+                    android:visibility="gone" />
+            </LinearLayout>            
             <EditText
                 android:id="@+id/account_username"
                 android:layout_width="match_parent"

+ 6 - 0
res/values/strings.xml

@@ -187,6 +187,12 @@
     <string name="filedisplay_unexpected_bad_get_content">"Unexpected problem ; please, try other app to select the file"</string>
     <string name="filedisplay_no_file_selected">No file was selected</string>
     
+    <string name="oauth_host_url">oAuth2 URL</string> 
+    <string name="oauth_check_onoff">Login with oAuth2.</string> 
+    <string name="oauth_login_connection">Connecting to oAuth2 server…</string>    
+    <string name="oauth_code_validation_message">Please, open a web browser and go to:\n%1$s.\nValidate this code there:\n%2$s</string>
+    <string name="oauth_connection_url_unavailable">Connection to this URL not available.</string> 
+        
     <string name="ssl_validator_title">Warning</string>
     <string name="ssl_validator_header">The identity of the site could not be verified</string>
     <string name="ssl_validator_reason_cert_not_trusted">- The server certificate is not trusted</string>

+ 4 - 0
res/values/styles.xml

@@ -83,6 +83,10 @@
 		<item name="android:singleLine">true</item>
   		    
     </style>
+	
+	<style name="OAuthDialog" parent="@android:style/Theme.Dialog">
+		<item name="android:windowNoTitle">false</item>	
+	</style>    
 		
 	<color name="setup_text_hint">#777777</color>
 	<color name="setup_text_typed">#000000</color>

+ 20 - 0
src/com/owncloud/android/authenticator/oauth2/OAuth2Context.java

@@ -0,0 +1,20 @@
+package com.owncloud.android.authenticator.oauth2;
+
+/** 
+ *  Class used to store data from the app registration in oAuth2 server.
+ *  THIS VALUES ARE ORIENTATIVE.
+ *  MUST BE CHANGED WITH THE CORRECT ONES.
+ *  
+ * @author SolidGear S.L.
+ *
+ */
+
+public class OAuth2Context {
+    
+    public static final String OAUTH2_DEVICE_CLIENT_ID = "0000000000000.apps.googleusercontent.com";  
+    public static final String OAUTH2_DEVICE_CLIENT_SECRET = "XXXXXXXXXXXXXXXXXXXXXXXXX";
+    public static final String OAUTH_DEVICE_GETTOKEN_GRANT_TYPE = "http://oauth.net/grant_type/device/1.0";
+    public static final String OAUTH2_DEVICE_GETCODE_URL = "/o/oauth2/device/code";  
+    public static final String OAUTH2_DEVICE_GETTOKEN_URL = "/o/oauth2/token";
+    public static final String OAUTH2_DEVICE_GETCODE_SCOPES = "https://www.googleapis.com/auth/userinfo.email";
+}

+ 113 - 0
src/com/owncloud/android/authenticator/oauth2/OAuth2GetCodeRunnable.java

@@ -0,0 +1,113 @@
+package com.owncloud.android.authenticator.oauth2;
+
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.http.NameValuePair;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.message.BasicNameValuePair;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.os.Handler;
+import android.util.Log;
+
+import com.owncloud.android.authenticator.oauth2.OnOAuth2GetCodeResultListener.ResultOAuthType;
+import com.owncloud.android.authenticator.oauth2.connection.ConnectorOAuth2;
+
+/**
+ * Implements the communication with oAuth2 server to get User Code and other useful values.
+ * 
+ * @author SolidGear S.L.
+ *
+ */
+public class OAuth2GetCodeRunnable implements Runnable {
+
+    public static final String CODE_USER_CODE  =  "user_code";
+    public static final String CODE_CLIENT_ID  =  "client_id";
+    public static final String CODE_CLIENT_SCOPE  =  "scope";    
+    public static final String CODE_VERIFICATION_URL  =  "verification_url";
+    public static final String CODE_EXPIRES_IN  =  "expires_in";
+    public static final String CODE_DEVICE_CODE = "device_code";
+    public static final String CODE_INTERVAL = "interval";
+
+    private static final String TAG = "OAuth2GetCodeRunnable";
+    private OnOAuth2GetCodeResultListener mListener;
+    private String mUrl;
+    private Handler mHandler;
+    private Context mContext;
+
+    public void setListener(OnOAuth2GetCodeResultListener listener, Handler handler) {
+        mListener = listener;
+        mHandler = handler;
+    }
+
+    public OAuth2GetCodeRunnable(String url, Context context) {
+        mListener = null;
+        mHandler = null;
+        mUrl = url;
+        mContext = context;
+    }
+
+    @Override
+    public void run() {
+        ResultOAuthType mLatestResult;
+        String targetURI = null;
+        JSONObject codeResponseJson = null;
+
+        if (!isOnline()) {
+            postResult(ResultOAuthType.NO_NETWORK_CONNECTION,null);
+            return;
+        }
+
+        if (mUrl.startsWith("http://") || mUrl.startsWith("https://")) {        
+            mLatestResult = (mUrl.startsWith("https://"))? ResultOAuthType.OK_SSL : ResultOAuthType.OK_NO_SSL;            
+        } else {
+            mUrl = "https://" + mUrl;
+            mLatestResult = ResultOAuthType.OK_SSL;
+        }
+        targetURI = mUrl + OAuth2Context.OAUTH2_DEVICE_GETCODE_URL;
+
+        ConnectorOAuth2 connectorOAuth2 = new ConnectorOAuth2(targetURI);
+
+        try {
+            List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(2);
+            nameValuePairs.add(new BasicNameValuePair(CODE_CLIENT_ID, OAuth2Context.OAUTH2_DEVICE_CLIENT_ID));
+            nameValuePairs.add(new BasicNameValuePair(CODE_CLIENT_SCOPE,OAuth2Context.OAUTH2_DEVICE_GETCODE_SCOPES));
+            UrlEncodedFormEntity params = new UrlEncodedFormEntity(nameValuePairs);        
+            codeResponseJson = new JSONObject(connectorOAuth2.connPost(params));         
+        } catch (JSONException e) {
+            Log.e(TAG, "JSONException converting to Json: " + e.toString());
+        } catch (UnsupportedEncodingException e) {
+            Log.e(TAG, "UnsupportedEncodingException encoding URL values: " + e.toString());
+        } catch (Exception e) {
+            Log.e(TAG, "Exception : " + e.toString());
+        }
+
+        if (codeResponseJson == null) {            
+            mLatestResult = ResultOAuthType.HOST_NOT_AVAILABLE;
+        }
+
+        postResult(mLatestResult, codeResponseJson);
+    }
+
+    private boolean isOnline() {
+        ConnectivityManager cm = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+        return cm != null && cm.getActiveNetworkInfo() != null && cm.getActiveNetworkInfo().isConnectedOrConnecting();
+    }
+
+    private void postResult(final ResultOAuthType result,final JSONObject codeResponseJson) {
+        if (mHandler != null && mListener != null) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    mListener.onOAuth2GetCodeResult(result, codeResponseJson);
+                }
+            });
+        }
+    }
+
+}

+ 19 - 0
src/com/owncloud/android/authenticator/oauth2/OnOAuth2GetCodeResultListener.java

@@ -0,0 +1,19 @@
+package com.owncloud.android.authenticator.oauth2;
+
+import org.json.JSONObject;
+
+/** 
+ * Listener that expects results from OAuth2GetCodeRunnable class.
+ * 
+ * @author SolidGear S.L.
+ *
+ */
+public interface OnOAuth2GetCodeResultListener {
+    
+    enum ResultOAuthType {
+        OK_SSL, OK_NO_SSL, SSL_INIT_ERROR, HOST_NOT_AVAILABLE, TIMEOUT, NO_NETWORK_CONNECTION, INCORRECT_ADDRESS, INSTANCE_NOT_CONFIGURED, FILE_NOT_FOUND, UNKNOWN_ERROR, WRONG_CONNECTION,  SSL_UNVERIFIED_SERVER, BAD_OC_VERSION
+    }    
+    
+    public void onOAuth2GetCodeResult(ResultOAuthType type, JSONObject code);
+
+}

+ 113 - 0
src/com/owncloud/android/authenticator/oauth2/connection/ConnectorOAuth2.java

@@ -0,0 +1,113 @@
+package com.owncloud.android.authenticator.oauth2.connection;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.params.ClientPNames;
+import org.apache.http.client.params.CookiePolicy;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.params.BasicHttpParams;
+import org.apache.http.params.HttpConnectionParams;
+import org.apache.http.params.HttpParams;
+import org.apache.http.protocol.BasicHttpContext;
+import org.apache.http.protocol.HttpContext;
+import org.apache.http.util.EntityUtils;
+
+import android.util.Log;
+
+/**
+ * Implements HTTP POST communications with an oAuth2 server.
+ * 
+ * @author SolidGear S.L.
+ *
+ */
+public class ConnectorOAuth2 {
+
+    private static final String TAG = "ConnectorOAuth2";
+    /** Maximum time to wait for a response from the server when the connection is being tested, in MILLISECONDs.  */
+    private static final int TRY_CONNECTION_TIMEOUT = 5000;    
+
+    private DefaultHttpClient httpClient;
+    private HttpContext localContext;
+    private String ConnectorOAuth2Url;    
+
+    public ConnectorOAuth2 (String destUrl) {
+        prepareConn();
+        setConnectorOAuth2Url(destUrl);        
+    }
+
+    public ConnectorOAuth2 () {
+        prepareConn();    
+    }      
+
+    public String getConnectorOAuth2Url() {
+        return ConnectorOAuth2Url;
+    }
+
+    public void setConnectorOAuth2Url(String connectorOAuth2Url) {
+        ConnectorOAuth2Url = connectorOAuth2Url;
+    }    
+    
+    /**
+     * Starts the communication with the server.
+     * 
+     * @param UrlEncodedFormEntity : parameters included in the POST call.
+     * @return String : data returned from the server in String format.
+     */
+
+    public String connPost(UrlEncodedFormEntity  data) {
+        String dataOut = null;
+        HttpPost httpPost = null;
+        int responseCode = -1;
+        HttpResponse response = null;
+
+        httpClient.getParams().setParameter(ClientPNames.COOKIE_POLICY, CookiePolicy.RFC_2109);
+
+        if (ConnectorOAuth2Url == null) {
+            Log.e(TAG, "connPost error: destination URI could not be null");
+            return null;
+        }
+
+        if (data == null){
+            Log.e(TAG, "connPost error: data to send to URI " + ConnectorOAuth2Url + "could not be null");            
+            return null; 
+        }
+
+        httpPost = new HttpPost(ConnectorOAuth2Url);       
+
+        httpPost.setHeader("Accept","text/html,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5");
+        httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded");
+        httpPost.setEntity((UrlEncodedFormEntity) data);
+
+        try {
+            response = httpClient.execute(httpPost,localContext);
+
+            if (response == null) {
+                Log.e(TAG, "connPost error: response from uri " + ConnectorOAuth2Url + " is null");
+                return null;
+            } 
+
+            responseCode = response.getStatusLine().getStatusCode();
+
+            if ((responseCode != 200)) {
+                Log.e(TAG, "connPost error: response from uri "+ ConnectorOAuth2Url + " returns status " + responseCode);
+                return null;
+            }
+
+            dataOut = EntityUtils.toString(response.getEntity());
+
+        } catch (Exception e) {
+            Log.e(TAG, "connPost Exception: " + e);
+        }
+
+        return dataOut;
+    }
+
+    private void prepareConn () {
+        HttpParams localParams = new BasicHttpParams();
+        HttpConnectionParams.setConnectionTimeout(localParams, TRY_CONNECTION_TIMEOUT);
+        HttpConnectionParams.setSoTimeout(localParams, TRY_CONNECTION_TIMEOUT);
+        httpClient = new DefaultHttpClient(localParams);
+        localContext = new BasicHttpContext();    
+    }
+}

+ 201 - 0
src/com/owncloud/android/authenticator/oauth2/services/OAuth2GetTokenService.java

@@ -0,0 +1,201 @@
+package com.owncloud.android.authenticator.oauth2.services;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import org.apache.http.NameValuePair;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.message.BasicNameValuePair;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.util.Log;
+
+import com.owncloud.android.authenticator.oauth2.OAuth2Context;
+import com.owncloud.android.authenticator.oauth2.connection.ConnectorOAuth2;
+
+/**
+ * Service class that implements the second communication with the oAuth2 server:
+ * pooling for the token in an interval. It send a broadcast with the results when are positive;
+ * otherwise, it continues asking to the server.
+ * 
+ * @author Solid Gear S.L.
+ *
+ */
+public class OAuth2GetTokenService extends Service {
+
+    public static final String TOKEN_RECEIVED_MESSAGE = "TOKEN_RECEIVED";
+    public static final String TOKEN_RECEIVED_DATA = "TOKEN_DATA";
+    public static final String TOKEN_BASE_URI = "baseURI";
+    public static final String TOKEN_DEVICE_CODE = "device_code";
+    public static final String TOKEN_INTERVAL = "interval";
+    public static final String TOKEN_RECEIVED_ERROR = "error";
+    public static final String TOKEN_RECEIVED_ERROR_AUTH_TOKEN = "authorization_pending";
+    public static final String TOKEN_RECEIVED_ERROR_SLOW_DOWN = "slow_down";
+    public static final String TOKEN_ACCESS_TOKEN = "access_token";
+    public static final String TOKEN_TOKEN_TYPE = "token_type";
+    public static final String TOKEN_EXPIRES_IN = "expires_in";
+    public static final String TOKEN_REFRESH_TOKEN = "refresh_token";   
+    
+    private String requestDeviceCode;
+    private int requestInterval = -1;
+    private String requestBaseURI;
+    private ConnectorOAuth2 connectorOAuth2;
+    private static final String TAG = "OAuth2GetTokenService";
+    private Timer timer = new Timer();
+
+    @Override
+    public IBinder onBind(Intent arg0) {
+        return null;
+    }
+    
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        Bundle param = intent.getExtras();
+
+        if (param != null) {
+            String mUrl = param.getString(TOKEN_BASE_URI);     
+            if (!mUrl.startsWith("http://") || !mUrl.startsWith("https://")) {        
+                requestBaseURI = "https://" + mUrl;            
+            }     
+            requestDeviceCode = param.getString(TOKEN_DEVICE_CODE);
+            requestInterval = param.getInt(TOKEN_INTERVAL);
+            
+            Log.d(TAG, "onBind -> baseURI=" + requestBaseURI);
+            Log.d(TAG, "onBind -> requestDeviceCode=" + requestDeviceCode);
+            Log.d(TAG, "onBind -> requestInterval=" + requestInterval);                  
+        } else  {
+            Log.e(TAG, "onBind -> params could not be null");
+        }
+        startService();
+        return Service.START_NOT_STICKY;
+    }
+
+    @Override
+    public void onCreate() {
+        super.onCreate();       
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        shutdownService();
+    }
+
+    private void startService() {
+        final UrlEncodedFormEntity params = prepareComm();
+        timer.scheduleAtFixedRate(
+                new TimerTask() {
+                    public void run() {
+                        requestToken(params);
+                    }
+                },  0, requestInterval * 1000);
+        Log.d(TAG, "startService -> Timer started");
+    }    
+
+    private void shutdownService() {
+        if (timer != null) timer.cancel();
+        Log.d(TAG, "shutdownService -> Timer stopped");
+    }
+
+
+    private UrlEncodedFormEntity prepareComm() {
+
+        UrlEncodedFormEntity params = null;
+        connectorOAuth2 = new ConnectorOAuth2();
+
+        if (requestBaseURI == null || requestBaseURI.trim().equals("")) {
+            Log.e(TAG, "run -> request URI could not be null");
+            postResult(null);
+        }
+
+        if (requestInterval == -1) {
+            Log.e(TAG, "run -> request Interval must have valid positive value");
+            postResult(null);
+        }
+
+        if (requestDeviceCode == null || requestDeviceCode.trim().equals("")) {
+            Log.e(TAG, "run -> request DeviceCode could not be null");
+            postResult(null);
+        }        
+
+        try{            
+            connectorOAuth2.setConnectorOAuth2Url(requestBaseURI + OAuth2Context.OAUTH2_DEVICE_GETTOKEN_URL);
+
+            List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(2);
+            nameValuePairs.add(new BasicNameValuePair("client_id", OAuth2Context.OAUTH2_DEVICE_CLIENT_ID));
+            nameValuePairs.add(new BasicNameValuePair("client_secret", OAuth2Context.OAUTH2_DEVICE_CLIENT_SECRET));
+            nameValuePairs.add(new BasicNameValuePair("code",requestDeviceCode));            
+            nameValuePairs.add(new BasicNameValuePair("grant_type",OAuth2Context.OAUTH_DEVICE_GETTOKEN_GRANT_TYPE));  
+
+            params = new UrlEncodedFormEntity(nameValuePairs);
+        }
+        catch (Exception ex1){
+            Log.w(TAG, ex1.toString());
+            postResult(null);
+        }
+
+        return params;
+    }
+
+    protected void requestToken(UrlEncodedFormEntity params){
+        JSONObject tokenJson = null;
+        String error = null;
+        HashMap<String, String> resultTokenMap;
+
+        String tokenResponse = connectorOAuth2.connPost(params);
+
+        try {
+            tokenJson = new JSONObject(tokenResponse);
+        } catch (JSONException e) {
+            Log.e(TAG, "Exception converting to Json " + e.toString());
+        }        
+
+        try {
+            // We try to get error string.
+            if (tokenJson.has(TOKEN_RECEIVED_ERROR)) {
+                error = tokenJson.getString(TOKEN_RECEIVED_ERROR);
+                Log.d(TAG, "requestToken -> Obtained error "+ error);
+            } else {
+                //We have got the token. Parse the answer.
+                resultTokenMap = parseResult(tokenJson);
+                postResult(resultTokenMap);                
+            }
+        } catch (JSONException e) {
+            Log.e(TAG, "Exception converting to Json " + e.toString());
+        }
+    }
+    
+    private HashMap<String, String> parseResult (JSONObject tokenJson) {
+        HashMap<String, String> resultTokenMap=new HashMap<String, String>();
+        
+        try {
+            resultTokenMap.put(TOKEN_ACCESS_TOKEN, tokenJson.getString(TOKEN_ACCESS_TOKEN)); 
+            resultTokenMap.put(TOKEN_TOKEN_TYPE, tokenJson.getString(TOKEN_TOKEN_TYPE)); 
+            resultTokenMap.put(TOKEN_EXPIRES_IN, tokenJson.getString(TOKEN_EXPIRES_IN)); 
+            resultTokenMap.put(TOKEN_REFRESH_TOKEN, tokenJson.getString(TOKEN_REFRESH_TOKEN));             
+        } catch (JSONException e) {
+            Log.e(TAG, "parseResult: Exception converting to Json " + e.toString());
+        }    
+        return resultTokenMap;      
+    }
+
+    /**
+     * Returns obtained values with a broadcast.
+     * 
+     * @param tokenResponse : obtained values.
+     */
+    private void postResult(HashMap<String, String> tokenResponse) {
+        Intent intent = new Intent(TOKEN_RECEIVED_MESSAGE);       
+        intent.putExtra(TOKEN_RECEIVED_DATA,tokenResponse);
+        sendBroadcast(intent);
+        shutdownService();
+    }
+}

+ 311 - 3
src/com/owncloud/android/ui/activity/AuthenticatorActivity.java

@@ -20,12 +20,20 @@ package com.owncloud.android.ui.activity;
 
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.util.HashMap;
+
+import org.json.JSONException;
+import org.json.JSONObject;
 
 import com.owncloud.android.AccountUtils;
 import com.owncloud.android.authenticator.AccountAuthenticator;
 import com.owncloud.android.authenticator.AuthenticationRunnable;
 import com.owncloud.android.authenticator.OnAuthenticationResultListener;
 import com.owncloud.android.authenticator.OnConnectCheckListener;
+import com.owncloud.android.authenticator.oauth2.OAuth2GetCodeRunnable;
+import com.owncloud.android.authenticator.oauth2.OnOAuth2GetCodeResultListener;
+import com.owncloud.android.authenticator.oauth2.connection.ConnectorOAuth2;
+import com.owncloud.android.authenticator.oauth2.services.OAuth2GetTokenService;
 import com.owncloud.android.ui.dialog.SslValidatorDialog;
 import com.owncloud.android.ui.dialog.SslValidatorDialog.OnSslValidatorListener;
 import com.owncloud.android.network.OwnCloudClientUtils;
@@ -40,9 +48,12 @@ import android.accounts.AccountManager;
 import android.app.AlertDialog;
 import android.app.Dialog;
 import android.app.ProgressDialog;
+import android.content.BroadcastReceiver;
 import android.content.ContentResolver;
+import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.SharedPreferences;
 import android.net.Uri;
 import android.os.Bundle;
@@ -54,6 +65,8 @@ import android.view.View;
 import android.view.View.OnClickListener;
 import android.view.View.OnFocusChangeListener;
 import android.view.Window;
+import android.widget.CheckBox;
+import android.widget.EditText;
 import android.widget.ImageView;
 import android.widget.TextView;
 import com.owncloud.android.R;
@@ -68,7 +81,7 @@ import eu.alefzero.webdav.WebdavClient;
  */
 public class AuthenticatorActivity extends AccountAuthenticatorActivity
         implements OnAuthenticationResultListener, OnConnectCheckListener, OnRemoteOperationListener, OnSslValidatorListener, 
-        OnFocusChangeListener, OnClickListener {
+        OnFocusChangeListener, OnClickListener, OnOAuth2GetCodeResultListener {
 
     private static final int DIALOG_LOGIN_PROGRESS = 0;
     private static final int DIALOG_SSL_VALIDATOR = 1;
@@ -94,6 +107,30 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
     public static final String PARAM_USERNAME = "param_Username";
     public static final String PARAM_HOSTNAME = "param_Hostname";
 
+    // oAuth2 variables.
+    private static final int OAUTH2_LOGIN_PROGRESS = 3;
+    private static final String OAUTH2_STATUS_TEXT = "OAUTH2_STATUS_TEXT";
+    private static final String OAUTH2_STATUS_ICON = "OAUTH2_STATUS_ICON";
+    private static final String OAUTH2_CODE_RESULT = "CODE_RESULT";
+    private static final String OAUTH2_BASE_URL = "BASE_URL"; 
+    private static final String OAUTH2_IS_CHECKED = "OAUTH2_IS_CHECKED";    
+    private Thread mOAuth2GetCodeThread;
+    private OAuth2GetCodeRunnable mOAuth2GetCodeRunnable;     
+    private String oAuth2BaseUrl;
+    private TokenReceiver tokenReceiver;
+    private JSONObject codeResponseJson; 
+    private int mOAuth2StatusText, mOAuth2StatusIcon;    
+    
+    public ConnectorOAuth2 connectorOAuth2;
+    
+    // Variables used to save the on the state the contents of all fields.
+    private static final String HOST_URL_TEXT = "HOST_URL_TEXT";
+    private static final String OAUTH2_URL_TEXT = "OAUTH2_URL_TEXT";
+    private static final String ACCOUNT_USERNAME = "ACCOUNT_USERNAME";
+    private static final String ACCOUNT_PASSWORD = "ACCOUNT_PASSWORD";
+
+    // END of oAuth2 variables.
+    
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -103,6 +140,8 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
         ImageView iv2 = (ImageView) findViewById(R.id.viewPassword);
         TextView tv = (TextView) findViewById(R.id.host_URL);
         TextView tv2 = (TextView) findViewById(R.id.account_password);
+        // New textview to oAuth2 URL.
+        TextView tv3 = (TextView) findViewById(R.id.oAuth_URL);
 
         if (savedInstanceState != null) {
             mStatusIcon = savedInstanceState.getInt(STATUS_ICON);
@@ -114,7 +153,35 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
             if (!mStatusCorrect)
                 iv.setVisibility(View.VISIBLE);
             else
-                iv.setVisibility(View.INVISIBLE);
+                iv.setVisibility(View.INVISIBLE);            
+            
+            // Getting the state of oAuth2 components.
+            mOAuth2StatusIcon = savedInstanceState.getInt(OAUTH2_STATUS_ICON);
+            mOAuth2StatusText = savedInstanceState.getInt(OAUTH2_STATUS_TEXT);
+                // We set this to true if the rotation happens when the user is validating oAuth2 user_code.
+            changeViewByOAuth2Check(savedInstanceState.getBoolean(OAUTH2_IS_CHECKED));
+            oAuth2BaseUrl = savedInstanceState.getString(OAUTH2_BASE_URL);
+                // We store a JSon object with all the data returned from oAuth2 server when we get user_code.
+                // Is better than store variable by variable. We use String object to serialize from/to it.
+            try {
+                if (savedInstanceState.containsKey(OAUTH2_CODE_RESULT)) {
+                    codeResponseJson = new JSONObject(savedInstanceState.getString(OAUTH2_CODE_RESULT));
+                }
+            } catch (JSONException e) {
+                Log.e(TAG, "onCreate->JSONException: " + e.toString());
+            }
+            // END of getting the state of oAuth2 components.
+            
+            // Getting contents of each field.
+            EditText hostUrl = (EditText)findViewById(R.id.host_URL);
+            hostUrl.setText(savedInstanceState.getString(HOST_URL_TEXT), TextView.BufferType.EDITABLE);
+            EditText oauth2Url = (EditText)findViewById(R.id.oAuth_URL);
+            oauth2Url.setText(savedInstanceState.getString(OAUTH2_URL_TEXT), TextView.BufferType.EDITABLE);
+            EditText accountUsername = (EditText)findViewById(R.id.account_username);
+            accountUsername.setText(savedInstanceState.getString(ACCOUNT_USERNAME), TextView.BufferType.EDITABLE);
+            EditText accountPassword = (EditText)findViewById(R.id.account_password);
+            accountPassword.setText(savedInstanceState.getString(ACCOUNT_PASSWORD), TextView.BufferType.EDITABLE);
+            // END of getting contents of each field
 
         } else {
             mStatusText = mStatusIcon = 0;
@@ -125,6 +192,10 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
         iv2.setOnClickListener(this);
         tv.setOnFocusChangeListener(this);
         tv2.setOnFocusChangeListener(this);
+        // Setting the listener for oAuth2 URL TextView.
+        tv3.setOnFocusChangeListener(this);
+        
+        super.onCreate(savedInstanceState);
     }
 
     @Override
@@ -132,6 +203,24 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
         outState.putInt(STATUS_ICON, mStatusIcon);
         outState.putInt(STATUS_TEXT, mStatusText);
         outState.putBoolean(STATUS_CORRECT, mStatusCorrect);
+        
+        // Saving the state of oAuth2 components.
+        outState.putInt(OAUTH2_STATUS_ICON, mOAuth2StatusIcon);
+        outState.putInt(OAUTH2_STATUS_TEXT, mOAuth2StatusText);
+        CheckBox oAuth2Check = (CheckBox) findViewById(R.id.oauth_onOff_check);
+        outState.putBoolean(OAUTH2_IS_CHECKED, oAuth2Check.isChecked());
+        if (codeResponseJson != null){
+            outState.putString(OAUTH2_CODE_RESULT, codeResponseJson.toString());
+        }
+        outState.putString(OAUTH2_BASE_URL, oAuth2BaseUrl);
+        // END of saving the state of oAuth2 components.
+        
+        // Saving contents of each field.
+        outState.putString(HOST_URL_TEXT,((TextView) findViewById(R.id.host_URL)).getText().toString().trim());
+        outState.putString(OAUTH2_URL_TEXT,((TextView) findViewById(R.id.oAuth_URL)).getText().toString().trim());
+        outState.putString(ACCOUNT_USERNAME,((TextView) findViewById(R.id.account_username)).getText().toString().trim());
+        outState.putString(ACCOUNT_PASSWORD,((TextView) findViewById(R.id.account_password)).getText().toString().trim());
+        
         super.onSaveInstanceState(outState);
     }
 
@@ -159,6 +248,37 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
             dialog = working_dialog;
             break;
         }
+        // oAuth2 dialog. We show here to the user the URL and user_code that the user must validate in a web browser.
+        case OAUTH2_LOGIN_PROGRESS: {
+            ProgressDialog working_dialog = new ProgressDialog(this);
+            try {
+                working_dialog.setMessage(String.format(getString(R.string.oauth_code_validation_message), 
+                        codeResponseJson.getString(OAuth2GetCodeRunnable.CODE_VERIFICATION_URL), 
+                        codeResponseJson.getString(OAuth2GetCodeRunnable.CODE_USER_CODE)));
+            } catch (JSONException e) {
+                Log.e(TAG, "onCreateDialog->JSONException: " + e.toString());
+            }
+            working_dialog.setIndeterminate(true);
+            working_dialog.setCancelable(true);
+            working_dialog
+            .setOnCancelListener(new DialogInterface.OnCancelListener() {
+                @Override
+                public void onCancel(DialogInterface dialog) {
+                    Log.i(TAG, "Login canceled");
+                    if (mOAuth2GetCodeThread != null) {
+                        mOAuth2GetCodeThread.interrupt();
+                        finish();
+                    } 
+                    if (tokenReceiver != null) {
+                        unregisterReceiver(tokenReceiver);
+                        tokenReceiver = null;
+                        finish();
+                    }
+                }
+            });
+            dialog = working_dialog;
+            break;
+        }
         case DIALOG_SSL_VALIDATOR: {
             dialog = SslValidatorDialog.newInstance(this, mLastSslUntrustedServerResult, this);
             break;
@@ -196,6 +316,25 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
             Log.e(TAG, "Incorrect dialog called with id = " + id);
         }
     }
+    
+    @Override
+    protected void onResume() {
+        Log.d(TAG, "onResume() start");
+        // Registering token receiver. We must listening to the service that is pooling to the oAuth server for a token.
+        if (tokenReceiver == null) {
+            IntentFilter tokenFilter = new IntentFilter(OAuth2GetTokenService.TOKEN_RECEIVED_MESSAGE);                
+            tokenReceiver = new TokenReceiver();
+            this.registerReceiver(tokenReceiver,tokenFilter);
+        }
+        super.onResume();
+    }
+
+    @Override
+    protected void onPause() {
+        Log.d(TAG, "onPause() start");
+        super.onPause();
+    }    
+    
 
     public void onAuthenticationResult(boolean success, String message) {
         if (success) {
@@ -240,6 +379,8 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
                     AccountAuthenticator.ACCOUNT_TYPE);
             intent.putExtra(AccountManager.KEY_USERDATA, username);
 
+            accManager.setUserData(account, AccountAuthenticator.KEY_OC_URL,
+                    url.toString());
             accManager.setUserData(account,
                     AccountAuthenticator.KEY_OC_VERSION, mConnChkRunnable
                             .getDiscoveredVersion().toString());
@@ -452,6 +593,28 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
                 v.setInputType(input_type);
                 iv.setVisibility(View.INVISIBLE);
             }
+        // If the focusChange occurs on the oAuth2 URL field, we do this.
+        } else if (view.getId() == R.id.oAuth_URL) {
+            if (!hasFocus) {
+                TextView tv3 = ((TextView) findViewById(R.id.oAuth_URL));
+                // We get the URL of oAuth2 server.
+                oAuth2BaseUrl = tv3.getText().toString().trim();
+                if (oAuth2BaseUrl.length() != 0) {
+                    // We start a thread to get user_code from the oAuth2 server.
+                    setOAuth2ResultIconAndText(R.drawable.progress_small, R.string.oauth_login_connection);
+                    mOAuth2GetCodeRunnable = new OAuth2GetCodeRunnable(oAuth2BaseUrl, this);
+                    mOAuth2GetCodeRunnable.setListener(this, mHandler);
+                    mOAuth2GetCodeThread = new Thread(mOAuth2GetCodeRunnable);
+                    mOAuth2GetCodeThread.start();
+                } else {
+                    findViewById(R.id.refreshButton).setVisibility(
+                            View.INVISIBLE);
+                    setOAuth2ResultIconAndText(0, 0);
+                }
+            } else {
+                // avoids that the 'connect' button can be clicked if the test was previously passed
+                findViewById(R.id.buttonOK).setEnabled(false); 
+            }
         }
     }
 
@@ -481,6 +644,151 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
             view.setInputType(input_type);
         }
     }
+    
+    @Override protected void onDestroy() {       
+        // We must stop the service thats it's pooling to oAuth2 server for a token.
+        Intent tokenService = new Intent(this, OAuth2GetTokenService.class);
+        stopService(tokenService);
+        
+        // We stop listening the result of the pooling service.
+        if (tokenReceiver != null) {
+            unregisterReceiver(tokenReceiver);
+            tokenReceiver = null;
+            finish();
+        }
+
+        super.onDestroy();
+    }    
+    
+    // Controlling the oAuth2 checkbox on the activity: hide and show widgets.
+    public void onOff_check_Click(View view) {
+        CheckBox oAuth2Check = (CheckBox)view;      
+        changeViewByOAuth2Check(oAuth2Check.isChecked());
+
+    }
+    
+    public void changeViewByOAuth2Check(Boolean checked) {
+        
+        EditText oAuth2Url = (EditText) findViewById(R.id.oAuth_URL);
+        EditText accountUsername = (EditText) findViewById(R.id.account_username);
+        EditText accountPassword = (EditText) findViewById(R.id.account_password);
+        ImageView viewPassword = (ImageView) findViewById(R.id.viewPassword); 
+        ImageView auth2ActionIndicator = (ImageView) findViewById(R.id.auth2_action_indicator); 
+        TextView oauth2StatusText = (TextView) findViewById(R.id.oauth2_status_text);         
+
+        if (checked) {
+            oAuth2Url.setVisibility(View.VISIBLE);
+            accountUsername.setVisibility(View.GONE);
+            accountPassword.setVisibility(View.GONE);
+            viewPassword.setVisibility(View.GONE);
+            auth2ActionIndicator.setVisibility(View.INVISIBLE);
+            oauth2StatusText.setVisibility(View.INVISIBLE);
+        } else {
+            oAuth2Url.setVisibility(View.GONE);
+            accountUsername.setVisibility(View.VISIBLE);
+            accountPassword.setVisibility(View.VISIBLE);
+            viewPassword.setVisibility(View.INVISIBLE);
+            auth2ActionIndicator.setVisibility(View.GONE);
+            oauth2StatusText.setVisibility(View.GONE);
+        }     
+
+    }    
+    
+    // Controlling the oAuth2 result of server connection.
+    private void setOAuth2ResultIconAndText(int drawable_id, int text_id) {
+        ImageView iv = (ImageView) findViewById(R.id.auth2_action_indicator);
+        TextView tv = (TextView) findViewById(R.id.oauth2_status_text);
+
+        if (drawable_id == 0 && text_id == 0) {
+            iv.setVisibility(View.INVISIBLE);
+            tv.setVisibility(View.INVISIBLE);
+        } else {
+            iv.setImageResource(drawable_id);
+            tv.setText(text_id);
+            iv.setVisibility(View.VISIBLE);
+            tv.setVisibility(View.VISIBLE);
+        }
+    }     
+    
+    // Results from the first call to oAuth2 server : getting the user_code and verification_url.
+    @Override
+    public void onOAuth2GetCodeResult(ResultOAuthType type, JSONObject responseJson) {
+        if ((type == ResultOAuthType.OK_SSL)||(type == ResultOAuthType.OK_NO_SSL)) {
+            codeResponseJson = responseJson;
+            startOAuth2Authentication();
+        } else if (type == ResultOAuthType.HOST_NOT_AVAILABLE) {
+            setOAuth2ResultIconAndText(R.drawable.common_error, R.string.oauth_connection_url_unavailable);
+        }
+    }
+
+    // If the results of getting the user_code and verification_url are OK, we get the received data and we start
+    // the pooling service to oAuth2 server to get a valid token.
+    private void startOAuth2Authentication () {
+        String deviceCode = null;
+        String verificationUrl = null;
+        String userCode = null;
+        int expiresIn = -1;
+        int interval = -1;
+
+        Log.d(TAG, "ResponseOAuth2->" + codeResponseJson.toString());
+
+        try {
+            // We get data that we must show to the user or we will use internally.
+            verificationUrl = codeResponseJson.getString(OAuth2GetCodeRunnable.CODE_VERIFICATION_URL);
+            userCode = codeResponseJson.getString(OAuth2GetCodeRunnable.CODE_USER_CODE);
+            expiresIn = codeResponseJson.getInt(OAuth2GetCodeRunnable.CODE_EXPIRES_IN);                
+
+            // And we get data that we must use to get a token.
+            deviceCode = codeResponseJson.getString(OAuth2GetCodeRunnable.CODE_DEVICE_CODE);
+            interval = codeResponseJson.getInt(OAuth2GetCodeRunnable.CODE_INTERVAL);
+
+        } catch (JSONException e) {
+            Log.e(TAG, "Exception accesing data in Json object" + e.toString());
+        }
+
+        // Updating status widget to OK.
+        setOAuth2ResultIconAndText(R.drawable.ic_ok, R.string.auth_connection_established);
+        
+        // Showing the dialog with instructions for the user.
+        showDialog(OAUTH2_LOGIN_PROGRESS);
+
+        // Loggin all the data.
+        Log.d(TAG, "verificationUrl->" + verificationUrl);
+        Log.d(TAG, "userCode->" + userCode);
+        Log.d(TAG, "deviceCode->" + deviceCode);
+        Log.d(TAG, "expiresIn->" + expiresIn);
+        Log.d(TAG, "interval->" + interval);
+
+        // Starting the pooling service.
+        try {
+            Intent tokenService = new Intent(this, OAuth2GetTokenService.class);
+            tokenService.putExtra(OAuth2GetTokenService.TOKEN_BASE_URI, oAuth2BaseUrl);
+            tokenService.putExtra(OAuth2GetTokenService.TOKEN_DEVICE_CODE, deviceCode);
+            tokenService.putExtra(OAuth2GetTokenService.TOKEN_INTERVAL, interval);
+
+            startService(tokenService);
+        }
+        catch (Exception e) {
+            Log.e(TAG, "tokenService creation problem :", e);
+        }
+    }   
+
+    // We get data from the oAuth2 token service with this broadcast receiver.
+    private class TokenReceiver extends BroadcastReceiver {
+        /**
+         * The token is received.
+         *  @author
+         * {@link BroadcastReceiver} to enable oAuth2 token receiving.
+         */
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            @SuppressWarnings("unchecked")
+            HashMap<String, String> tokenResponse = (HashMap<String, String>)intent.getExtras().get(OAuth2GetTokenService.TOKEN_RECEIVED_DATA);
+            Log.d(TAG, "TokenReceiver->" + tokenResponse.get(OAuth2GetTokenService.TOKEN_ACCESS_TOKEN));
+            dismissDialog(OAUTH2_LOGIN_PROGRESS);
+
+        }
+    }
 
 	@Override
 	public void onRemoteOperationFinish(RemoteOperation operation, RemoteOperationResult result) {
@@ -584,5 +892,5 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
     public void onFailedSavingCertificate() {
         showDialog(DIALOG_CERT_NOT_SAVED);
     }
-    
+
 }