--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="src" path="src"/>
+ <classpathentry kind="src" path="gen"/>
+ <classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
+ <classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
+ <classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
+ <classpathentry kind="output" path="bin/classes"/>
+</classpath>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>oc_framework</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>org.eclipse.jdt.core.javabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>com.android.ide.eclipse.adt.ApkBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>com.android.ide.eclipse.adt.AndroidNature</nature>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ </natures>
+</projectDescription>
--- /dev/null
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.source=1.6
--- /dev/null
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.owncloud.android.oc_framework"
+ android:versionCode="1"
+ android:versionName="1.0" >
+
+ <uses-sdk
+ android:minSdkVersion="8"
+ android:targetSdkVersion="18" />
+
+ <application
+ android:allowBackup="true"
+ android:icon="@drawable/ic_launcher"
+ android:label="@string/app_name"
+ android:theme="@style/AppTheme" >
+ </application>
+
+</manifest>
--- /dev/null
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system edit
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+#
+# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
+#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
+
+# Project target.
+target=android-17
+android.library=true
--- /dev/null
+<resources>
+
+ <!--
+ Base application theme for API 11+. This theme completely replaces
+ AppBaseTheme from res/values/styles.xml on API 11+ devices.
+ -->
+ <style name="AppBaseTheme" parent="android:Theme.Holo.Light">
+ <!-- API 11 theme customizations can go here. -->
+ </style>
+
+</resources>
--- /dev/null
+<resources>
+
+ <!--
+ Base application theme for API 14+. This theme completely replaces
+ AppBaseTheme from BOTH res/values/styles.xml and
+ res/values-v11/styles.xml on API 14+ devices.
+ -->
+ <style name="AppBaseTheme" parent="android:Theme.Holo.Light.DarkActionBar">
+ <!-- API 14 theme customizations can go here. -->
+ </style>
+
+</resources>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="account_type">owncloud</string>
+ <string name="authority">org.owncloud</string>
+</resources>
+
--- /dev/null
+<resources>
+
+ <string name="app_name">oc_framework</string>
+
+</resources>
--- /dev/null
+<resources>
+
+ <!--
+ Base application theme, dependent on API level. This theme is replaced
+ by AppBaseTheme from res/values-vXX/styles.xml on newer devices.
+ -->
+ <style name="AppBaseTheme" parent="android:Theme.Light">
+ <!--
+ Theme customizations available in newer API levels can go in
+ res/values-vXX/styles.xml, while customizations related to
+ backward-compatibility can go here.
+ -->
+ </style>
+
+ <!-- Application theme. -->
+ <style name="AppTheme" parent="AppBaseTheme">
+ <!-- All customizations that are NOT specific to a particular API-level can go here. -->
+ </style>
+
+</resources>
--- /dev/null
+package com.owncloud.android.oc_framework;
+
+import android.app.Application;
+import android.content.Context;
+
+public class MainApp extends Application {
+
+ private static Context mContext;
+
+ public void onCreate(){
+ super.onCreate();
+ MainApp.mContext = getApplicationContext();
+ }
+
+ public static Context getAppContext() {
+ return MainApp.mContext;
+ }
+
+ // Methods to obtain Strings referring app_name
+ // From AccountAuthenticator
+ // public static final String ACCOUNT_TYPE = "owncloud";
+ public static String getAccountType() {
+ return getAppContext().getResources().getString(R.string.account_type);
+ }
+
+ // From AccountAuthenticator
+ // public static final String AUTHORITY = "org.owncloud";
+ public static String getAuthority() {
+ return getAppContext().getResources().getString(R.string.authority);
+ }
+
+ // From AccountAuthenticator
+ // public static final String AUTH_TOKEN_TYPE = "org.owncloud";
+ public static String getAuthTokenType() {
+ return getAppContext().getResources().getString(R.string.authority);
+ }
+
+ // From AccountAuthenticator
+ // public static final String AUTH_TOKEN_TYPE_PASSWORD = "owncloud.password";
+ public static String getAuthTokenTypePass() {
+ return getAppContext().getResources().getString(R.string.account_type) + ".password";
+ }
+
+ // From AccountAuthenticator
+ // public static final String AUTH_TOKEN_TYPE_ACCESS_TOKEN = "owncloud.oauth2.access_token";
+ public static String getAuthTokenTypeAccessToken() {
+ return getAppContext().getResources().getString(R.string.account_type) + ".oauth2.access_token";
+ }
+
+ // From AccountAuthenticator
+ // public static final String AUTH_TOKEN_TYPE_REFRESH_TOKEN = "owncloud.oauth2.refresh_token";
+ public static String getAuthTokenTypeRefreshToken() {
+ return getAppContext().getResources().getString(R.string.account_type) + ".oauth2.refresh_token";
+ }
+
+ // From AccountAuthenticator
+ // public static final String AUTH_TOKEN_TYPE_SAML_WEB_SSO_SESSION_COOKIE = "owncloud.saml.web_sso.session_cookie";
+ public static String getAuthTokenTypeSamlSessionCookie() {
+ return getAppContext().getResources().getString(R.string.account_type) + ".saml.web_sso.session_cookie";
+ }
+
+}
--- /dev/null
+/* ownCloud Android client application
+ * Copyright (C) 2012 Bartek Przybylski
+ * Copyright (C) 2012-2013 ownCloud Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package com.owncloud.android.oc_framework;
+
+public class OwnCloudVersion implements Comparable<OwnCloudVersion> {
+ public static final OwnCloudVersion owncloud_v1 = new OwnCloudVersion(
+ 0x010000);
+ public static final OwnCloudVersion owncloud_v2 = new OwnCloudVersion(
+ 0x020000);
+ public static final OwnCloudVersion owncloud_v3 = new OwnCloudVersion(
+ 0x030000);
+ public static final OwnCloudVersion owncloud_v4 = new OwnCloudVersion(
+ 0x040000);
+ public static final OwnCloudVersion owncloud_v4_5 = new OwnCloudVersion(
+ 0x040500);
+
+ // format is in version
+ // 0xAABBCC
+ // for version AA.BB.CC
+ // ie version 2.0.3 will be stored as 0x030003
+ private int mVersion;
+ private boolean mIsValid;
+
+ public OwnCloudVersion(int version) {
+ mVersion = version;
+ mIsValid = true;
+ }
+
+ public OwnCloudVersion(String version) {
+ mVersion = 0;
+ mIsValid = false;
+ parseVersionString(version);
+ }
+
+ public String toString() {
+ return ((mVersion >> 16) % 256) + "." + ((mVersion >> 8) % 256) + "."
+ + ((mVersion) % 256);
+ }
+
+ public boolean isVersionValid() {
+ return mIsValid;
+ }
+
+ @Override
+ public int compareTo(OwnCloudVersion another) {
+ return another.mVersion == mVersion ? 0
+ : another.mVersion < mVersion ? 1 : -1;
+ }
+
+ private void parseVersionString(String version) {
+ try {
+ String[] nums = version.split("\\.");
+ if (nums.length > 0) {
+ mVersion += Integer.parseInt(nums[0]);
+ }
+ mVersion = mVersion << 8;
+ if (nums.length > 1) {
+ mVersion += Integer.parseInt(nums[1]);
+ }
+ mVersion = mVersion << 8;
+ if (nums.length > 2) {
+ mVersion += Integer.parseInt(nums[2]);
+ }
+ mIsValid = true;
+ } catch (Exception e) {
+ mIsValid = false;
+ }
+ }
+}
--- /dev/null
+package com.owncloud.android.oc_framework.authentication;
+
+public class AccountAuthenticatorConstants {
+
+ public static final String KEY_AUTH_TOKEN_TYPE = "authTokenType";
+ public static final String KEY_REQUIRED_FEATURES = "requiredFeatures";
+ public static final String KEY_LOGIN_OPTIONS = "loginOptions";
+ public static final String KEY_ACCOUNT = "account";
+
+ /**
+ * Value under this key should handle path to webdav php script. Will be
+ * removed and usage should be replaced by combining
+ * {@link com.owncloud.android.authentication.AuthenticatorActivity.KEY_OC_BASE_URL} and
+ * {@link com.owncloud.android.utils.OwnCloudVersion}
+ *
+ * @deprecated
+ */
+ public static final String KEY_OC_URL = "oc_url";
+ /**
+ * Version should be 3 numbers separated by dot so it can be parsed by
+ * {@link com.owncloud.android.utils.OwnCloudVersion}
+ */
+ public static final String KEY_OC_VERSION = "oc_version";
+ /**
+ * Base url should point to owncloud installation without trailing / ie:
+ * http://server/path or https://owncloud.server
+ */
+ public static final String KEY_OC_BASE_URL = "oc_base_url";
+ /**
+ * Flag signaling if the ownCloud server can be accessed with OAuth2 access tokens.
+ */
+ public static final String KEY_SUPPORTS_OAUTH2 = "oc_supports_oauth2";
+ /**
+ * Flag signaling if the ownCloud server can be accessed with session cookies from SAML-based web single-sign-on.
+ */
+ public static final String KEY_SUPPORTS_SAML_WEB_SSO = "oc_supports_saml_web_sso";
+
+}
--- /dev/null
+/* ownCloud Android client application
+ * Copyright (C) 2012 Bartek Przybylski
+ * Copyright (C) 2012-2013 ownCloud Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package com.owncloud.android.oc_framework.authentication;
+
+
+import com.owncloud.android.oc_framework.MainApp;
+import com.owncloud.android.oc_framework.OwnCloudVersion;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AccountsException;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+
+public class AccountUtils {
+ public static final String WEBDAV_PATH_1_2 = "/webdav/owncloud.php";
+ public static final String WEBDAV_PATH_2_0 = "/files/webdav.php";
+ public static final String WEBDAV_PATH_4_0 = "/remote.php/webdav";
+ private static final String ODAV_PATH = "/remote.php/odav";
+ private static final String SAML_SSO_PATH = "/remote.php/webdav";
+ public static final String CARDDAV_PATH_2_0 = "/apps/contacts/carddav.php";
+ public static final String CARDDAV_PATH_4_0 = "/remote/carddav.php";
+ public static final String STATUS_PATH = "/status.php";
+
+ /**
+ * Can be used to get the currently selected ownCloud {@link Account} in the
+ * application preferences.
+ *
+ * @param context The current application {@link Context}
+ * @return The ownCloud {@link Account} currently saved in preferences, or the first
+ * {@link Account} available, if valid (still registered in the system as ownCloud
+ * account). If none is available and valid, returns null.
+ */
+ public static Account getCurrentOwnCloudAccount(Context context) {
+ Account[] ocAccounts = AccountManager.get(context).getAccountsByType(
+ MainApp.getAccountType());
+ Account defaultAccount = null;
+
+ SharedPreferences appPreferences = PreferenceManager
+ .getDefaultSharedPreferences(context);
+ String accountName = appPreferences
+ .getString("select_oc_account", null);
+
+ // account validation: the saved account MUST be in the list of ownCloud Accounts known by the AccountManager
+ if (accountName != null) {
+ for (Account account : ocAccounts) {
+ if (account.name.equals(accountName)) {
+ defaultAccount = account;
+ break;
+ }
+ }
+ }
+
+ if (defaultAccount == null && ocAccounts.length != 0) {
+ // take first account as fallback
+ defaultAccount = ocAccounts[0];
+ }
+
+ return defaultAccount;
+ }
+
+
+ public static boolean exists(Account account, Context context) {
+ Account[] ocAccounts = AccountManager.get(context).getAccountsByType(
+ MainApp.getAccountType());
+
+ if (account != null && account.name != null) {
+ for (Account ac : ocAccounts) {
+ if (ac.name.equals(account.name)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+
+ /**
+ * Checks, whether or not there are any ownCloud accounts setup.
+ *
+ * @return true, if there is at least one account.
+ */
+ public static boolean accountsAreSetup(Context context) {
+ AccountManager accMan = AccountManager.get(context);
+ Account[] accounts = accMan
+ .getAccountsByType(MainApp.getAccountType());
+ return accounts.length > 0;
+ }
+
+
+ public static boolean setCurrentOwnCloudAccount(Context context, String accountName) {
+ boolean result = false;
+ if (accountName != null) {
+ Account[] ocAccounts = AccountManager.get(context).getAccountsByType(
+ MainApp.getAccountType());
+ boolean found = false;
+ for (Account account : ocAccounts) {
+ found = (account.name.equals(accountName));
+ if (found) {
+ SharedPreferences.Editor appPrefs = PreferenceManager
+ .getDefaultSharedPreferences(context).edit();
+ appPrefs.putString("select_oc_account", accountName);
+
+ appPrefs.commit();
+ result = true;
+ break;
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ *
+ * @param version version of owncloud
+ * @return webdav path for given OC version, null if OC version unknown
+ */
+ public static String getWebdavPath(OwnCloudVersion version, boolean supportsOAuth, boolean supportsSamlSso) {
+ if (version != null) {
+ if (supportsOAuth) {
+ return ODAV_PATH;
+ }
+ if (supportsSamlSso) {
+ return SAML_SSO_PATH;
+ }
+ if (version.compareTo(OwnCloudVersion.owncloud_v4) >= 0)
+ return WEBDAV_PATH_4_0;
+ if (version.compareTo(OwnCloudVersion.owncloud_v3) >= 0
+ || version.compareTo(OwnCloudVersion.owncloud_v2) >= 0)
+ return WEBDAV_PATH_2_0;
+ if (version.compareTo(OwnCloudVersion.owncloud_v1) >= 0)
+ return WEBDAV_PATH_1_2;
+ }
+ return null;
+ }
+
+ /**
+ * Returns the proper URL path to access the WebDAV interface of an ownCloud server,
+ * according to its version and the authorization method used.
+ *
+ * @param version Version of ownCloud server.
+ * @param authTokenType Authorization token type, matching some of the AUTH_TOKEN_TYPE_* constants in {@link AccountAuthenticator}.
+ * @return WebDAV path for given OC version and authorization method, null if OC version is unknown.
+ */
+ public static String getWebdavPath(OwnCloudVersion version, String authTokenType) {
+ if (version != null) {
+ if (MainApp.getAuthTokenTypeAccessToken().equals(authTokenType)) {
+ return ODAV_PATH;
+ }
+ if (MainApp.getAuthTokenTypeSamlSessionCookie().equals(authTokenType)) {
+ return SAML_SSO_PATH;
+ }
+ if (version.compareTo(OwnCloudVersion.owncloud_v4) >= 0)
+ return WEBDAV_PATH_4_0;
+ if (version.compareTo(OwnCloudVersion.owncloud_v3) >= 0
+ || version.compareTo(OwnCloudVersion.owncloud_v2) >= 0)
+ return WEBDAV_PATH_2_0;
+ if (version.compareTo(OwnCloudVersion.owncloud_v1) >= 0)
+ return WEBDAV_PATH_1_2;
+ }
+ return null;
+ }
+
+ /**
+ * Constructs full url to host and webdav resource basing on host version
+ * @param context
+ * @param account
+ * @return url or null on failure
+ * @throws AccountNotFoundException When 'account' is unknown for the AccountManager
+ */
+ public static String constructFullURLForAccount(Context context, Account account) throws AccountNotFoundException {
+ AccountManager ama = AccountManager.get(context);
+ String baseurl = ama.getUserData(account, AccountAuthenticatorConstants.KEY_OC_BASE_URL);
+ String strver = ama.getUserData(account, AccountAuthenticatorConstants.KEY_OC_VERSION);
+ boolean supportsOAuth = (ama.getUserData(account, AccountAuthenticatorConstants.KEY_SUPPORTS_OAUTH2) != null);
+ boolean supportsSamlSso = (ama.getUserData(account, AccountAuthenticatorConstants.KEY_SUPPORTS_SAML_WEB_SSO) != null);
+ OwnCloudVersion ver = new OwnCloudVersion(strver);
+ String webdavpath = getWebdavPath(ver, supportsOAuth, supportsSamlSso);
+
+ if (baseurl == null || webdavpath == null)
+ throw new AccountNotFoundException(account, "Account not found", null);
+
+ return baseurl + webdavpath;
+ }
+
+
+ public static class AccountNotFoundException extends AccountsException {
+
+ /** Generated - should be refreshed every time the class changes!! */
+ private static final long serialVersionUID = -9013287181793186830L;
+
+ private Account mFailedAccount;
+
+ public AccountNotFoundException(Account failedAccount, String message, Throwable cause) {
+ super(message, cause);
+ mFailedAccount = failedAccount;
+ }
+
+ public Account getFailedAccount() {
+ return mFailedAccount;
+ }
+ }
+
+}
--- /dev/null
+package com.owncloud.android.oc_framework.network;
+/* ownCloud Android client application
+ * Copyright (C) 2012 Bartek Przybylski
+ * Copyright (C) 2012-2013 ownCloud Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.net.UnknownHostException;
+import java.security.cert.X509Certificate;
+
+import javax.net.SocketFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocket;
+
+import org.apache.commons.httpclient.ConnectTimeoutException;
+import org.apache.commons.httpclient.params.HttpConnectionParams;
+import org.apache.commons.httpclient.protocol.ProtocolSocketFactory;
+import org.apache.http.conn.ssl.X509HostnameVerifier;
+
+import android.util.Log;
+
+
+/**
+ * AdvancedSSLProtocolSocketFactory allows to create SSL {@link Socket}s with
+ * a custom SSLContext and an optional Hostname Verifier.
+ *
+ * @author David A. Velasco
+ */
+
+public class AdvancedSslSocketFactory implements ProtocolSocketFactory {
+
+ private static final String TAG = AdvancedSslSocketFactory.class.getSimpleName();
+
+ private SSLContext mSslContext = null;
+ private AdvancedX509TrustManager mTrustManager = null;
+ private X509HostnameVerifier mHostnameVerifier = null;
+
+ public SSLContext getSslContext() {
+ return mSslContext;
+ }
+
+ /**
+ * Constructor for AdvancedSSLProtocolSocketFactory.
+ */
+ public AdvancedSslSocketFactory(SSLContext sslContext, AdvancedX509TrustManager trustManager, X509HostnameVerifier hostnameVerifier) {
+ if (sslContext == null)
+ throw new IllegalArgumentException("AdvancedSslSocketFactory can not be created with a null SSLContext");
+ if (trustManager == null)
+ throw new IllegalArgumentException("AdvancedSslSocketFactory can not be created with a null Trust Manager");
+ mSslContext = sslContext;
+ mTrustManager = trustManager;
+ mHostnameVerifier = hostnameVerifier;
+ }
+
+ /**
+ * @see ProtocolSocketFactory#createSocket(java.lang.String,int,java.net.InetAddress,int)
+ */
+ public Socket createSocket(String host, int port, InetAddress clientHost, int clientPort) throws IOException, UnknownHostException {
+ Socket socket = mSslContext.getSocketFactory().createSocket(host, port, clientHost, clientPort);
+ verifyPeerIdentity(host, port, socket);
+ return socket;
+ }
+
+
+ /**
+ * Attempts to get a new socket connection to the given host within the
+ * given time limit.
+ *
+ * @param host the host name/IP
+ * @param port the port on the host
+ * @param clientHost the local host name/IP to bind the socket to
+ * @param clientPort the port on the local machine
+ * @param params {@link HttpConnectionParams Http connection parameters}
+ *
+ * @return Socket a new socket
+ *
+ * @throws IOException if an I/O error occurs while creating the socket
+ * @throws UnknownHostException if the IP address of the host cannot be
+ * determined
+ */
+ public Socket createSocket(final String host, final int port,
+ final InetAddress localAddress, final int localPort,
+ final HttpConnectionParams params) throws IOException,
+ UnknownHostException, ConnectTimeoutException {
+ Log.d(TAG, "Creating SSL Socket with remote " + host + ":" + port + ", local " + localAddress + ":" + localPort + ", params: " + params);
+ if (params == null) {
+ throw new IllegalArgumentException("Parameters may not be null");
+ }
+ int timeout = params.getConnectionTimeout();
+ SocketFactory socketfactory = mSslContext.getSocketFactory();
+ Log.d(TAG, " ... with connection timeout " + timeout + " and socket timeout " + params.getSoTimeout());
+ Socket socket = socketfactory.createSocket();
+ SocketAddress localaddr = new InetSocketAddress(localAddress, localPort);
+ SocketAddress remoteaddr = new InetSocketAddress(host, port);
+ socket.setSoTimeout(params.getSoTimeout());
+ socket.bind(localaddr);
+ socket.connect(remoteaddr, timeout);
+ verifyPeerIdentity(host, port, socket);
+ return socket;
+ }
+
+ /**
+ * @see ProtocolSocketFactory#createSocket(java.lang.String,int)
+ */
+ public Socket createSocket(String host, int port) throws IOException,
+ UnknownHostException {
+ Log.d(TAG, "Creating SSL Socket with remote " + host + ":" + port);
+ Socket socket = mSslContext.getSocketFactory().createSocket(host, port);
+ verifyPeerIdentity(host, port, socket);
+ return socket;
+ }
+
+ public boolean equals(Object obj) {
+ return ((obj != null) && obj.getClass().equals(
+ AdvancedSslSocketFactory.class));
+ }
+
+ public int hashCode() {
+ return AdvancedSslSocketFactory.class.hashCode();
+ }
+
+
+ public X509HostnameVerifier getHostNameVerifier() {
+ return mHostnameVerifier;
+ }
+
+
+ public void setHostNameVerifier(X509HostnameVerifier hostnameVerifier) {
+ mHostnameVerifier = hostnameVerifier;
+ }
+
+ /**
+ * Verifies the identity of the server.
+ *
+ * The server certificate is verified first.
+ *
+ * Then, the host name is compared with the content of the server certificate using the current host name verifier, if any.
+ * @param socket
+ */
+ private void verifyPeerIdentity(String host, int port, Socket socket) throws IOException {
+ try {
+ CertificateCombinedException failInHandshake = null;
+ /// 1. VERIFY THE SERVER CERTIFICATE through the registered TrustManager (that should be an instance of AdvancedX509TrustManager)
+ try {
+ SSLSocket sock = (SSLSocket) socket; // a new SSLSession instance is created as a "side effect"
+ sock.startHandshake();
+
+ } catch (RuntimeException e) {
+
+ if (e instanceof CertificateCombinedException) {
+ failInHandshake = (CertificateCombinedException) e;
+ } else {
+ Throwable cause = e.getCause();
+ Throwable previousCause = null;
+ while (cause != null && cause != previousCause && !(cause instanceof CertificateCombinedException)) {
+ previousCause = cause;
+ cause = cause.getCause();
+ }
+ if (cause != null && cause instanceof CertificateCombinedException) {
+ failInHandshake = (CertificateCombinedException)cause;
+ }
+ }
+ if (failInHandshake == null) {
+ throw e;
+ }
+ failInHandshake.setHostInUrl(host);
+
+ }
+
+ /// 2. VERIFY HOSTNAME
+ SSLSession newSession = null;
+ boolean verifiedHostname = true;
+ if (mHostnameVerifier != null) {
+ if (failInHandshake != null) {
+ /// 2.1 : a new SSLSession instance was NOT created in the handshake
+ X509Certificate serverCert = failInHandshake.getServerCertificate();
+ try {
+ mHostnameVerifier.verify(host, serverCert);
+ } catch (SSLException e) {
+ verifiedHostname = false;
+ }
+
+ } else {
+ /// 2.2 : a new SSLSession instance was created in the handshake
+ newSession = ((SSLSocket)socket).getSession();
+ if (!mTrustManager.isKnownServer((X509Certificate)(newSession.getPeerCertificates()[0]))) {
+ verifiedHostname = mHostnameVerifier.verify(host, newSession);
+ }
+ }
+ }
+
+ /// 3. Combine the exceptions to throw, if any
+ if (!verifiedHostname) {
+ SSLPeerUnverifiedException pue = new SSLPeerUnverifiedException("Names in the server certificate do not match to " + host + " in the URL");
+ if (failInHandshake == null) {
+ failInHandshake = new CertificateCombinedException((X509Certificate) newSession.getPeerCertificates()[0]);
+ failInHandshake.setHostInUrl(host);
+ }
+ failInHandshake.setSslPeerUnverifiedException(pue);
+ pue.initCause(failInHandshake);
+ throw pue;
+
+ } else if (failInHandshake != null) {
+ SSLHandshakeException hse = new SSLHandshakeException("Server certificate could not be verified");
+ hse.initCause(failInHandshake);
+ throw hse;
+ }
+
+ } catch (IOException io) {
+ try {
+ socket.close();
+ } catch (Exception x) {
+ // NOTHING - irrelevant exception for the caller
+ }
+ throw io;
+ }
+ }
+
+}
--- /dev/null
+package com.owncloud.android.oc_framework.network;
+/* ownCloud Android client application
+ * Copyright (C) 2012-2013 ownCloud Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertPathValidatorException;
+import java.security.cert.CertStoreException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.X509Certificate;
+
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509TrustManager;
+
+import android.util.Log;
+
+
+/**
+ * @author David A. Velasco
+ */
+public class AdvancedX509TrustManager implements X509TrustManager {
+
+ private static final String TAG = AdvancedX509TrustManager.class.getSimpleName();
+
+ private X509TrustManager mStandardTrustManager = null;
+ private KeyStore mKnownServersKeyStore;
+
+ /**
+ * Constructor for AdvancedX509TrustManager
+ *
+ * @param knownServersCertStore Local certificates store with server certificates explicitly trusted by the user.
+ * @throws CertStoreException When no default X509TrustManager instance was found in the system.
+ */
+ public AdvancedX509TrustManager(KeyStore knownServersKeyStore)
+ throws NoSuchAlgorithmException, KeyStoreException, CertStoreException {
+ super();
+ TrustManagerFactory factory = TrustManagerFactory
+ .getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ factory.init((KeyStore)null);
+ mStandardTrustManager = findX509TrustManager(factory);
+
+ mKnownServersKeyStore = knownServersKeyStore;
+ }
+
+
+ /**
+ * Locates the first X509TrustManager provided by a given TrustManagerFactory
+ * @param factory TrustManagerFactory to inspect in the search for a X509TrustManager
+ * @return The first X509TrustManager found in factory.
+ * @throws CertStoreException When no X509TrustManager instance was found in factory
+ */
+ private X509TrustManager findX509TrustManager(TrustManagerFactory factory) throws CertStoreException {
+ TrustManager tms[] = factory.getTrustManagers();
+ for (int i = 0; i < tms.length; i++) {
+ if (tms[i] instanceof X509TrustManager) {
+ return (X509TrustManager) tms[i];
+ }
+ }
+ return null;
+ }
+
+
+ /**
+ * @see javax.net.ssl.X509TrustManager#checkClientTrusted(X509Certificate[],
+ * String authType)
+ */
+ public void checkClientTrusted(X509Certificate[] certificates, String authType) throws CertificateException {
+ mStandardTrustManager.checkClientTrusted(certificates, authType);
+ }
+
+
+ /**
+ * @see javax.net.ssl.X509TrustManager#checkServerTrusted(X509Certificate[],
+ * String authType)
+ */
+ public void checkServerTrusted(X509Certificate[] certificates, String authType) throws CertificateException {
+ if (!isKnownServer(certificates[0])) {
+ CertificateCombinedException result = new CertificateCombinedException(certificates[0]);
+ try {
+ certificates[0].checkValidity();
+ } catch (CertificateExpiredException c) {
+ result.setCertificateExpiredException(c);
+
+ } catch (CertificateNotYetValidException c) {
+ result.setCertificateNotYetException(c);
+ }
+
+ try {
+ mStandardTrustManager.checkServerTrusted(certificates, authType);
+ } catch (CertificateException c) {
+ Throwable cause = c.getCause();
+ Throwable previousCause = null;
+ while (cause != null && cause != previousCause && !(cause instanceof CertPathValidatorException)) { // getCause() is not funny
+ previousCause = cause;
+ cause = cause.getCause();
+ }
+ if (cause != null && cause instanceof CertPathValidatorException) {
+ result.setCertPathValidatorException((CertPathValidatorException)cause);
+ } else {
+ result.setOtherCertificateException(c);
+ }
+ }
+
+ if (result.isException())
+ throw result;
+
+ }
+ }
+
+
+ /**
+ * @see javax.net.ssl.X509TrustManager#getAcceptedIssuers()
+ */
+ public X509Certificate[] getAcceptedIssuers() {
+ return mStandardTrustManager.getAcceptedIssuers();
+ }
+
+
+ public boolean isKnownServer(X509Certificate cert) {
+ try {
+ return (mKnownServersKeyStore.getCertificateAlias(cert) != null);
+ } catch (KeyStoreException e) {
+ Log.d(TAG, "Fail while checking certificate in the known-servers store");
+ return false;
+ }
+ }
+
+}
\ No newline at end of file
--- /dev/null
+package com.owncloud.android.oc_framework.network;
+/* ownCloud Android client application
+ * Copyright (C) 2012 ownCloud Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+
+
+import java.util.Map;
+
+import org.apache.commons.httpclient.Credentials;
+import org.apache.commons.httpclient.HttpMethod;
+import org.apache.commons.httpclient.auth.AuthChallengeParser;
+import org.apache.commons.httpclient.auth.AuthScheme;
+import org.apache.commons.httpclient.auth.AuthenticationException;
+import org.apache.commons.httpclient.auth.InvalidCredentialsException;
+import org.apache.commons.httpclient.auth.MalformedChallengeException;
+
+import android.util.Log;
+
+
+/**
+ * Bearer authentication scheme as defined in RFC 6750.
+ *
+ * @author David A. Velasco
+ */
+
+public class BearerAuthScheme implements AuthScheme /*extends RFC2617Scheme*/ {
+
+ private static final String TAG = BearerAuthScheme.class.getSimpleName();
+
+ public static final String AUTH_POLICY = "Bearer";
+
+ /** Whether the bearer authentication process is complete */
+ private boolean mComplete;
+
+ /** Authentication parameter map */
+ private Map mParams = null;
+
+
+ /**
+ * Default constructor for the bearer authentication scheme.
+ */
+ public BearerAuthScheme() {
+ mComplete = false;
+ }
+
+ /**
+ * Constructor for the basic authentication scheme.
+ *
+ * @param challenge Authentication challenge
+ *
+ * @throws MalformedChallengeException Thrown if the authentication challenge is malformed
+ *
+ * @deprecated Use parameterless constructor and {@link AuthScheme#processChallenge(String)} method
+ */
+ public BearerAuthScheme(final String challenge) throws MalformedChallengeException {
+ processChallenge(challenge);
+ mComplete = true;
+ }
+
+ /**
+ * Returns textual designation of the bearer authentication scheme.
+ *
+ * @return "Bearer"
+ */
+ public String getSchemeName() {
+ return "bearer";
+ }
+
+ /**
+ * Processes the Bearer challenge.
+ *
+ * @param challenge The challenge string
+ *
+ * @throws MalformedChallengeException Thrown if the authentication challenge is malformed
+ */
+ public void processChallenge(String challenge) throws MalformedChallengeException {
+ String s = AuthChallengeParser.extractScheme(challenge);
+ if (!s.equalsIgnoreCase(getSchemeName())) {
+ throw new MalformedChallengeException(
+ "Invalid " + getSchemeName() + " challenge: " + challenge);
+ }
+ mParams = AuthChallengeParser.extractParams(challenge);
+ mComplete = true;
+ }
+
+ /**
+ * Tests if the Bearer authentication process has been completed.
+ *
+ * @return 'true' if Bearer authorization has been processed, 'false' otherwise.
+ */
+ public boolean isComplete() {
+ return this.mComplete;
+ }
+
+ /**
+ * Produces bearer authorization string for the given set of
+ * {@link Credentials}.
+ *
+ * @param credentials The set of credentials to be used for authentication
+ * @param method Method name is ignored by the bearer authentication scheme
+ * @param uri URI is ignored by the bearer authentication scheme
+ * @throws InvalidCredentialsException If authentication credentials are not valid or not applicable
+ * for this authentication scheme
+ * @throws AuthenticationException If authorization string cannot be generated due to an authentication failure
+ * @return A bearer authorization string
+ *
+ * @deprecated Use {@link #authenticate(Credentials, HttpMethod)}
+ */
+ public String authenticate(Credentials credentials, String method, String uri) throws AuthenticationException {
+ Log.d(TAG, "enter BearerScheme.authenticate(Credentials, String, String)");
+
+ BearerCredentials bearer = null;
+ try {
+ bearer = (BearerCredentials) credentials;
+ } catch (ClassCastException e) {
+ throw new InvalidCredentialsException(
+ "Credentials cannot be used for bearer authentication: "
+ + credentials.getClass().getName());
+ }
+ return BearerAuthScheme.authenticate(bearer);
+ }
+
+
+ /**
+ * Returns 'false'. Bearer authentication scheme is request based.
+ *
+ * @return 'false'.
+ */
+ public boolean isConnectionBased() {
+ return false;
+ }
+
+ /**
+ * Produces bearer authorization string for the given set of {@link Credentials}.
+ *
+ * @param credentials The set of credentials to be used for authentication
+ * @param method The method being authenticated
+ * @throws InvalidCredentialsException If authentication credentials are not valid or not applicable for this authentication
+ * scheme.
+ * @throws AuthenticationException If authorization string cannot be generated due to an authentication failure.
+ *
+ * @return a basic authorization string
+ */
+ public String authenticate(Credentials credentials, HttpMethod method) throws AuthenticationException {
+ Log.d(TAG, "enter BearerScheme.authenticate(Credentials, HttpMethod)");
+
+ if (method == null) {
+ throw new IllegalArgumentException("Method may not be null");
+ }
+ BearerCredentials bearer = null;
+ try {
+ bearer = (BearerCredentials) credentials;
+ } catch (ClassCastException e) {
+ throw new InvalidCredentialsException(
+ "Credentials cannot be used for bearer authentication: "
+ + credentials.getClass().getName());
+ }
+ return BearerAuthScheme.authenticate(
+ bearer,
+ method.getParams().getCredentialCharset());
+ }
+
+ /**
+ * @deprecated Use {@link #authenticate(BearerCredentials, String)}
+ *
+ * Returns a bearer Authorization header value for the given
+ * {@link BearerCredentials}.
+ *
+ * @param credentials The credentials to encode.
+ *
+ * @return A bearer authorization string
+ */
+ public static String authenticate(BearerCredentials credentials) {
+ return authenticate(credentials, "ISO-8859-1");
+ }
+
+ /**
+ * Returns a bearer Authorization header value for the given
+ * {@link BearerCredentials} and charset.
+ *
+ * @param credentials The credentials to encode.
+ * @param charset The charset to use for encoding the credentials
+ *
+ * @return A bearer authorization string
+ *
+ * @since 3.0
+ */
+ public static String authenticate(BearerCredentials credentials, String charset) {
+ Log.d(TAG, "enter BearerAuthScheme.authenticate(BearerCredentials, String)");
+
+ if (credentials == null) {
+ throw new IllegalArgumentException("Credentials may not be null");
+ }
+ if (charset == null || charset.length() == 0) {
+ throw new IllegalArgumentException("charset may not be null or empty");
+ }
+ StringBuffer buffer = new StringBuffer();
+ buffer.append(credentials.getAccessToken());
+
+ //return "Bearer " + EncodingUtil.getAsciiString(EncodingUtil.getBytes(buffer.toString(), charset));
+ return "Bearer " + buffer.toString();
+ }
+
+ /**
+ * Returns a String identifying the authentication challenge. This is
+ * used, in combination with the host and port to determine if
+ * authorization has already been attempted or not. Schemes which
+ * require multiple requests to complete the authentication should
+ * return a different value for each stage in the request.
+ *
+ * Additionally, the ID should take into account any changes to the
+ * authentication challenge and return a different value when appropriate.
+ * For example when the realm changes in basic authentication it should be
+ * considered a different authentication attempt and a different value should
+ * be returned.
+ *
+ * This method simply returns the realm for the challenge.
+ *
+ * @return String a String identifying the authentication challenge.
+ *
+ * @deprecated no longer used
+ */
+ @Override
+ public String getID() {
+ return getRealm();
+ }
+
+ /**
+ * Returns authentication parameter with the given name, if available.
+ *
+ * @param name The name of the parameter to be returned
+ *
+ * @return The parameter with the given name
+ */
+ @Override
+ public String getParameter(String name) {
+ if (name == null) {
+ throw new IllegalArgumentException("Parameter name may not be null");
+ }
+ if (mParams == null) {
+ return null;
+ }
+ return (String) mParams.get(name.toLowerCase());
+ }
+
+ /**
+ * Returns authentication realm. The realm may not be null.
+ *
+ * @return The authentication realm
+ */
+ @Override
+ public String getRealm() {
+ return getParameter("realm");
+ }
+
+}
--- /dev/null
+package com.owncloud.android.oc_framework.network;
+/* ownCloud Android client application
+ * Copyright (C) 2012 ownCloud Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+
+
+import org.apache.commons.httpclient.Credentials;
+import org.apache.commons.httpclient.util.LangUtils;
+
+/**
+ * Bearer token {@link Credentials}
+ *
+ * @author David A. Velasco
+ */
+public class BearerCredentials implements Credentials {
+
+
+ private String mAccessToken;
+
+
+ /**
+ * The constructor with the bearer token
+ *
+ * @param token The bearer token
+ */
+ public BearerCredentials(String token) {
+ /*if (token == null) {
+ throw new IllegalArgumentException("Bearer token may not be null");
+ }*/
+ mAccessToken = (token == null) ? "" : token;
+ }
+
+
+ /**
+ * Returns the access token
+ *
+ * @return The access token
+ */
+ public String getAccessToken() {
+ return mAccessToken;
+ }
+
+
+ /**
+ * Get this object string.
+ *
+ * @return The access token
+ */
+ public String toString() {
+ return mAccessToken;
+ }
+
+ /**
+ * Does a hash of the access token.
+ *
+ * @return The hash code of the access token
+ */
+ public int hashCode() {
+ int hash = LangUtils.HASH_SEED;
+ hash = LangUtils.hashCode(hash, mAccessToken);
+ return hash;
+ }
+
+ /**
+ * These credentials are assumed equal if accessToken is the same.
+ *
+ * @param o The other object to compare with.
+ *
+ * @return 'True' if the object is equivalent.
+ */
+ public boolean equals(Object o) {
+ if (o == null) return false;
+ if (this == o) return true;
+ if (this.getClass().equals(o.getClass())) {
+ BearerCredentials that = (BearerCredentials) o;
+ if (LangUtils.equals(mAccessToken, that.mAccessToken)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+}
+
--- /dev/null
+package com.owncloud.android.oc_framework.network;
+/* ownCloud Android client application
+ * Copyright (C) 2012-2013 ownCloud Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+
+
+import java.security.cert.CertPathValidatorException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.X509Certificate;
+
+import javax.net.ssl.SSLPeerUnverifiedException;
+
+/**
+ * Exception joining all the problems that {@link AdvancedX509TrustManager} can find in
+ * a certificate chain for a server.
+ *
+ * This was initially created as an extension of CertificateException, but some
+ * implementations of the SSL socket layer in existing devices are REPLACING the CertificateException
+ * instances thrown by {@link javax.net.ssl.X509TrustManager#checkServerTrusted(X509Certificate[], String)}
+ * with SSLPeerUnverifiedException FORGETTING THE CAUSING EXCEPTION instead of wrapping it.
+ *
+ * Due to this, extending RuntimeException is necessary to get that the CertificateCombinedException
+ * instance reaches {@link AdvancedSslSocketFactory#verifyPeerIdentity}.
+ *
+ * BE CAREFUL. As a RuntimeException extensions, Java compilers do not require to handle it
+ * in client methods. Be sure to use it only when you know exactly where it will go.
+ *
+ * @author David A. Velasco
+ */
+public class CertificateCombinedException extends RuntimeException {
+
+ /** Generated - to refresh every time the class changes */
+ private static final long serialVersionUID = -8875782030758554999L;
+
+ private X509Certificate mServerCert = null;
+ private String mHostInUrl;
+
+ private CertificateExpiredException mCertificateExpiredException = null;
+ private CertificateNotYetValidException mCertificateNotYetValidException = null;
+ private CertPathValidatorException mCertPathValidatorException = null;
+ private CertificateException mOtherCertificateException = null;
+ private SSLPeerUnverifiedException mSslPeerUnverifiedException = null;
+
+ public CertificateCombinedException(X509Certificate x509Certificate) {
+ mServerCert = x509Certificate;
+ }
+
+ public X509Certificate getServerCertificate() {
+ return mServerCert;
+ }
+
+ public String getHostInUrl() {
+ return mHostInUrl;
+ }
+
+ public void setHostInUrl(String host) {
+ mHostInUrl = host;
+ }
+
+ public CertificateExpiredException getCertificateExpiredException() {
+ return mCertificateExpiredException;
+ }
+
+ public void setCertificateExpiredException(CertificateExpiredException c) {
+ mCertificateExpiredException = c;
+ }
+
+ public CertificateNotYetValidException getCertificateNotYetValidException() {
+ return mCertificateNotYetValidException;
+ }
+
+ public void setCertificateNotYetException(CertificateNotYetValidException c) {
+ mCertificateNotYetValidException = c;
+ }
+
+ public CertPathValidatorException getCertPathValidatorException() {
+ return mCertPathValidatorException;
+ }
+
+ public void setCertPathValidatorException(CertPathValidatorException c) {
+ mCertPathValidatorException = c;
+ }
+
+ public CertificateException getOtherCertificateException() {
+ return mOtherCertificateException;
+ }
+
+ public void setOtherCertificateException(CertificateException c) {
+ mOtherCertificateException = c;
+ }
+
+ public SSLPeerUnverifiedException getSslPeerUnverifiedException() {
+ return mSslPeerUnverifiedException ;
+ }
+
+ public void setSslPeerUnverifiedException(SSLPeerUnverifiedException s) {
+ mSslPeerUnverifiedException = s;
+ }
+
+ public boolean isException() {
+ return (mCertificateExpiredException != null ||
+ mCertificateNotYetValidException != null ||
+ mCertPathValidatorException != null ||
+ mOtherCertificateException != null ||
+ mSslPeerUnverifiedException != null);
+ }
+
+ public boolean isRecoverable() {
+ return (mCertificateExpiredException != null ||
+ mCertificateNotYetValidException != null ||
+ mCertPathValidatorException != null ||
+ mSslPeerUnverifiedException != null);
+ }
+
+}
--- /dev/null
+package com.owncloud.android.oc_framework.network;
+/* ownCloud Android client application
+ * Copyright (C) 2012-2013 ownCloud Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+
+import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
+import org.apache.commons.httpclient.protocol.Protocol;
+import org.apache.http.conn.ssl.BrowserCompatHostnameVerifier;
+import org.apache.http.conn.ssl.X509HostnameVerifier;
+
+import com.owncloud.android.oc_framework.MainApp;
+import com.owncloud.android.oc_framework.authentication.AccountAuthenticatorConstants;
+import com.owncloud.android.oc_framework.authentication.AccountUtils;
+import com.owncloud.android.oc_framework.authentication.AccountUtils.AccountNotFoundException;
+import com.owncloud.android.oc_framework.network.webdav.WebdavClient;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AccountManagerFuture;
+import android.accounts.AuthenticatorException;
+import android.accounts.OperationCanceledException;
+import android.app.Activity;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+
+public class OwnCloudClientUtils {
+
+ final private static String TAG = OwnCloudClientUtils.class.getSimpleName();
+
+ /** Default timeout for waiting data from the server */
+ public static final int DEFAULT_DATA_TIMEOUT = 60000;
+
+ /** Default timeout for establishing a connection */
+ public static final int DEFAULT_CONNECTION_TIMEOUT = 60000;
+
+ /** Connection manager for all the WebdavClients */
+ private static MultiThreadedHttpConnectionManager mConnManager = null;
+
+ private static Protocol mDefaultHttpsProtocol = null;
+
+ private static AdvancedSslSocketFactory mAdvancedSslSocketFactory = null;
+
+ private static X509HostnameVerifier mHostnameVerifier = null;
+
+
+ /**
+ * Creates a WebdavClient setup for an ownCloud account
+ *
+ * Do not call this method from the main thread.
+ *
+ * @param account The ownCloud account
+ * @param appContext Android application context
+ * @return A WebdavClient object ready to be used
+ * @throws AuthenticatorException If the authenticator failed to get the authorization token for the account.
+ * @throws OperationCanceledException If the authenticator operation was cancelled while getting the authorization token for the account.
+ * @throws IOException If there was some I/O error while getting the authorization token for the account.
+ * @throws AccountNotFoundException If 'account' is unknown for the AccountManager
+ */
+ public static WebdavClient createOwnCloudClient (Account account, Context appContext) throws OperationCanceledException, AuthenticatorException, IOException, AccountNotFoundException {
+ //Log_OC.d(TAG, "Creating WebdavClient associated to " + account.name);
+
+ Uri uri = Uri.parse(AccountUtils.constructFullURLForAccount(appContext, account));
+ AccountManager am = AccountManager.get(appContext);
+ boolean isOauth2 = am.getUserData(account, AccountAuthenticatorConstants.KEY_SUPPORTS_OAUTH2) != null; // TODO avoid calling to getUserData here
+ boolean isSamlSso = am.getUserData(account, AccountAuthenticatorConstants.KEY_SUPPORTS_SAML_WEB_SSO) != null;
+ WebdavClient client = createOwnCloudClient(uri, appContext, !isSamlSso);
+ if (isOauth2) {
+ String accessToken = am.blockingGetAuthToken(account, MainApp.getAuthTokenTypeAccessToken(), false);
+ client.setBearerCredentials(accessToken); // TODO not assume that the access token is a bearer token
+
+ } else if (isSamlSso) { // TODO avoid a call to getUserData here
+ String accessToken = am.blockingGetAuthToken(account, MainApp.getAuthTokenTypeSamlSessionCookie(), false);
+ client.setSsoSessionCookie(accessToken);
+
+ } else {
+ String username = account.name.substring(0, account.name.lastIndexOf('@'));
+ //String password = am.getPassword(account);
+ String password = am.blockingGetAuthToken(account, MainApp.getAuthTokenTypePass(), false);
+ client.setBasicCredentials(username, password);
+ }
+
+ return client;
+ }
+
+
+ public static WebdavClient createOwnCloudClient (Account account, Context appContext, Activity currentActivity) throws OperationCanceledException, AuthenticatorException, IOException, AccountNotFoundException {
+ Uri uri = Uri.parse(AccountUtils.constructFullURLForAccount(appContext, account));
+ AccountManager am = AccountManager.get(appContext);
+ boolean isOauth2 = am.getUserData(account, AccountAuthenticatorConstants.KEY_SUPPORTS_OAUTH2) != null; // TODO avoid calling to getUserData here
+ boolean isSamlSso = am.getUserData(account, AccountAuthenticatorConstants.KEY_SUPPORTS_SAML_WEB_SSO) != null;
+ WebdavClient client = createOwnCloudClient(uri, appContext, !isSamlSso);
+
+ if (isOauth2) { // TODO avoid a call to getUserData here
+ AccountManagerFuture<Bundle> future = am.getAuthToken(account, MainApp.getAuthTokenTypeAccessToken(), null, currentActivity, null, null);
+ Bundle result = future.getResult();
+ String accessToken = result.getString(AccountManager.KEY_AUTHTOKEN);
+ if (accessToken == null) throw new AuthenticatorException("WTF!");
+ client.setBearerCredentials(accessToken); // TODO not assume that the access token is a bearer token
+
+ } else if (isSamlSso) { // TODO avoid a call to getUserData here
+ AccountManagerFuture<Bundle> future = am.getAuthToken(account, MainApp.getAuthTokenTypeSamlSessionCookie(), null, currentActivity, null, null);
+ Bundle result = future.getResult();
+ String accessToken = result.getString(AccountManager.KEY_AUTHTOKEN);
+ if (accessToken == null) throw new AuthenticatorException("WTF!");
+ client.setSsoSessionCookie(accessToken);
+
+ } else {
+ String username = account.name.substring(0, account.name.lastIndexOf('@'));
+ //String password = am.getPassword(account);
+ //String password = am.blockingGetAuthToken(account, MainApp.getAuthTokenTypePass(), false);
+ AccountManagerFuture<Bundle> future = am.getAuthToken(account, MainApp.getAuthTokenTypePass(), null, currentActivity, null, null);
+ Bundle result = future.getResult();
+ String password = result.getString(AccountManager.KEY_AUTHTOKEN);
+ client.setBasicCredentials(username, password);
+ }
+
+ return client;
+ }
+
+ /**
+ * Creates a WebdavClient to access a URL and sets the desired parameters for ownCloud client connections.
+ *
+ * @param uri URL to the ownCloud server
+ * @param context Android context where the WebdavClient is being created.
+ * @return A WebdavClient object ready to be used
+ */
+ public static WebdavClient createOwnCloudClient(Uri uri, Context context, boolean followRedirects) {
+ try {
+ registerAdvancedSslContext(true, context);
+ } catch (GeneralSecurityException e) {
+ Log.e(TAG, "Advanced SSL Context could not be loaded. Default SSL management in the system will be used for HTTPS connections", e);
+
+ } catch (IOException e) {
+ Log.e(TAG, "The local server truststore could not be read. Default SSL management in the system will be used for HTTPS connections", e);
+ }
+
+ WebdavClient client = new WebdavClient(getMultiThreadedConnManager());
+
+ client.setDefaultTimeouts(DEFAULT_DATA_TIMEOUT, DEFAULT_CONNECTION_TIMEOUT);
+ client.setBaseUri(uri);
+ client.setFollowRedirects(followRedirects);
+
+ return client;
+ }
+
+
+ /**
+ * Registers or unregisters the proper components for advanced SSL handling.
+ * @throws IOException
+ */
+ private static void registerAdvancedSslContext(boolean register, Context context) throws GeneralSecurityException, IOException {
+ Protocol pr = null;
+ try {
+ pr = Protocol.getProtocol("https");
+ if (pr != null && mDefaultHttpsProtocol == null) {
+ mDefaultHttpsProtocol = pr;
+ }
+ } catch (IllegalStateException e) {
+ // nothing to do here; really
+ }
+ boolean isRegistered = (pr != null && pr.getSocketFactory() instanceof AdvancedSslSocketFactory);
+ if (register && !isRegistered) {
+ Protocol.registerProtocol("https", new Protocol("https", getAdvancedSslSocketFactory(context), 443));
+
+ } else if (!register && isRegistered) {
+ if (mDefaultHttpsProtocol != null) {
+ Protocol.registerProtocol("https", mDefaultHttpsProtocol);
+ }
+ }
+ }
+
+ public static AdvancedSslSocketFactory getAdvancedSslSocketFactory(Context context) throws GeneralSecurityException, IOException {
+ if (mAdvancedSslSocketFactory == null) {
+ KeyStore trustStore = getKnownServersStore(context);
+ AdvancedX509TrustManager trustMgr = new AdvancedX509TrustManager(trustStore);
+ TrustManager[] tms = new TrustManager[] { trustMgr };
+
+ SSLContext sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(null, tms, null);
+
+ mHostnameVerifier = new BrowserCompatHostnameVerifier();
+ mAdvancedSslSocketFactory = new AdvancedSslSocketFactory(sslContext, trustMgr, mHostnameVerifier);
+ }
+ return mAdvancedSslSocketFactory;
+ }
+
+
+ private static String LOCAL_TRUSTSTORE_FILENAME = "knownServers.bks";
+
+ private static String LOCAL_TRUSTSTORE_PASSWORD = "password";
+
+ private static KeyStore mKnownServersStore = null;
+
+ /**
+ * Returns the local store of reliable server certificates, explicitly accepted by the user.
+ *
+ * Returns a KeyStore instance with empty content if the local store was never created.
+ *
+ * Loads the store from the storage environment if needed.
+ *
+ * @param context Android context where the operation is being performed.
+ * @return KeyStore instance with explicitly-accepted server certificates.
+ * @throws KeyStoreException When the KeyStore instance could not be created.
+ * @throws IOException When an existing local trust store could not be loaded.
+ * @throws NoSuchAlgorithmException When the existing local trust store was saved with an unsupported algorithm.
+ * @throws CertificateException When an exception occurred while loading the certificates from the local trust store.
+ */
+ private static KeyStore getKnownServersStore(Context context) throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException {
+ if (mKnownServersStore == null) {
+ //mKnownServersStore = KeyStore.getInstance("BKS");
+ mKnownServersStore = KeyStore.getInstance(KeyStore.getDefaultType());
+ File localTrustStoreFile = new File(context.getFilesDir(), LOCAL_TRUSTSTORE_FILENAME);
+ Log.d(TAG, "Searching known-servers store at " + localTrustStoreFile.getAbsolutePath());
+ if (localTrustStoreFile.exists()) {
+ InputStream in = new FileInputStream(localTrustStoreFile);
+ try {
+ mKnownServersStore.load(in, LOCAL_TRUSTSTORE_PASSWORD.toCharArray());
+ } finally {
+ in.close();
+ }
+ } else {
+ mKnownServersStore.load(null, LOCAL_TRUSTSTORE_PASSWORD.toCharArray()); // necessary to initialize an empty KeyStore instance
+ }
+ }
+ return mKnownServersStore;
+ }
+
+
+ public static void addCertToKnownServersStore(Certificate cert, Context context) throws KeyStoreException, NoSuchAlgorithmException,
+ CertificateException, IOException {
+ KeyStore knownServers = getKnownServersStore(context);
+ knownServers.setCertificateEntry(Integer.toString(cert.hashCode()), cert);
+ FileOutputStream fos = null;
+ try {
+ fos = context.openFileOutput(LOCAL_TRUSTSTORE_FILENAME, Context.MODE_PRIVATE);
+ knownServers.store(fos, LOCAL_TRUSTSTORE_PASSWORD.toCharArray());
+ } finally {
+ fos.close();
+ }
+ }
+
+
+ static private MultiThreadedHttpConnectionManager getMultiThreadedConnManager() {
+ if (mConnManager == null) {
+ mConnManager = new MultiThreadedHttpConnectionManager();
+ mConnManager.getParams().setDefaultMaxConnectionsPerHost(5);
+ mConnManager.getParams().setMaxTotalConnections(5);
+ }
+ return mConnManager;
+ }
+
+
+}
--- /dev/null
+package com.owncloud.android.oc_framework.network;
+/* ownCloud Android client application
+ * Copyright (C) 2012-2013 ownCloud Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+
+
+import java.util.Collection;
+
+import com.owncloud.android.oc_framework.network.webdav.OnDatatransferProgressListener;
+
+
+public interface ProgressiveDataTransferer {
+
+ public void addDatatransferProgressListener (OnDatatransferProgressListener listener);
+
+ public void addDatatransferProgressListeners(Collection<OnDatatransferProgressListener> listeners);
+
+ public void removeDatatransferProgressListener(OnDatatransferProgressListener listener);
+
+}
--- /dev/null
+/* ownCloud Android client application
+ * Copyright (C) 2012-2013 ownCloud Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package com.owncloud.android.oc_framework.network.webdav;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+import org.apache.commons.httpclient.methods.RequestEntity;
+
+import com.owncloud.android.oc_framework.network.ProgressiveDataTransferer;
+
+import android.util.Log;
+
+
+
+/**
+ * A RequestEntity that represents a PIECE of a file.
+ *
+ * @author David A. Velasco
+ */
+public class ChunkFromFileChannelRequestEntity implements RequestEntity, ProgressiveDataTransferer {
+
+ private static final String TAG = ChunkFromFileChannelRequestEntity.class.getSimpleName();
+
+ //private final File mFile;
+ private final FileChannel mChannel;
+ private final String mContentType;
+ private final long mChunkSize;
+ private final File mFile;
+ private long mOffset;
+ private long mTransferred;
+ Set<OnDatatransferProgressListener> mDataTransferListeners = new HashSet<OnDatatransferProgressListener>();
+ private ByteBuffer mBuffer = ByteBuffer.allocate(4096);
+
+ public ChunkFromFileChannelRequestEntity(final FileChannel channel, final String contentType, long chunkSize, final File file) {
+ super();
+ if (channel == null) {
+ throw new IllegalArgumentException("File may not be null");
+ }
+ if (chunkSize <= 0) {
+ throw new IllegalArgumentException("Chunk size must be greater than zero");
+ }
+ mChannel = channel;
+ mContentType = contentType;
+ mChunkSize = chunkSize;
+ mFile = file;
+ mOffset = 0;
+ mTransferred = 0;
+ }
+
+ public void setOffset(long offset) {
+ mOffset = offset;
+ }
+
+ public long getContentLength() {
+ try {
+ return Math.min(mChunkSize, mChannel.size() - mChannel.position());
+ } catch (IOException e) {
+ return mChunkSize;
+ }
+ }
+
+ public String getContentType() {
+ return mContentType;
+ }
+
+ public boolean isRepeatable() {
+ return true;
+ }
+
+ @Override
+ public void addDatatransferProgressListener(OnDatatransferProgressListener listener) {
+ synchronized (mDataTransferListeners) {
+ mDataTransferListeners.add(listener);
+ }
+ }
+
+ @Override
+ public void addDatatransferProgressListeners(Collection<OnDatatransferProgressListener> listeners) {
+ synchronized (mDataTransferListeners) {
+ mDataTransferListeners.addAll(listeners);
+ }
+ }
+
+ @Override
+ public void removeDatatransferProgressListener(OnDatatransferProgressListener listener) {
+ synchronized (mDataTransferListeners) {
+ mDataTransferListeners.remove(listener);
+ }
+ }
+
+
+ public void writeRequest(final OutputStream out) throws IOException {
+ int readCount = 0;
+ Iterator<OnDatatransferProgressListener> it = null;
+
+ try {
+ mChannel.position(mOffset);
+ long size = mFile.length();
+ if (size == 0) size = -1;
+ long maxCount = Math.min(mOffset + mChunkSize, mChannel.size());
+ while (mChannel.position() < maxCount) {
+ readCount = mChannel.read(mBuffer);
+ out.write(mBuffer.array(), 0, readCount);
+ mBuffer.clear();
+ if (mTransferred < maxCount) { // condition to avoid accumulate progress for repeated chunks
+ mTransferred += readCount;
+ }
+ synchronized (mDataTransferListeners) {
+ it = mDataTransferListeners.iterator();
+ while (it.hasNext()) {
+ it.next().onTransferProgress(readCount, mTransferred, size, mFile.getName());
+ }
+ }
+ }
+
+ } catch (IOException io) {
+ Log.e(TAG, io.getMessage());
+ throw new RuntimeException("Ugly solution to workaround the default policy of retries when the server falls while uploading ; temporal fix; really", io);
+
+ }
+ }
+
+}
\ No newline at end of file
--- /dev/null
+/* ownCloud Android client application
+ * Copyright (C) 2012 Bartek Przybylski
+ * Copyright (C) 2012-2013 ownCloud Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package com.owncloud.android.oc_framework.network.webdav;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+import org.apache.commons.httpclient.methods.RequestEntity;
+
+import android.util.Log;
+
+import com.owncloud.android.oc_framework.network.ProgressiveDataTransferer;
+
+
+
+
+/**
+ * A RequestEntity that represents a File.
+ *
+ */
+public class FileRequestEntity implements RequestEntity, ProgressiveDataTransferer {
+
+ final File mFile;
+ final String mContentType;
+ Set<OnDatatransferProgressListener> mDataTransferListeners = new HashSet<OnDatatransferProgressListener>();
+
+ public FileRequestEntity(final File file, final String contentType) {
+ super();
+ this.mFile = file;
+ this.mContentType = contentType;
+ if (file == null) {
+ throw new IllegalArgumentException("File may not be null");
+ }
+ }
+
+ @Override
+ public long getContentLength() {
+ return mFile.length();
+ }
+
+ @Override
+ public String getContentType() {
+ return mContentType;
+ }
+
+ @Override
+ public boolean isRepeatable() {
+ return true;
+ }
+
+ @Override
+ public void addDatatransferProgressListener(OnDatatransferProgressListener listener) {
+ synchronized (mDataTransferListeners) {
+ mDataTransferListeners.add(listener);
+ }
+ }
+
+ @Override
+ public void addDatatransferProgressListeners(Collection<OnDatatransferProgressListener> listeners) {
+ synchronized (mDataTransferListeners) {
+ mDataTransferListeners.addAll(listeners);
+ }
+ }
+
+ @Override
+ public void removeDatatransferProgressListener(OnDatatransferProgressListener listener) {
+ synchronized (mDataTransferListeners) {
+ mDataTransferListeners.remove(listener);
+ }
+ }
+
+
+ @Override
+ public void writeRequest(final OutputStream out) throws IOException {
+ //byte[] tmp = new byte[4096];
+ ByteBuffer tmp = ByteBuffer.allocate(4096);
+ int readResult = 0;
+
+ // TODO(bprzybylski): each mem allocation can throw OutOfMemoryError we need to handle it
+ // globally in some fashionable manner
+ RandomAccessFile raf = new RandomAccessFile(mFile, "r");
+ FileChannel channel = raf.getChannel();
+ Iterator<OnDatatransferProgressListener> it = null;
+ long transferred = 0;
+ long size = mFile.length();
+ if (size == 0) size = -1;
+ try {
+ while ((readResult = channel.read(tmp)) >= 0) {
+ out.write(tmp.array(), 0, readResult);
+ tmp.clear();
+ transferred += readResult;
+ synchronized (mDataTransferListeners) {
+ it = mDataTransferListeners.iterator();
+ while (it.hasNext()) {
+ it.next().onTransferProgress(readResult, transferred, size, mFile.getName());
+ }
+ }
+ }
+
+ } catch (IOException io) {
+ Log.e("FileRequestException", io.getMessage());
+ throw new RuntimeException("Ugly solution to workaround the default policy of retries when the server falls while uploading ; temporal fix; really", io);
+
+ } finally {
+ channel.close();
+ raf.close();
+ }
+ }
+
+}
--- /dev/null
+/* ownCloud Android client application
+ * Copyright (C) 2012 Bartek Przybylski
+ * Copyright (C) 2012-2013 ownCloud Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package com.owncloud.android.oc_framework.network.webdav;
+
+public interface OnDatatransferProgressListener {
+ public void onTransferProgress(long progressRate);
+ public void onTransferProgress(long progressRate, long totalTransferredSoFar, long totalToTransfer, String fileName);
+}
--- /dev/null
+/* ownCloud Android client application
+ * Copyright (C) 2011 Bartek Przybylski
+ * Copyright (C) 2012-2013 ownCloud Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package com.owncloud.android.oc_framework.network.webdav;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.httpclient.Credentials;
+import org.apache.commons.httpclient.Header;
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.HttpConnectionManager;
+import org.apache.commons.httpclient.HttpException;
+import org.apache.commons.httpclient.HttpMethod;
+import org.apache.commons.httpclient.HttpMethodBase;
+import org.apache.commons.httpclient.HttpVersion;
+import org.apache.commons.httpclient.URI;
+import org.apache.commons.httpclient.UsernamePasswordCredentials;
+import org.apache.commons.httpclient.auth.AuthPolicy;
+import org.apache.commons.httpclient.auth.AuthScope;
+import org.apache.commons.httpclient.cookie.CookiePolicy;
+import org.apache.commons.httpclient.methods.HeadMethod;
+import org.apache.commons.httpclient.params.HttpMethodParams;
+import org.apache.http.HttpStatus;
+import org.apache.http.params.CoreProtocolPNames;
+
+import com.owncloud.android.oc_framework.MainApp;
+import com.owncloud.android.oc_framework.network.BearerAuthScheme;
+import com.owncloud.android.oc_framework.network.BearerCredentials;
+
+
+import android.net.Uri;
+import android.util.Log;
+
+public class WebdavClient extends HttpClient {
+ private static final int MAX_REDIRECTIONS_COUNT = 3;
+
+ private Uri mUri;
+ private Credentials mCredentials;
+ private boolean mFollowRedirects;
+ private String mSsoSessionCookie;
+ private String mAuthTokenType;
+ final private static String TAG = "WebdavClient";
+ public static final String USER_AGENT = "Android-ownCloud";
+
+ static private byte[] sExhaustBuffer = new byte[1024];
+
+ /**
+ * Constructor
+ */
+ public WebdavClient(HttpConnectionManager connectionMgr) {
+ super(connectionMgr);
+ Log.d(TAG, "Creating WebdavClient");
+ getParams().setParameter(HttpMethodParams.USER_AGENT, USER_AGENT);
+ getParams().setParameter(CoreProtocolPNames.PROTOCOL_VERSION, HttpVersion.HTTP_1_1);
+ mFollowRedirects = true;
+ mSsoSessionCookie = null;
+ mAuthTokenType = MainApp.getAuthTokenTypePass();
+ }
+
+ public void setBearerCredentials(String accessToken) {
+ AuthPolicy.registerAuthScheme(BearerAuthScheme.AUTH_POLICY, BearerAuthScheme.class);
+
+ List<String> authPrefs = new ArrayList<String>(1);
+ authPrefs.add(BearerAuthScheme.AUTH_POLICY);
+ getParams().setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY, authPrefs);
+
+ mCredentials = new BearerCredentials(accessToken);
+ getState().setCredentials(AuthScope.ANY, mCredentials);
+ mSsoSessionCookie = null;
+ mAuthTokenType = MainApp.getAuthTokenTypeAccessToken();
+ }
+
+ public void setBasicCredentials(String username, String password) {
+ List<String> authPrefs = new ArrayList<String>(1);
+ authPrefs.add(AuthPolicy.BASIC);
+ getParams().setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY, authPrefs);
+
+ getParams().setAuthenticationPreemptive(true);
+ mCredentials = new UsernamePasswordCredentials(username, password);
+ getState().setCredentials(AuthScope.ANY, mCredentials);
+ mSsoSessionCookie = null;
+ mAuthTokenType = MainApp.getAuthTokenTypePass();
+ }
+
+ public void setSsoSessionCookie(String accessToken) {
+ getParams().setAuthenticationPreemptive(false);
+ getParams().setCookiePolicy(CookiePolicy.IGNORE_COOKIES);
+ mSsoSessionCookie = accessToken;
+ mCredentials = null;
+ mAuthTokenType = MainApp.getAuthTokenTypeSamlSessionCookie();
+ }
+
+
+ /**
+ * Check if a file exists in the OC server
+ *
+ * TODO replace with ExistenceOperation
+ *
+ * @return 'true' if the file exists; 'false' it doesn't exist
+ * @throws Exception When the existence could not be determined
+ */
+ public boolean existsFile(String path) throws IOException, HttpException {
+ HeadMethod head = new HeadMethod(mUri.toString() + WebdavUtils.encodePath(path));
+ try {
+ int status = executeMethod(head);
+ Log.d(TAG, "HEAD to " + path + " finished with HTTP status " + status + ((status != HttpStatus.SC_OK)?"(FAIL)":""));
+ exhaustResponse(head.getResponseBodyAsStream());
+ return (status == HttpStatus.SC_OK);
+
+ } finally {
+ head.releaseConnection(); // let the connection available for other methods
+ }
+ }
+
+ /**
+ * Requests the received method with the received timeout (milliseconds).
+ *
+ * Executes the method through the inherited HttpClient.executedMethod(method).
+ *
+ * Sets the socket and connection timeouts only for the method received.
+ *
+ * The timeouts are both in milliseconds; 0 means 'infinite'; < 0 means 'do not change the default'
+ *
+ * @param method HTTP method request.
+ * @param readTimeout Timeout to set for data reception
+ * @param conntionTimout Timeout to set for connection establishment
+ */
+ public int executeMethod(HttpMethodBase method, int readTimeout, int connectionTimeout) throws HttpException, IOException {
+ int oldSoTimeout = getParams().getSoTimeout();
+ int oldConnectionTimeout = getHttpConnectionManager().getParams().getConnectionTimeout();
+ try {
+ if (readTimeout >= 0) {
+ method.getParams().setSoTimeout(readTimeout); // this should be enough...
+ getParams().setSoTimeout(readTimeout); // ... but this looks like necessary for HTTPS
+ }
+ if (connectionTimeout >= 0) {
+ getHttpConnectionManager().getParams().setConnectionTimeout(connectionTimeout);
+ }
+ return executeMethod(method);
+ } finally {
+ getParams().setSoTimeout(oldSoTimeout);
+ getHttpConnectionManager().getParams().setConnectionTimeout(oldConnectionTimeout);
+ }
+ }
+
+
+ @Override
+ public int executeMethod(HttpMethod method) throws IOException, HttpException {
+ boolean customRedirectionNeeded = false;
+ try {
+ method.setFollowRedirects(mFollowRedirects);
+ } catch (Exception e) {
+ //if (mFollowRedirects) Log_OC.d(TAG, "setFollowRedirects failed for " + method.getName() + " method, custom redirection will be used if needed");
+ customRedirectionNeeded = mFollowRedirects;
+ }
+ if (mSsoSessionCookie != null && mSsoSessionCookie.length() > 0) {
+ method.setRequestHeader("Cookie", mSsoSessionCookie);
+ }
+ int status = super.executeMethod(method);
+ int redirectionsCount = 0;
+ while (customRedirectionNeeded &&
+ redirectionsCount < MAX_REDIRECTIONS_COUNT &&
+ ( status == HttpStatus.SC_MOVED_PERMANENTLY ||
+ status == HttpStatus.SC_MOVED_TEMPORARILY ||
+ status == HttpStatus.SC_TEMPORARY_REDIRECT)
+ ) {
+
+ Header location = method.getResponseHeader("Location");
+ if (location != null) {
+ Log.d(TAG, "Location to redirect: " + location.getValue());
+ method.setURI(new URI(location.getValue(), true));
+ status = super.executeMethod(method);
+ redirectionsCount++;
+
+ } else {
+ Log.d(TAG, "No location to redirect!");
+ status = HttpStatus.SC_NOT_FOUND;
+ }
+ }
+
+ return status;
+ }
+
+
+ /**
+ * Exhausts a not interesting HTTP response. Encouraged by HttpClient documentation.
+ *
+ * @param responseBodyAsStream InputStream with the HTTP response to exhaust.
+ */
+ public void exhaustResponse(InputStream responseBodyAsStream) {
+ if (responseBodyAsStream != null) {
+ try {
+ while (responseBodyAsStream.read(sExhaustBuffer) >= 0);
+ responseBodyAsStream.close();
+
+ } catch (IOException io) {
+ Log.e(TAG, "Unexpected exception while exhausting not interesting HTTP response; will be IGNORED", io);
+ }
+ }
+ }
+
+ /**
+ * Sets the connection and wait-for-data timeouts to be applied by default to the methods performed by this client.
+ */
+ public void setDefaultTimeouts(int defaultDataTimeout, int defaultConnectionTimeout) {
+ getParams().setSoTimeout(defaultDataTimeout);
+ getHttpConnectionManager().getParams().setConnectionTimeout(defaultConnectionTimeout);
+ }
+
+ /**
+ * Sets the base URI for the helper methods that receive paths as parameters, instead of full URLs
+ * @param uri
+ */
+ public void setBaseUri(Uri uri) {
+ mUri = uri;
+ }
+
+ public Uri getBaseUri() {
+ return mUri;
+ }
+
+ public final Credentials getCredentials() {
+ return mCredentials;
+ }
+
+ public final String getSsoSessionCookie() {
+ return mSsoSessionCookie;
+ }
+
+ public void setFollowRedirects(boolean followRedirects) {
+ mFollowRedirects = followRedirects;
+ }
+
+ public String getAuthTokenType() {
+ return mAuthTokenType;
+ }
+
+}
--- /dev/null
+/* ownCloud Android client application
+ * Copyright (C) 2012 ownCloud
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+package com.owncloud.android.oc_framework.network.webdav;
+
+import java.util.Date;
+
+import org.apache.jackrabbit.webdav.MultiStatusResponse;
+import org.apache.jackrabbit.webdav.property.DavProperty;
+import org.apache.jackrabbit.webdav.property.DavPropertyName;
+import org.apache.jackrabbit.webdav.property.DavPropertySet;
+
+
+import android.net.Uri;
+import android.util.Log;
+
+public class WebdavEntry {
+ private String mName, mPath, mUri, mContentType, mEtag;
+ private long mContentLength, mCreateTimestamp, mModifiedTimestamp;
+
+ public WebdavEntry(MultiStatusResponse ms, String splitElement) {
+ resetData();
+ if (ms.getStatus().length != 0) {
+ mUri = ms.getHref();
+
+ mPath = mUri.split(splitElement, 2)[1];
+
+ int status = ms.getStatus()[0].getStatusCode();
+ DavPropertySet propSet = ms.getProperties(status);
+ @SuppressWarnings("rawtypes")
+ DavProperty prop = propSet.get(DavPropertyName.DISPLAYNAME);
+ if (prop != null) {
+ mName = (String) prop.getName().toString();
+ mName = mName.substring(1, mName.length()-1);
+ }
+ else {
+ String[] tmp = mPath.split("/");
+ if (tmp.length > 0)
+ mName = tmp[tmp.length - 1];
+ }
+
+ // use unknown mimetype as default behavior
+ mContentType = "application/octet-stream";
+ prop = propSet.get(DavPropertyName.GETCONTENTTYPE);
+ if (prop != null) {
+ mContentType = (String) prop.getValue();
+ // dvelasco: some builds of ownCloud server 4.0.x added a trailing ';' to the MIME type ; if looks fixed, but let's be cautious
+ if (mContentType.indexOf(";") >= 0) {
+ mContentType = mContentType.substring(0, mContentType.indexOf(";"));
+ }
+ }
+
+ // check if it's a folder in the standard way: see RFC2518 12.2 . RFC4918 14.3
+ prop = propSet.get(DavPropertyName.RESOURCETYPE);
+ if (prop!= null) {
+ Object value = prop.getValue();
+ if (value != null) {
+ mContentType = "DIR"; // a specific attribute would be better, but this is enough; unless while we have no reason to distinguish MIME types for folders
+ }
+ }
+
+ prop = propSet.get(DavPropertyName.GETCONTENTLENGTH);
+ if (prop != null)
+ mContentLength = Long.parseLong((String) prop.getValue());
+
+ prop = propSet.get(DavPropertyName.GETLASTMODIFIED);
+ if (prop != null) {
+ Date d = WebdavUtils
+ .parseResponseDate((String) prop.getValue());
+ mModifiedTimestamp = (d != null) ? d.getTime() : 0;
+ }
+
+ prop = propSet.get(DavPropertyName.CREATIONDATE);
+ if (prop != null) {
+ Date d = WebdavUtils
+ .parseResponseDate((String) prop.getValue());
+ mCreateTimestamp = (d != null) ? d.getTime() : 0;
+ }
+
+ prop = propSet.get(DavPropertyName.GETETAG);
+ if (prop != null) {
+ mEtag = (String) prop.getValue();
+ mEtag = mEtag.substring(1, mEtag.length()-1);
+ }
+
+ } else {
+ Log.e("WebdavEntry",
+ "General fuckup, no status for webdav response");
+ }
+ }
+
+ public String path() {
+ return mPath;
+ }
+
+ public String decodedPath() {
+ return Uri.decode(mPath);
+ }
+
+ public String name() {
+ return mName;
+ }
+
+ public boolean isDirectory() {
+ return mContentType.equals("DIR");
+ }
+
+ public String contentType() {
+ return mContentType;
+ }
+
+ public String uri() {
+ return mUri;
+ }
+
+ public long contentLength() {
+ return mContentLength;
+ }
+
+ public long createTimestamp() {
+ return mCreateTimestamp;
+ }
+
+ public long modifiedTimestamp() {
+ return mModifiedTimestamp;
+ }
+
+ public String etag() {
+ return mEtag;
+ }
+
+ private void resetData() {
+ mName = mUri = mContentType = null;
+ mContentLength = mCreateTimestamp = mModifiedTimestamp = 0;
+ }
+}
--- /dev/null
+/* ownCloud Android client application
+ * Copyright (C) 2012 Bartek Przybylski
+ * Copyright (C) 2012-2013 ownCloud Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package com.owncloud.android.oc_framework.network.webdav;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+import android.net.Uri;
+
+public class WebdavUtils {
+ public static final SimpleDateFormat DISPLAY_DATE_FORMAT = new SimpleDateFormat(
+ "dd.MM.yyyy hh:mm");
+ private static final SimpleDateFormat DATETIME_FORMATS[] = {
+ new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US),
+ new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US),
+ new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.sss'Z'", Locale.US),
+ new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US),
+ new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy", Locale.US),
+ new SimpleDateFormat("EEEEEE, dd-MMM-yy HH:mm:ss zzz", Locale.US),
+ new SimpleDateFormat("EEE MMMM d HH:mm:ss yyyy", Locale.US) };
+
+ public static String prepareXmlForPropFind() {
+ String ret = "<?xml version=\"1.0\" ?><D:propfind xmlns:D=\"DAV:\"><D:allprop/></D:propfind>";
+ return ret;
+ }
+
+ public static String prepareXmlForPatch() {
+ return "<?xml version=\"1.0\" ?><D:propertyupdate xmlns:D=\"DAV:\"></D:propertyupdate>";
+ }
+
+ public static Date parseResponseDate(String date) {
+ Date returnDate = null;
+ for (int i = 0; i < DATETIME_FORMATS.length; ++i) {
+ try {
+ returnDate = DATETIME_FORMATS[i].parse(date);
+ return returnDate;
+ } catch (ParseException e) {
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Encodes a path according to URI RFC 2396.
+ *
+ * If the received path doesn't start with "/", the method adds it.
+ *
+ * @param remoteFilePath Path
+ * @return Encoded path according to RFC 2396, always starting with "/"
+ */
+ public static String encodePath(String remoteFilePath) {
+ String encodedPath = Uri.encode(remoteFilePath, "/");
+ if (!encodedPath.startsWith("/"))
+ encodedPath = "/" + encodedPath;
+ return encodedPath;
+ }
+
+}
--- /dev/null
+/* ownCloud Android client application
+ * Copyright (C) 2012 Bartek Przybylski
+ * Copyright (C) 2012-2013 ownCloud Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package com.owncloud.android.oc_framework.operations;
+
+public interface OnRemoteOperationListener {
+
+ void onRemoteOperationFinish(RemoteOperation caller, RemoteOperationResult result);
+
+}
--- /dev/null
+/* ownCloud Android client application
+ * Copyright (C) 2012 Bartek Przybylski
+ * Copyright (C) 2012-2013 ownCloud Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package com.owncloud.android.oc_framework.operations;
+
+public class OperationCancelledException extends Exception {
+
+ /**
+ * Generated serial version - to avoid Java warning
+ */
+ private static final long serialVersionUID = -6350981497740424983L;
+
+}
--- /dev/null
+/* ownCloud Android client application
+ * Copyright (C) 2012-2013 ownCloud Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+package com.owncloud.android.oc_framework.operations;
+
+import java.io.IOException;
+
+import org.apache.commons.httpclient.Credentials;
+
+import com.owncloud.android.oc_framework.MainApp;
+import com.owncloud.android.oc_framework.network.BearerCredentials;
+import com.owncloud.android.oc_framework.network.OwnCloudClientUtils;
+import com.owncloud.android.oc_framework.network.webdav.WebdavClient;
+import com.owncloud.android.oc_framework.operations.RemoteOperationResult.ResultCode;
+
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AccountsException;
+import android.app.Activity;
+import android.content.Context;
+import android.os.Handler;
+import android.util.Log;
+
+
+/**
+ * Operation which execution involves one or several interactions with an ownCloud server.
+ *
+ * Provides methods to execute the operation both synchronously or asynchronously.
+ *
+ * @author David A. Velasco
+ */
+public abstract class RemoteOperation implements Runnable {
+
+ private static final String TAG = RemoteOperation.class.getSimpleName();
+
+ /** ownCloud account in the remote ownCloud server to operate */
+ private Account mAccount = null;
+
+ /** Android Application context */
+ private Context mContext = null;
+
+ /** Object to interact with the remote server */
+ private WebdavClient mClient = null;
+
+ /** Callback object to notify about the execution of the remote operation */
+ private OnRemoteOperationListener mListener = null;
+
+ /** Handler to the thread where mListener methods will be called */
+ private Handler mListenerHandler = null;
+
+ /** Activity */
+ private Activity mCallerActivity;
+
+
+ /**
+ * Abstract method to implement the operation in derived classes.
+ */
+ protected abstract RemoteOperationResult run(WebdavClient client);
+
+
+ /**
+ * Synchronously executes the remote operation on the received ownCloud account.
+ *
+ * Do not call this method from the main thread.
+ *
+ * This method should be used whenever an ownCloud account is available, instead of {@link #execute(WebdavClient)}.
+ *
+ * @param account ownCloud account in remote ownCloud server to reach during the execution of the operation.
+ * @param context Android context for the component calling the method.
+ * @return Result of the operation.
+ */
+ public final RemoteOperationResult execute(Account account, Context context) {
+ if (account == null)
+ throw new IllegalArgumentException("Trying to execute a remote operation with a NULL Account");
+ if (context == null)
+ throw new IllegalArgumentException("Trying to execute a remote operation with a NULL Context");
+ mAccount = account;
+ mContext = context.getApplicationContext();
+ try {
+ mClient = OwnCloudClientUtils.createOwnCloudClient(mAccount, mContext);
+ } catch (Exception e) {
+ Log.e(TAG, "Error while trying to access to " + mAccount.name, e);
+ return new RemoteOperationResult(e);
+ }
+ return run(mClient);
+ }
+
+
+ /**
+ * Synchronously executes the remote operation
+ *
+ * Do not call this method from the main thread.
+ *
+ * @param client Client object to reach an ownCloud server during the execution of the operation.
+ * @return Result of the operation.
+ */
+ public final RemoteOperationResult execute(WebdavClient client) {
+ if (client == null)
+ throw new IllegalArgumentException("Trying to execute a remote operation with a NULL WebdavClient");
+ mClient = client;
+ return run(client);
+ }
+
+
+ /**
+ * Asynchronously executes the remote operation
+ *
+ * This method should be used whenever an ownCloud account is available, instead of {@link #execute(WebdavClient)}.
+ *
+ * @param account ownCloud account in remote ownCloud server to reach during the execution of the operation.
+ * @param context Android context for the component calling the method.
+ * @param listener Listener to be notified about the execution of the operation.
+ * @param listenerHandler Handler associated to the thread where the methods of the listener objects must be called.
+ * @return Thread were the remote operation is executed.
+ */
+ public final Thread execute(Account account, Context context, OnRemoteOperationListener listener, Handler listenerHandler, Activity callerActivity) {
+ if (account == null)
+ throw new IllegalArgumentException("Trying to execute a remote operation with a NULL Account");
+ if (context == null)
+ throw new IllegalArgumentException("Trying to execute a remote operation with a NULL Context");
+ mAccount = account;
+ mContext = context.getApplicationContext();
+ mCallerActivity = callerActivity;
+ mClient = null; // the client instance will be created from mAccount and mContext in the runnerThread to create below
+
+ mListener = listener;
+
+ mListenerHandler = listenerHandler;
+
+ Thread runnerThread = new Thread(this);
+ runnerThread.start();
+ return runnerThread;
+ }
+
+
+ /**
+ * Asynchronously executes the remote operation
+ *
+ * @param client Client object to reach an ownCloud server during the execution of the operation.
+ * @param listener Listener to be notified about the execution of the operation.
+ * @param listenerHandler Handler associated to the thread where the methods of the listener objects must be called.
+ * @return Thread were the remote operation is executed.
+ */
+ public final Thread execute(WebdavClient client, OnRemoteOperationListener listener, Handler listenerHandler) {
+ if (client == null) {
+ throw new IllegalArgumentException("Trying to execute a remote operation with a NULL WebdavClient");
+ }
+ mClient = client;
+
+ if (listener == null) {
+ throw new IllegalArgumentException("Trying to execute a remote operation asynchronously without a listener to notiy the result");
+ }
+ mListener = listener;
+
+ if (listenerHandler == null) {
+ throw new IllegalArgumentException("Trying to execute a remote operation asynchronously without a handler to the listener's thread");
+ }
+ mListenerHandler = listenerHandler;
+
+ Thread runnerThread = new Thread(this);
+ runnerThread.start();
+ return runnerThread;
+ }
+
+ /**
+ * Synchronously retries the remote operation using the same WebdavClient in the last call to {@link RemoteOperation#execute(WebdavClient)}
+ *
+ * @param listener Listener to be notified about the execution of the operation.
+ * @param listenerHandler Handler associated to the thread where the methods of the listener objects must be called.
+ * @return Thread were the remote operation is executed.
+ */
+ public final RemoteOperationResult retry() {
+ return execute(mClient);
+ }
+
+ /**
+ * Asynchronously retries the remote operation using the same WebdavClient in the last call to {@link RemoteOperation#execute(WebdavClient, OnRemoteOperationListener, Handler)}
+ *
+ * @param listener Listener to be notified about the execution of the operation.
+ * @param listenerHandler Handler associated to the thread where the methods of the listener objects must be called.
+ * @return Thread were the remote operation is executed.
+ */
+ public final Thread retry(OnRemoteOperationListener listener, Handler listenerHandler) {
+ return execute(mClient, listener, listenerHandler);
+ }
+
+
+ /**
+ * Asynchronous execution of the operation
+ * started by {@link RemoteOperation#execute(WebdavClient, OnRemoteOperationListener, Handler)},
+ * and result posting.
+ *
+ * TODO refactor && clean the code; now it's a mess
+ */
+ @Override
+ public final void run() {
+ RemoteOperationResult result = null;
+ boolean repeat = false;
+ do {
+ try{
+ if (mClient == null) {
+ if (mAccount != null && mContext != null) {
+ if (mCallerActivity != null) {
+ mClient = OwnCloudClientUtils.createOwnCloudClient(mAccount, mContext, mCallerActivity);
+ } else {
+ mClient = OwnCloudClientUtils.createOwnCloudClient(mAccount, mContext);
+ }
+ } else {
+ throw new IllegalStateException("Trying to run a remote operation asynchronously with no client instance or account");
+ }
+ }
+
+ } catch (IOException e) {
+ Log.e(TAG, "Error while trying to access to " + mAccount.name, new AccountsException("I/O exception while trying to authorize the account", e));
+ result = new RemoteOperationResult(e);
+
+ } catch (AccountsException e) {
+ Log.e(TAG, "Error while trying to access to " + mAccount.name, e);
+ result = new RemoteOperationResult(e);
+ }
+
+ if (result == null)
+ result = run(mClient);
+
+ repeat = false;
+ if (mCallerActivity != null && mAccount != null && mContext != null && !result.isSuccess() &&
+// (result.getCode() == ResultCode.UNAUTHORIZED || (result.isTemporalRedirection() && result.isIdPRedirection()))) {
+ (result.getCode() == ResultCode.UNAUTHORIZED || result.isIdPRedirection())) {
+ /// possible fail due to lack of authorization in an operation performed in foreground
+ Credentials cred = mClient.getCredentials();
+ String ssoSessionCookie = mClient.getSsoSessionCookie();
+ if (cred != null || ssoSessionCookie != null) {
+ /// confirmed : unauthorized operation
+ AccountManager am = AccountManager.get(mContext);
+ boolean bearerAuthorization = (cred != null && cred instanceof BearerCredentials);
+ boolean samlBasedSsoAuthorization = (cred == null && ssoSessionCookie != null);
+ if (bearerAuthorization) {
+ am.invalidateAuthToken(MainApp.getAccountType(), ((BearerCredentials)cred).getAccessToken());
+ } else if (samlBasedSsoAuthorization ) {
+ am.invalidateAuthToken(MainApp.getAccountType(), ssoSessionCookie);
+ } else {
+ am.clearPassword(mAccount);
+ }
+ mClient = null;
+ repeat = true; // when repeated, the creation of a new OwnCloudClient after erasing the saved credentials will trigger the login activity
+ result = null;
+ }
+ }
+ } while (repeat);
+
+ final RemoteOperationResult resultToSend = result;
+ if (mListenerHandler != null && mListener != null) {
+ mListenerHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mListener.onRemoteOperationFinish(RemoteOperation.this, resultToSend);
+ }
+ });
+ }
+ }
+
+
+ /**
+ * Returns the current client instance to access the remote server.
+ *
+ * @return Current client instance to access the remote server.
+ */
+ public final WebdavClient getClient() {
+ return mClient;
+ }
+
+
+}
--- /dev/null
+package com.owncloud.android.oc_framework.operations;
+/* ownCloud Android client application
+ * Copyright (C) 2012 Bartek Przybylski
+ * Copyright (C) 2012-2013 ownCloud Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.net.MalformedURLException;
+import java.net.SocketException;
+import java.net.SocketTimeoutException;
+import java.net.UnknownHostException;
+
+import javax.net.ssl.SSLException;
+
+import org.apache.commons.httpclient.ConnectTimeoutException;
+import org.apache.commons.httpclient.Header;
+import org.apache.commons.httpclient.HttpException;
+import org.apache.commons.httpclient.HttpStatus;
+import org.apache.jackrabbit.webdav.DavException;
+
+import com.owncloud.android.oc_framework.authentication.AccountUtils.AccountNotFoundException;
+import com.owncloud.android.oc_framework.network.CertificateCombinedException;
+
+import android.accounts.Account;
+import android.accounts.AccountsException;
+import android.util.Log;
+
+
+/**
+ * The result of a remote operation required to an ownCloud server.
+ *
+ * Provides a common classification of remote operation results for all the
+ * application.
+ *
+ * @author David A. Velasco
+ */
+public class RemoteOperationResult implements Serializable {
+
+ /** Generated - should be refreshed every time the class changes!! */
+ private static final long serialVersionUID = -4415103901492836870L;
+
+
+
+ private static final String TAG = "RemoteOperationResult";
+
+ public enum ResultCode {
+ OK,
+ OK_SSL,
+ OK_NO_SSL,
+ UNHANDLED_HTTP_CODE,
+ UNAUTHORIZED,
+ FILE_NOT_FOUND,
+ INSTANCE_NOT_CONFIGURED,
+ UNKNOWN_ERROR,
+ WRONG_CONNECTION,
+ TIMEOUT,
+ INCORRECT_ADDRESS,
+ HOST_NOT_AVAILABLE,
+ NO_NETWORK_CONNECTION,
+ SSL_ERROR,
+ SSL_RECOVERABLE_PEER_UNVERIFIED,
+ BAD_OC_VERSION,
+ CANCELLED,
+ INVALID_LOCAL_FILE_NAME,
+ INVALID_OVERWRITE,
+ CONFLICT,
+ OAUTH2_ERROR,
+ SYNC_CONFLICT,
+ LOCAL_STORAGE_FULL,
+ LOCAL_STORAGE_NOT_MOVED,
+ LOCAL_STORAGE_NOT_COPIED,
+ OAUTH2_ERROR_ACCESS_DENIED,
+ QUOTA_EXCEEDED,
+ ACCOUNT_NOT_FOUND,
+ ACCOUNT_EXCEPTION,
+ ACCOUNT_NOT_NEW,
+ ACCOUNT_NOT_THE_SAME
+ }
+
+ private boolean mSuccess = false;
+ private int mHttpCode = -1;
+ private Exception mException = null;
+ private ResultCode mCode = ResultCode.UNKNOWN_ERROR;
+ private String mRedirectedLocation;
+
+ public RemoteOperationResult(ResultCode code) {
+ mCode = code;
+ mSuccess = (code == ResultCode.OK || code == ResultCode.OK_SSL || code == ResultCode.OK_NO_SSL);
+ }
+
+ private RemoteOperationResult(boolean success, int httpCode) {
+ mSuccess = success;
+ mHttpCode = httpCode;
+
+ if (success) {
+ mCode = ResultCode.OK;
+
+ } else if (httpCode > 0) {
+ switch (httpCode) {
+ case HttpStatus.SC_UNAUTHORIZED:
+ mCode = ResultCode.UNAUTHORIZED;
+ break;
+ case HttpStatus.SC_NOT_FOUND:
+ mCode = ResultCode.FILE_NOT_FOUND;
+ break;
+ case HttpStatus.SC_INTERNAL_SERVER_ERROR:
+ mCode = ResultCode.INSTANCE_NOT_CONFIGURED;
+ break;
+ case HttpStatus.SC_CONFLICT:
+ mCode = ResultCode.CONFLICT;
+ break;
+ case HttpStatus.SC_INSUFFICIENT_STORAGE:
+ mCode = ResultCode.QUOTA_EXCEEDED;
+ break;
+ default:
+ mCode = ResultCode.UNHANDLED_HTTP_CODE;
+ Log.d(TAG, "RemoteOperationResult has processed UNHANDLED_HTTP_CODE: " + httpCode);
+ }
+ }
+ }
+
+ public RemoteOperationResult(boolean success, int httpCode, Header[] headers) {
+ this(success, httpCode);
+ if (headers != null) {
+ Header current;
+ for (int i=0; i<headers.length; i++) {
+ current = headers[i];
+ if ("Location".equals(current.getName())) {
+ mRedirectedLocation = current.getValue();
+ break;
+ }
+ }
+ }
+ }
+
+ public RemoteOperationResult(Exception e) {
+ mException = e;
+
+ if (e instanceof OperationCancelledException) {
+ mCode = ResultCode.CANCELLED;
+
+ } else if (e instanceof SocketException) {
+ mCode = ResultCode.WRONG_CONNECTION;
+
+ } else if (e instanceof SocketTimeoutException) {
+ mCode = ResultCode.TIMEOUT;
+
+ } else if (e instanceof ConnectTimeoutException) {
+ mCode = ResultCode.TIMEOUT;
+
+ } else if (e instanceof MalformedURLException) {
+ mCode = ResultCode.INCORRECT_ADDRESS;
+
+ } else if (e instanceof UnknownHostException) {
+ mCode = ResultCode.HOST_NOT_AVAILABLE;
+
+ } else if (e instanceof AccountNotFoundException) {
+ mCode = ResultCode.ACCOUNT_NOT_FOUND;
+
+ } else if (e instanceof AccountsException) {
+ mCode = ResultCode.ACCOUNT_EXCEPTION;
+
+ } else if (e instanceof SSLException || e instanceof RuntimeException) {
+ CertificateCombinedException se = getCertificateCombinedException(e);
+ if (se != null) {
+ mException = se;
+ if (se.isRecoverable()) {
+ mCode = ResultCode.SSL_RECOVERABLE_PEER_UNVERIFIED;
+ }
+ } else if (e instanceof RuntimeException) {
+ mCode = ResultCode.HOST_NOT_AVAILABLE;
+
+ } else {
+ mCode = ResultCode.SSL_ERROR;
+ }
+
+ } else {
+ mCode = ResultCode.UNKNOWN_ERROR;
+ }
+
+ }
+
+ public boolean isSuccess() {
+ return mSuccess;
+ }
+
+ public boolean isCancelled() {
+ return mCode == ResultCode.CANCELLED;
+ }
+
+ public int getHttpCode() {
+ return mHttpCode;
+ }
+
+ public ResultCode getCode() {
+ return mCode;
+ }
+
+ public Exception getException() {
+ return mException;
+ }
+
+ public boolean isSslRecoverableException() {
+ return mCode == ResultCode.SSL_RECOVERABLE_PEER_UNVERIFIED;
+ }
+
+ private CertificateCombinedException getCertificateCombinedException(Exception e) {
+ CertificateCombinedException result = null;
+ if (e instanceof CertificateCombinedException) {
+ return (CertificateCombinedException) e;
+ }
+ Throwable cause = mException.getCause();
+ Throwable previousCause = null;
+ while (cause != null && cause != previousCause && !(cause instanceof CertificateCombinedException)) {
+ previousCause = cause;
+ cause = cause.getCause();
+ }
+ if (cause != null && cause instanceof CertificateCombinedException) {
+ result = (CertificateCombinedException) cause;
+ }
+ return result;
+ }
+
+ public String getLogMessage() {
+
+ if (mException != null) {
+ if (mException instanceof OperationCancelledException) {
+ return "Operation cancelled by the caller";
+
+ } else if (mException instanceof SocketException) {
+ return "Socket exception";
+
+ } else if (mException instanceof SocketTimeoutException) {
+ return "Socket timeout exception";
+
+ } else if (mException instanceof ConnectTimeoutException) {
+ return "Connect timeout exception";
+
+ } else if (mException instanceof MalformedURLException) {
+ return "Malformed URL exception";
+
+ } else if (mException instanceof UnknownHostException) {
+ return "Unknown host exception";
+
+ } else if (mException instanceof CertificateCombinedException) {
+ if (((CertificateCombinedException) mException).isRecoverable())
+ return "SSL recoverable exception";
+ else
+ return "SSL exception";
+
+ } else if (mException instanceof SSLException) {
+ return "SSL exception";
+
+ } else if (mException instanceof DavException) {
+ return "Unexpected WebDAV exception";
+
+ } else if (mException instanceof HttpException) {
+ return "HTTP violation";
+
+ } else if (mException instanceof IOException) {
+ return "Unrecovered transport exception";
+
+ } else if (mException instanceof AccountNotFoundException) {
+ Account failedAccount = ((AccountNotFoundException)mException).getFailedAccount();
+ return mException.getMessage() + " (" + (failedAccount != null ? failedAccount.name : "NULL") + ")";
+
+ } else if (mException instanceof AccountsException) {
+ return "Exception while using account";
+
+ } else {
+ return "Unexpected exception";
+ }
+ }
+
+ if (mCode == ResultCode.INSTANCE_NOT_CONFIGURED) {
+ return "The ownCloud server is not configured!";
+
+ } else if (mCode == ResultCode.NO_NETWORK_CONNECTION) {
+ return "No network connection";
+
+ } else if (mCode == ResultCode.BAD_OC_VERSION) {
+ return "No valid ownCloud version was found at the server";
+
+ } else if (mCode == ResultCode.LOCAL_STORAGE_FULL) {
+ return "Local storage full";
+
+ } else if (mCode == ResultCode.LOCAL_STORAGE_NOT_MOVED) {
+ return "Error while moving file to final directory";
+
+ } else if (mCode == ResultCode.ACCOUNT_NOT_NEW) {
+ return "Account already existing when creating a new one";
+
+ } else if (mCode == ResultCode.ACCOUNT_NOT_THE_SAME) {
+ return "Authenticated with a different account than the one updating";
+ }
+
+ return "Operation finished with HTTP status code " + mHttpCode + " (" + (isSuccess() ? "success" : "fail") + ")";
+
+ }
+
+ public boolean isServerFail() {
+ return (mHttpCode >= HttpStatus.SC_INTERNAL_SERVER_ERROR);
+ }
+
+ public boolean isException() {
+ return (mException != null);
+ }
+
+ public boolean isTemporalRedirection() {
+ return (mHttpCode == 302 || mHttpCode == 307);
+ }
+
+ public String getRedirectedLocation() {
+ return mRedirectedLocation;
+ }
+
+ public boolean isIdPRedirection() {
+ return (mRedirectedLocation != null &&
+ (mRedirectedLocation.toUpperCase().contains("SAML") ||
+ mRedirectedLocation.toLowerCase().contains("wayf")));
+ }
+
+}
--- /dev/null
+package com.owncloud.android.oc_framework.operations.remote;
+
+import java.io.File;
+
+import org.apache.commons.httpclient.HttpStatus;
+import org.apache.jackrabbit.webdav.client.methods.MkColMethod;
+
+import android.util.Log;
+
+import com.owncloud.android.oc_framework.network.webdav.WebdavClient;
+import com.owncloud.android.oc_framework.network.webdav.WebdavUtils;
+import com.owncloud.android.oc_framework.operations.RemoteOperation;
+import com.owncloud.android.oc_framework.operations.RemoteOperationResult;
+
+
+
+/**
+ * Remote operation performing the creation of a new folder in the ownCloud server.
+ *
+ * @author David A. Velasco
+ * @author masensio
+ *
+ */
+public class CreateRemoteFolderOperation extends RemoteOperation {
+
+ private static final String TAG = CreateRemoteFolderOperation.class.getSimpleName();
+
+ private static final int READ_TIMEOUT = 10000;
+ private static final int CONNECTION_TIMEOUT = 5000;
+
+ private static final String PATH_SEPARATOR = "/";
+
+ protected String mRemotePath;
+ protected boolean mCreateFullPath;
+
+ /**
+ * Constructor
+ *
+ * @param remotePath Full path to the new directory to create in the remote server.
+ * @param createFullPath 'True' means that all the ancestor folders should be created if don't exist yet.
+ */
+ public CreateRemoteFolderOperation(String remotePath, boolean createFullPath) {
+ mRemotePath = remotePath;
+ mCreateFullPath = createFullPath;
+ }
+
+ /**
+ * Performs the operation
+ *
+ * @param client Client object to communicate with the remote ownCloud server.
+ */
+ @Override
+ protected RemoteOperationResult run(WebdavClient client) {
+ RemoteOperationResult result = null;
+ MkColMethod mkcol = null;
+
+ try {
+ mkcol = new MkColMethod(client.getBaseUri() + WebdavUtils.encodePath(mRemotePath));
+ int status = client.executeMethod(mkcol, READ_TIMEOUT, CONNECTION_TIMEOUT);
+ if (!mkcol.succeeded() && mkcol.getStatusCode() == HttpStatus.SC_CONFLICT && mCreateFullPath) {
+ result = createParentFolder(getParentPath(), client);
+ status = client.executeMethod(mkcol, READ_TIMEOUT, CONNECTION_TIMEOUT); // second (and last) try
+ }
+
+ result = new RemoteOperationResult(mkcol.succeeded(), status, mkcol.getResponseHeaders());
+ Log.d(TAG, "Create directory " + mRemotePath + ": " + result.getLogMessage());
+ client.exhaustResponse(mkcol.getResponseBodyAsStream());
+
+ } catch (Exception e) {
+ result = new RemoteOperationResult(e);
+ Log.e(TAG, "Create directory " + mRemotePath + ": " + result.getLogMessage(), e);
+
+ } finally {
+ if (mkcol != null)
+ mkcol.releaseConnection();
+ }
+ return result;
+ }
+
+
+ private RemoteOperationResult createParentFolder(String parentPath, WebdavClient client) {
+ RemoteOperation operation = new CreateRemoteFolderOperation( parentPath,
+ mCreateFullPath);
+ return operation.execute(client);
+ }
+
+ private String getParentPath() {
+ String parentPath = new File(mRemotePath).getParent();
+ parentPath = parentPath.endsWith(PATH_SEPARATOR) ? parentPath : parentPath + PATH_SEPARATOR;
+ return parentPath;
+ }
+
+}