From: David A. Velasco Date: Mon, 18 Mar 2013 13:28:49 +0000 (+0100) Subject: Merge tag 'oc-android-1-3-22' into oauth_login X-Git-Tag: oc-android-1.4.3~29^2~11 X-Git-Url: http://git.linex4red.de/pub/Android/ownCloud.git/commitdiff_plain/5665d9c306165c144fc60b9749ac184cb32369c0?ds=inline;hp=-c Merge tag 'oc-android-1-3-22' into oauth_login Conflicts: AndroidManifest.xml src/com/owncloud/android/authenticator/AuthenticationRunnable.java src/com/owncloud/android/authenticator/OnAuthenticationResultListener.java src/com/owncloud/android/authenticator/OnConnectCheckListener.java src/com/owncloud/android/files/services/FileOperation.java src/com/owncloud/android/files/services/InstantUploadService.java src/com/owncloud/android/ui/activity/AuthenticatorActivity.java --- 5665d9c306165c144fc60b9749ac184cb32369c0 diff --combined AndroidManifest.xml index 3cb25781,341cbc5e..f031ad47 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@@ -3,9 -3,11 +3,11 @@@ 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 as published by - the Free Software Foundation, either version 3 of the License, or + the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, @@@ -17,8 -19,8 +19,8 @@@ along with this program. If not, see . --> + android:versionCode="103022" + android:versionName="1.3.22" xmlns:android="http://schemas.android.com/apk/res/android"> @@@ -120,16 -122,11 +122,18 @@@ + android:theme="@style/Theme.ownCloud.noActionBar" + android:launchMode="singleTask"> + + - + + - + + + + + + @@@ -146,6 -143,7 +150,6 @@@ - @@@ -160,11 -158,7 +164,11 @@@ - + + + + + diff --combined res/layout-land/account_setup.xml index 2f54c960,c45405fa..b8a8fcd0 --- a/res/layout-land/account_setup.xml +++ b/res/layout-land/account_setup.xml @@@ -2,10 -2,12 +2,12 @@@ + + + + diff --combined res/layout/account_setup.xml index 62eac2b8,cbb44112..a4868a4e --- a/res/layout/account_setup.xml +++ b/res/layout/account_setup.xml @@@ -2,10 -2,12 +2,12 @@@ + + + + diff --combined res/values/strings.xml index 21938f09,da4d1977..512f6569 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@@ -25,7 -25,6 +25,6 @@@ Settings General - Device tracking Add new session Create image thumbnails Select an account @@@ -111,7 -110,6 +110,7 @@@ Contacts Synchronization failed Synchronization of %1$s could not be completed + Invalid credentials for %1$s Conflicts found %1$d kept-in-sync files could not be sync\'ed Kept-in-sync files failed @@@ -177,14 -175,9 +176,14 @@@ Couldn\'t establish connection Secure connection established Login details - Invalid login / password + Invalid credentials + Unsuccessful authorization + Access denied by authorization server Wrong path given Internal server error, code %1$d + Unexpected state; please, enter the server URL again + Your authorization expired.\nPlease, authorize again + Your saved credentials are invalid.\nPlease, enter the current credentials Application terminated unexpectedly. Would you like to submit a crash report? Send report @@@ -221,12 -214,6 +220,12 @@@ "Unexpected problem ; please select the file from a different app" No file was selected + oAuth2 URL + Login with oAuth2. + Connecting to oAuth2 server… + Please, open a web browser and go to:\n%1$s.\nValidate this code there:\n%2$s + Connection to this URL not available. + Warning The identity of the site could not be verified - The server certificate is not trusted diff --combined res/values/styles.xml index 6cbed998,47d1ec3b..ef0cc095 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@@ -1,4 -1,23 +1,23 @@@ + + + #777777 #000000 diff --combined src/com/owncloud/android/AccountUtils.java index 06ab31a9,dd1893a5..fba0c368 --- a/src/com/owncloud/android/AccountUtils.java +++ b/src/com/owncloud/android/AccountUtils.java @@@ -1,9 -1,10 +1,10 @@@ /* 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 as published by - * the Free Software Foundation, either version 3 of the License, or + * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, @@@ -31,7 -32,6 +32,7 @@@ 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"; 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"; @@@ -61,7 -61,9 +62,9 @@@ break; } } - } else if (ocAccounts.length != 0) { + } + + if (defaultAccount == null && ocAccounts.length != 0) { // we at least need to take first account as fallback defaultAccount = ocAccounts[0]; } @@@ -84,11 -86,26 +87,26 @@@ } - public static void setCurrentOwnCloudAccount(Context context, String name) { - SharedPreferences.Editor appPrefs = PreferenceManager - .getDefaultSharedPreferences(context).edit(); - appPrefs.putString("select_oc_account", name); - appPrefs.commit(); + public static boolean setCurrentOwnCloudAccount(Context context, String accountName) { + boolean result = false; + if (accountName != null) { + Account[] ocAccounts = AccountManager.get(context).getAccountsByType( + AccountAuthenticator.ACCOUNT_TYPE); + 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; } /** @@@ -96,11 -113,8 +114,11 @@@ * @param version version of owncloud * @return webdav path for given OC version, null if OC version unknown */ - public static String getWebdavPath(OwnCloudVersion version) { + public static String getWebdavPath(OwnCloudVersion version, boolean supportsOAuth) { if (version != null) { + if (supportsOAuth) { + return ODAV_PATH; + } if (version.compareTo(OwnCloudVersion.owncloud_v4) >= 0) return WEBDAV_PATH_4_0; if (version.compareTo(OwnCloudVersion.owncloud_v3) >= 0 @@@ -123,9 -137,8 +141,9 @@@ AccountManager ama = AccountManager.get(context); String baseurl = ama.getUserData(account, AccountAuthenticator.KEY_OC_BASE_URL); String strver = ama.getUserData(account, AccountAuthenticator.KEY_OC_VERSION); + boolean supportsOAuth = (ama.getUserData(account, AccountAuthenticator.KEY_SUPPORTS_OAUTH2) != null); OwnCloudVersion ver = new OwnCloudVersion(strver); - String webdavpath = getWebdavPath(ver); + String webdavpath = getWebdavPath(ver, supportsOAuth); if (webdavpath == null) return null; return baseurl + webdavpath; diff --combined src/com/owncloud/android/Uploader.java index 02579d75,6f9dc6dd..aadf4d9e --- a/src/com/owncloud/android/Uploader.java +++ b/src/com/owncloud/android/Uploader.java @@@ -1,9 -1,10 +1,10 @@@ /* 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 as published by - * the Free Software Foundation, either version 3 of the License, or + * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, @@@ -30,6 -31,7 +31,6 @@@ import com.owncloud.android.datamodel.D import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.datamodel.OCFile; import com.owncloud.android.files.services.FileUploader; -import com.owncloud.android.network.OwnCloudClientUtils; import android.accounts.Account; import android.accounts.AccountManager; @@@ -59,6 -61,7 +60,6 @@@ import android.widget.SimpleAdapter import android.widget.Toast; import com.owncloud.android.R; -import eu.alefzero.webdav.WebdavClient; /** * This can be used to upload things to an ownCloud instance. @@@ -138,8 -141,8 +139,8 @@@ public class Uploader extends ListActiv // in API7 < this constatant is defined in // Settings.ADD_ACCOUNT_SETTINGS // and Settings.EXTRA_AUTHORITIES - Intent intent = new Intent("android.settings.ADD_ACCOUNT_SETTINGS"); - intent.putExtra("authorities", new String[] { AccountAuthenticator.AUTH_TOKEN_TYPE }); + Intent intent = new Intent(android.provider.Settings.ACTION_ADD_ACCOUNT); + intent.putExtra("authorities", new String[] { AccountAuthenticator.AUTHORITY }); startActivityForResult(intent, REQUEST_CODE_SETUP_ACCOUNT); } else { // since in API7 there is no direct call for @@@ -355,13 -358,12 +356,13 @@@ public void uploadFiles() { try { + /* TODO - mCreateDir can never be true at this moment; we will replace wdc.createDirectory by CreateFolderOperation when that is fixed WebdavClient wdc = OwnCloudClientUtils.createOwnCloudClient(mAccount, getApplicationContext()); - // create last directory in path if necessary if (mCreateDir) { wdc.createDirectory(mUploadPath); } + */ String[] local = new String[mStreamsToUpload.size()], remote = new String[mStreamsToUpload.size()]; diff --combined src/com/owncloud/android/authenticator/AccountAuthenticator.java index 8b85392e,9d8b4ab0..a42c1f20 --- a/src/com/owncloud/android/authenticator/AccountAuthenticator.java +++ b/src/com/owncloud/android/authenticator/AccountAuthenticator.java @@@ -1,9 -1,10 +1,10 @@@ /* 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 as published by - * the Free Software Foundation, either version 3 of the License, or + * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, @@@ -32,11 -33,7 +33,11 @@@ public class AccountAuthenticator exten * used by application and all extensions. */ public static final String ACCOUNT_TYPE = "owncloud"; + public static final String AUTHORITY = "org.owncloud"; public static final String AUTH_TOKEN_TYPE = "org.owncloud"; + public static final String AUTH_TOKEN_TYPE_PASSWORD = "owncloud.password"; + public static final String AUTH_TOKEN_TYPE_ACCESS_TOKEN = "owncloud.oauth2.access_token"; + public static final String AUTH_TOKEN_TYPE_REFRESH_TOKEN = "owncloud.oauth2.refresh_token"; public static final String KEY_AUTH_TOKEN_TYPE = "authTokenType"; public static final String KEY_REQUIRED_FEATURES = "requiredFeatures"; @@@ -61,13 -58,8 +62,13 @@@ * http://server/path or https://owncloud.server */ public static final String KEY_OC_BASE_URL = "oc_base_url"; - - private static final String TAG = "AccountAuthenticator"; + /** + * Flag signaling if the ownCloud server can be accessed with OAuth2 access tokens. + */ + public static final String KEY_SUPPORTS_OAUTH2 = "oc_supports_oauth2"; + + private static final String TAG = AccountAuthenticator.class.getSimpleName(); + private Context mContext; public AccountAuthenticator(Context context) { @@@ -94,14 -86,13 +95,14 @@@ return e.getFailureBundle(); } final Intent intent = new Intent(mContext, AuthenticatorActivity.class); - intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, - response); + intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response); intent.putExtra(KEY_AUTH_TOKEN_TYPE, authTokenType); intent.putExtra(KEY_REQUIRED_FEATURES, requiredFeatures); intent.putExtra(KEY_LOGIN_OPTIONS, options); + intent.putExtra(AuthenticatorActivity.EXTRA_ACTION, AuthenticatorActivity.ACTION_CREATE); setIntentFlags(intent); + final Bundle bundle = new Bundle(); bundle.putParcelable(AccountManager.KEY_INTENT, intent); return bundle; @@@ -140,14 -131,10 +141,14 @@@ return null; } + /** + * {@inheritDoc} + */ @Override public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException { + /// validate parameters try { validateAccountType(account.type); validateAuthTokenType(authTokenType); @@@ -157,31 -144,22 +158,31 @@@ e.printStackTrace(); return e.getFailureBundle(); } + + /// check if required token is stored final AccountManager am = AccountManager.get(mContext); - final String password = am.getPassword(account); - if (password != null) { + String accessToken; + if (authTokenType.equals(AUTH_TOKEN_TYPE_PASSWORD)) { + accessToken = am.getPassword(account); + } else { + accessToken = am.peekAuthToken(account, authTokenType); + } + if (accessToken != null) { final Bundle result = new Bundle(); result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name); result.putString(AccountManager.KEY_ACCOUNT_TYPE, ACCOUNT_TYPE); - result.putString(AccountManager.KEY_AUTHTOKEN, password); + result.putString(AccountManager.KEY_AUTHTOKEN, accessToken); return result; } - + + /// if not stored, return Intent to access the AuthenticatorActivity and UPDATE the token for the account final Intent intent = new Intent(mContext, AuthenticatorActivity.class); - intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, - response); + intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response); intent.putExtra(KEY_AUTH_TOKEN_TYPE, authTokenType); intent.putExtra(KEY_LOGIN_OPTIONS, options); - intent.putExtra(AuthenticatorActivity.PARAM_USERNAME, account.name); + intent.putExtra(AuthenticatorActivity.EXTRA_ACCOUNT, account); + intent.putExtra(AuthenticatorActivity.EXTRA_ACTION, AuthenticatorActivity.ACTION_UPDATE_TOKEN); + final Bundle bundle = new Bundle(); bundle.putParcelable(AccountManager.KEY_INTENT, intent); @@@ -197,7 -175,7 +198,7 @@@ public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features) throws NetworkErrorException { final Bundle result = new Bundle(); - result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false); + result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, true); return result; } @@@ -227,8 -205,8 +228,8 @@@ private void setIntentFlags(Intent intent) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK); - intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); + //intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK); + //intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); // incompatible with the authorization code grant in OAuth intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); intent.addFlags(Intent.FLAG_FROM_BACKGROUND); } @@@ -242,10 -220,7 +243,10 @@@ private void validateAuthTokenType(String authTokenType) throws UnsupportedAuthTokenTypeException { - if (!authTokenType.equals(AUTH_TOKEN_TYPE)) { + if (!authTokenType.equals(AUTH_TOKEN_TYPE) && + !authTokenType.equals(AUTH_TOKEN_TYPE_PASSWORD) && + !authTokenType.equals(AUTH_TOKEN_TYPE_ACCESS_TOKEN) && + !authTokenType.equals(AUTH_TOKEN_TYPE_REFRESH_TOKEN) ) { throw new UnsupportedAuthTokenTypeException(); } } diff --combined src/com/owncloud/android/files/OwnCloudFileObserver.java index d79a07e2,8e1f7363..8f987fa4 --- a/src/com/owncloud/android/files/OwnCloudFileObserver.java +++ b/src/com/owncloud/android/files/OwnCloudFileObserver.java @@@ -1,9 -1,10 +1,10 @@@ /* 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 as published by - * the Free Software Foundation, either version 3 of the License, or + * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, @@@ -22,11 -23,13 +23,11 @@@ import java.io.File import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.network.OwnCloudClientUtils; import com.owncloud.android.operations.RemoteOperationResult; import com.owncloud.android.operations.SynchronizeFileOperation; import com.owncloud.android.operations.RemoteOperationResult.ResultCode; import com.owncloud.android.ui.activity.ConflictsResolveActivity; -import eu.alefzero.webdav.WebdavClient; import android.accounts.Account; import android.content.Context; @@@ -75,6 -78,7 +76,6 @@@ public class OwnCloudFileObserver exten mPath); return; } - WebdavClient wc = OwnCloudClientUtils.createOwnCloudClient(mOCAccount, mContext); FileDataStorageManager storageManager = new FileDataStorageManager(mOCAccount, mContext.getContentResolver()); OCFile file = storageManager.getFileByLocalPath(mPath); // a fresh object is needed; many things could have occurred to the file since it was registered to observe // again, assuming that local files are linked to a remote file AT MOST, SOMETHING TO BE DONE; @@@ -85,7 -89,7 +86,7 @@@ true, true, mContext); - RemoteOperationResult result = sfo.execute(wc); + RemoteOperationResult result = sfo.execute(mOCAccount, mContext); if (result.getCode() == ResultCode.SYNC_CONFLICT) { // ISSUE 5: if the user is not running the app (this is a service!), this can be very intrusive; a notification should be preferred Intent i = new Intent(mContext, ConflictsResolveActivity.class); diff --combined src/com/owncloud/android/files/services/FileDownloader.java index ed83d863,ac70e39a..d813aaf3 --- a/src/com/owncloud/android/files/services/FileDownloader.java +++ b/src/com/owncloud/android/files/services/FileDownloader.java @@@ -1,9 -1,10 +1,10 @@@ /* 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 as published by - * the Free Software Foundation, either version 3 of the License, or + * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, @@@ -19,7 -20,6 +20,7 @@@ package com.owncloud.android.files.services; import java.io.File; +import java.io.IOException; import java.util.AbstractList; import java.util.Iterator; import java.util.Vector; @@@ -37,7 -37,6 +38,7 @@@ import com.owncloud.android.ui.activity import com.owncloud.android.ui.fragment.FileDetailFragment; import android.accounts.Account; +import android.accounts.AccountsException; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; @@@ -265,30 -264,21 +266,30 @@@ public class FileDownloader extends Ser notifyDownloadStart(mCurrentDownload); - /// prepare client object to send the request to the ownCloud server - if (mDownloadClient == null || !mLastAccount.equals(mCurrentDownload.getAccount())) { - mLastAccount = mCurrentDownload.getAccount(); - mStorageManager = new FileDataStorageManager(mLastAccount, getContentResolver()); - mDownloadClient = OwnCloudClientUtils.createOwnCloudClient(mLastAccount, getApplicationContext()); - } - - /// perform the download RemoteOperationResult downloadResult = null; try { - downloadResult = mCurrentDownload.execute(mDownloadClient); + /// prepare client object to send the request to the ownCloud server + if (mDownloadClient == null || !mLastAccount.equals(mCurrentDownload.getAccount())) { + mLastAccount = mCurrentDownload.getAccount(); + mStorageManager = new FileDataStorageManager(mLastAccount, getContentResolver()); + mDownloadClient = OwnCloudClientUtils.createOwnCloudClient(mLastAccount, getApplicationContext()); + } + + /// perform the download + if (downloadResult == null) { + downloadResult = mCurrentDownload.execute(mDownloadClient); + } if (downloadResult.isSuccess()) { saveDownloadedFile(); } + } catch (AccountsException e) { + Log.e(TAG, "Error while trying to get autorization for " + mLastAccount.name, e); + downloadResult = new RemoteOperationResult(e); + } catch (IOException e) { + Log.e(TAG, "Error while trying to get autorization for " + mLastAccount.name, e); + downloadResult = new RemoteOperationResult(e); + } finally { synchronized(mPendingDownloads) { mPendingDownloads.remove(downloadKey); diff --combined src/com/owncloud/android/files/services/FileUploader.java index 524f4c10,75b8a060..32aa576b --- a/src/com/owncloud/android/files/services/FileUploader.java +++ b/src/com/owncloud/android/files/services/FileUploader.java @@@ -1,9 -1,10 +1,10 @@@ /* 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 as published by - * the Free Software Foundation, either version 3 of the License, or + * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, @@@ -19,7 -20,6 +20,7 @@@ package com.owncloud.android.files.services; import java.io.File; +import java.io.IOException; import java.util.AbstractList; import java.util.Iterator; import java.util.Vector; @@@ -35,8 -35,6 +36,8 @@@ import com.owncloud.android.datamodel.F import com.owncloud.android.datamodel.OCFile; import com.owncloud.android.files.InstantUploadBroadcastReceiver; import com.owncloud.android.operations.ChunkedUploadFileOperation; +import com.owncloud.android.operations.CreateFolderOperation; +import com.owncloud.android.operations.RemoteOperation; import com.owncloud.android.operations.RemoteOperationResult; import com.owncloud.android.operations.UploadFileOperation; import com.owncloud.android.operations.RemoteOperationResult.ResultCode; @@@ -52,7 -50,6 +53,7 @@@ import com.owncloud.android.network.Own import android.accounts.Account; import android.accounts.AccountManager; +import android.accounts.AccountsException; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; @@@ -396,45 -393,34 +397,45 @@@ public class FileUploader extends Servi notifyUploadStart(mCurrentUpload); + RemoteOperationResult uploadResult = null; - /// prepare client object to send requests to the ownCloud server - if (mUploadClient == null || !mLastAccount.equals(mCurrentUpload.getAccount())) { - mLastAccount = mCurrentUpload.getAccount(); - mStorageManager = new FileDataStorageManager(mLastAccount, getContentResolver()); - mUploadClient = OwnCloudClientUtils.createOwnCloudClient(mLastAccount, getApplicationContext()); - } + try { + /// prepare client object to send requests to the ownCloud server + if (mUploadClient == null || !mLastAccount.equals(mCurrentUpload.getAccount())) { + mLastAccount = mCurrentUpload.getAccount(); + mStorageManager = new FileDataStorageManager(mLastAccount, getContentResolver()); + mUploadClient = OwnCloudClientUtils.createOwnCloudClient(mLastAccount, getApplicationContext()); + } - /// create remote folder for instant uploads - if (mCurrentUpload.isRemoteFolderToBeCreated()) { - mUploadClient.createDirectory(InstantUploadBroadcastReceiver.INSTANT_UPLOAD_DIR); // ignoring result; fail could just mean that it already exists, but local database is not synchronized; the upload will be tried anyway - } + /// create remote folder for instant uploads + if (mCurrentUpload.isRemoteFolderToBeCreated()) { + RemoteOperation operation = new CreateFolderOperation( InstantUploadBroadcastReceiver.INSTANT_UPLOAD_DIR, + mStorageManager.getFileByPath(OCFile.PATH_SEPARATOR).getFileId(), // TODO generalize this : INSTANT_UPLOAD_DIR could not be a child of root + mStorageManager); + operation.execute(mUploadClient); // ignoring result; fail could just mean that it already exists, but local database is not synchronized; the upload will be tried anyway + } - /// perform the upload - RemoteOperationResult uploadResult = null; - try { + /// perform the upload uploadResult = mCurrentUpload.execute(mUploadClient); if (uploadResult.isSuccess()) { saveUploadedFile(); } + } catch (AccountsException e) { + Log.e(TAG, "Error while trying to get autorization for " + mLastAccount.name, e); + uploadResult = new RemoteOperationResult(e); + + } catch (IOException e) { + Log.e(TAG, "Error while trying to get autorization for " + mLastAccount.name, e); + uploadResult = new RemoteOperationResult(e); + } finally { synchronized(mPendingUploads) { mPendingUploads.remove(uploadKey); } } - + /// notify result notifyUploadResult(uploadResult, mCurrentUpload); diff --combined src/com/owncloud/android/network/OwnCloudClientUtils.java index c4462188,19844130..a3f322b9 --- a/src/com/owncloud/android/network/OwnCloudClientUtils.java +++ b/src/com/owncloud/android/network/OwnCloudClientUtils.java @@@ -1,9 -1,9 +1,9 @@@ /* 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 as published by - * the Free Software Foundation, either version 3 of the License, or + * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, @@@ -38,24 -38,18 +38,24 @@@ import org.apache.http.conn.ssl.Browser import org.apache.http.conn.ssl.X509HostnameVerifier; import com.owncloud.android.AccountUtils; +import com.owncloud.android.authenticator.AccountAuthenticator; import eu.alefzero.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 = "OwnCloudClientFactory"; + final private static String TAG = OwnCloudClientUtils.class.getSimpleName(); /** Default timeout for waiting data from the server */ public static final int DEFAULT_DATA_TIMEOUT = 60000; @@@ -76,61 -70,45 +76,61 @@@ /** * Creates a WebdavClient setup for an ownCloud account * - * @param account The ownCloud account - * @param context The application context - * @return A WebdavClient object ready to be used + * 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. */ - public static WebdavClient createOwnCloudClient (Account account, Context context) { - Log.d(TAG, "Creating WebdavClient associated to " + account.name); + public static WebdavClient createOwnCloudClient (Account account, Context appContext) throws OperationCanceledException, AuthenticatorException, IOException { + //Log.d(TAG, "Creating WebdavClient associated to " + account.name); - Uri uri = Uri.parse(AccountUtils.constructFullURLForAccount(context, account)); - WebdavClient client = createOwnCloudClient(uri, context); - - String username = account.name.substring(0, account.name.lastIndexOf('@')); - String password = AccountManager.get(context).getPassword(account); - //String password = am.blockingGetAuthToken(mAccount, AccountAuthenticator.AUTH_TOKEN_TYPE, true); + Uri uri = Uri.parse(AccountUtils.constructFullURLForAccount(appContext, account)); + WebdavClient client = createOwnCloudClient(uri, appContext); + AccountManager am = AccountManager.get(appContext); + if (am.getUserData(account, AccountAuthenticator.KEY_SUPPORTS_OAUTH2) != null) { // TODO avoid a call to getUserData here + String accessToken = am.blockingGetAuthToken(account, AccountAuthenticator.AUTH_TOKEN_TYPE_ACCESS_TOKEN, false); + client.setBearerCredentials(accessToken); // TODO not assume that the access token is a bearer token - client.setCredentials(username, password); + } else { + String username = account.name.substring(0, account.name.lastIndexOf('@')); + //String password = am.getPassword(account); + String password = am.blockingGetAuthToken(account, AccountAuthenticator.AUTH_TOKEN_TYPE_PASSWORD, false); + client.setBasicCredentials(username, password); + } return client; } - /** - * Creates a WebdavClient to try a new account before saving it - * - * @param uri URL to the ownCloud server - * @param username User name - * @param password User password - * @param context Android context where the WebdavClient is being created. - * @return A WebdavClient object ready to be used - */ - public static WebdavClient createOwnCloudClient(Uri uri, String username, String password, Context context) { - Log.d(TAG, "Creating WebdavClient for " + username + "@" + uri); - - WebdavClient client = createOwnCloudClient(uri, context); - - client.setCredentials(username, password); + public static WebdavClient createOwnCloudClient (Account account, Context appContext, Activity currentActivity) throws OperationCanceledException, AuthenticatorException, IOException { + Uri uri = Uri.parse(AccountUtils.constructFullURLForAccount(appContext, account)); + WebdavClient client = createOwnCloudClient(uri, appContext); + AccountManager am = AccountManager.get(appContext); + if (am.getUserData(account, AccountAuthenticator.KEY_SUPPORTS_OAUTH2) != null) { // TODO avoid a call to getUserData here + AccountManagerFuture future = am.getAuthToken(account, AccountAuthenticator.AUTH_TOKEN_TYPE_ACCESS_TOKEN, null, currentActivity, null, null); + Bundle result = future.getResult(); + String accessToken = result.getString(AccountManager.KEY_AUTHTOKEN); + //String accessToken = am.blockingGetAuthToken(account, AccountAuthenticator.AUTH_TOKEN_TYPE_ACCESS_TOKEN, false); + if (accessToken == null) throw new AuthenticatorException("WTF!"); + client.setBearerCredentials(accessToken); // TODO not assume that the access token is a bearer token + + } else { + String username = account.name.substring(0, account.name.lastIndexOf('@')); + //String password = am.getPassword(account); + //String password = am.blockingGetAuthToken(account, AccountAuthenticator.AUTH_TOKEN_TYPE_PASSWORD, false); + AccountManagerFuture future = am.getAuthToken(account, AccountAuthenticator.AUTH_TOKEN_TYPE_PASSWORD, 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. @@@ -140,7 -118,7 +140,7 @@@ * @return A WebdavClient object ready to be used */ public static WebdavClient createOwnCloudClient(Uri uri, Context context) { - Log.d(TAG, "Creating WebdavClient for " + uri); + //Log.d(TAG, "Creating WebdavClient for " + uri); //allowSelfsignedCertificates(true); try { @@@ -162,32 -140,6 +162,6 @@@ /** - * Allows or disallows self-signed certificates in ownCloud servers to reach - * - * @param allow 'True' to allow, 'false' to disallow - */ - public static void allowSelfsignedCertificates(boolean allow) { - Protocol pr = null; - try { - pr = Protocol.getProtocol("https"); - if (pr != null && mDefaultHttpsProtocol == null) { - mDefaultHttpsProtocol = pr; - } - } catch (IllegalStateException e) { - // nothing to do here; really - } - boolean isAllowed = (pr != null && pr.getSocketFactory() instanceof EasySSLSocketFactory); - if (allow && !isAllowed) { - Protocol.registerProtocol("https", new Protocol("https", new EasySSLSocketFactory(), 443)); - } else if (!allow && isAllowed) { - if (mDefaultHttpsProtocol != null) { - Protocol.registerProtocol("https", mDefaultHttpsProtocol); - } - } - } - - - /** * Registers or unregisters the proper components for advanced SSL handling. * @throws IOException */ @@@ -292,5 -244,4 +266,5 @@@ return mConnManager; } + } diff --combined src/com/owncloud/android/operations/OwnCloudServerCheckOperation.java index e803616c,00000000..7132c53f mode 100644,000000..100644 --- a/src/com/owncloud/android/operations/OwnCloudServerCheckOperation.java +++ b/src/com/owncloud/android/operations/OwnCloudServerCheckOperation.java @@@ -1,138 -1,0 +1,138 @@@ +/* 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 as published by - * the Free Software Foundation, either version 3 of the License, or ++ * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 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 . + * + */ + +package com.owncloud.android.operations; + +import org.apache.commons.httpclient.HttpStatus; +import org.apache.commons.httpclient.methods.GetMethod; +import org.json.JSONException; +import org.json.JSONObject; + +import com.owncloud.android.AccountUtils; +import com.owncloud.android.utils.OwnCloudVersion; + +import eu.alefzero.webdav.WebdavClient; +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.Uri; +import android.util.Log; + +public class OwnCloudServerCheckOperation extends RemoteOperation { + + /** Maximum time to wait for a response from the server when the connection is being tested, in MILLISECONDs. */ + public static final int TRY_CONNECTION_TIMEOUT = 5000; + + private static final String TAG = OwnCloudServerCheckOperation.class.getSimpleName(); + + private String mUrl; + private RemoteOperationResult mLatestResult; + private Context mContext; + private OwnCloudVersion mOCVersion; + + public OwnCloudServerCheckOperation(String url, Context context) { + mUrl = url; + mContext = context; + mOCVersion = null; + } + + public OwnCloudVersion getDiscoveredVersion() { + return mOCVersion; + } + + private boolean tryConnection(WebdavClient wc, String urlSt) { + boolean retval = false; + GetMethod get = null; + try { + get = new GetMethod(urlSt); + int status = wc.executeMethod(get, TRY_CONNECTION_TIMEOUT, TRY_CONNECTION_TIMEOUT); + String response = get.getResponseBodyAsString(); + if (status == HttpStatus.SC_OK) { + JSONObject json = new JSONObject(response); + if (!json.getBoolean("installed")) { + mLatestResult = new RemoteOperationResult(RemoteOperationResult.ResultCode.INSTANCE_NOT_CONFIGURED); + } else { + mOCVersion = new OwnCloudVersion(json.getString("version")); + if (!mOCVersion.isVersionValid()) { + mLatestResult = new RemoteOperationResult(RemoteOperationResult.ResultCode.BAD_OC_VERSION); + + } else { + mLatestResult = new RemoteOperationResult(urlSt.startsWith("https://") ? + RemoteOperationResult.ResultCode.OK_SSL : + RemoteOperationResult.ResultCode.OK_NO_SSL + ); + + retval = true; + } + } + + } else { + mLatestResult = new RemoteOperationResult(false, status); + } + + } catch (JSONException e) { + mLatestResult = new RemoteOperationResult(RemoteOperationResult.ResultCode.INSTANCE_NOT_CONFIGURED); + + } catch (Exception e) { + mLatestResult = new RemoteOperationResult(e); + + } finally { + if (get != null) + get.releaseConnection(); + } + + if (mLatestResult.isSuccess()) { + Log.i(TAG, "Connection check at " + urlSt + ": " + mLatestResult.getLogMessage()); + + } else if (mLatestResult.getException() != null) { + Log.e(TAG, "Connection check at " + urlSt + ": " + mLatestResult.getLogMessage(), mLatestResult.getException()); + + } else { + Log.e(TAG, "Connection check at " + urlSt + ": " + mLatestResult.getLogMessage()); + } + + return retval; + } + + private boolean isOnline() { + ConnectivityManager cm = (ConnectivityManager) mContext + .getSystemService(Context.CONNECTIVITY_SERVICE); + return cm != null && cm.getActiveNetworkInfo() != null + && cm.getActiveNetworkInfo().isConnectedOrConnecting(); + } + + @Override + protected RemoteOperationResult run(WebdavClient client) { + if (!isOnline()) { + return new RemoteOperationResult(RemoteOperationResult.ResultCode.NO_NETWORK_CONNECTION); + } + if (mUrl.startsWith("http://") || mUrl.startsWith("https://")) { + tryConnection(client, mUrl + AccountUtils.STATUS_PATH); + + } else { + client.setBaseUri(Uri.parse("https://" + mUrl + AccountUtils.STATUS_PATH)); + boolean httpsSuccess = tryConnection(client, "https://" + mUrl + AccountUtils.STATUS_PATH); + if (!httpsSuccess && !mLatestResult.isSslRecoverableException()) { + Log.d(TAG, "establishing secure connection failed, trying non secure connection"); + client.setBaseUri(Uri.parse("http://" + mUrl + AccountUtils.STATUS_PATH)); + tryConnection(client, "http://" + mUrl + AccountUtils.STATUS_PATH); + } + } + return mLatestResult; + } + +} diff --combined src/com/owncloud/android/operations/RemoteOperation.java index 06a70f70,1564f193..19b67af0 --- a/src/com/owncloud/android/operations/RemoteOperation.java +++ b/src/com/owncloud/android/operations/RemoteOperation.java @@@ -1,9 -1,9 +1,9 @@@ /* 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 as published by - * the Free Software Foundation, either version 3 of the License, or + * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, @@@ -17,22 -17,7 +17,22 @@@ */ package com.owncloud.android.operations; +import java.io.IOException; + +import org.apache.commons.httpclient.Credentials; + +import com.owncloud.android.authenticator.AccountAuthenticator; +import com.owncloud.android.network.BearerCredentials; +import com.owncloud.android.network.OwnCloudClientUtils; +import com.owncloud.android.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; import eu.alefzero.webdav.WebdavClient; @@@ -45,15 -30,7 +45,15 @@@ */ public abstract class RemoteOperation implements Runnable { - /** Object to interact with the ownCloud server */ + 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 */ @@@ -62,49 -39,16 +62,49 @@@ /** 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. */ @@@ -116,43 -60,6 +116,43 @@@ } + /** + * 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 + + 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; + } + + /** * Asynchronously executes the remote operation * @@@ -209,74 -116,20 +209,74 @@@ * 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() { - final RemoteOperationResult result = execute(mClient); + 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) { + /// fail due to lack of authorization in an operation performed in foreground + AccountManager am = AccountManager.get(mContext); + Credentials cred = mClient.getCredentials(); + if (cred instanceof BearerCredentials) { + am.invalidateAuthToken(AccountAuthenticator.ACCOUNT_TYPE, ((BearerCredentials)cred).getAccessToken()); + } 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, result); + 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; + } + } diff --combined src/com/owncloud/android/operations/RemoteOperationResult.java index 1644e1a1,718988a4..14cf78fc --- a/src/com/owncloud/android/operations/RemoteOperationResult.java +++ b/src/com/owncloud/android/operations/RemoteOperationResult.java @@@ -1,9 -1,10 +1,10 @@@ /* 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 as published by - * the Free Software Foundation, either version 3 of the License, or + * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, @@@ -46,6 -47,7 +47,6 @@@ public class RemoteOperationResult impl /** Generated - should be refreshed every time the class changes!! */ private static final long serialVersionUID = -7805531062432602444L; - public enum ResultCode { OK, @@@ -68,12 -70,10 +69,12 @@@ INVALID_LOCAL_FILE_NAME, INVALID_OVERWRITE, CONFLICT, + OAUTH2_ERROR, SYNC_CONFLICT, LOCAL_STORAGE_FULL, LOCAL_STORAGE_NOT_MOVED, - LOCAL_STORAGE_NOT_COPIED + LOCAL_STORAGE_NOT_COPIED, + OAUTH2_ERROR_ACCESS_DENIED } private boolean mSuccess = false; diff --combined src/com/owncloud/android/operations/RenameFileOperation.java index cc1e324c,41233f18..b9806575 --- a/src/com/owncloud/android/operations/RenameFileOperation.java +++ b/src/com/owncloud/android/operations/RenameFileOperation.java @@@ -1,9 -1,9 +1,9 @@@ /* 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 as published by - * the Free Software Foundation, either version 3 of the License, or + * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, @@@ -42,7 -42,7 +42,7 @@@ import eu.alefzero.webdav.WebdavUtils */ public class RenameFileOperation extends RemoteOperation { - private static final String TAG = RemoveFileOperation.class.getSimpleName(); + private static final String TAG = RenameFileOperation.class.getSimpleName(); private static final int RENAME_READ_TIMEOUT = 10000; private static final int RENAME_CONNECTION_TIMEOUT = 5000; diff --combined src/com/owncloud/android/syncadapter/AbstractOwnCloudSyncAdapter.java index 8309fab4,d362c55d..b22fdf78 --- a/src/com/owncloud/android/syncadapter/AbstractOwnCloudSyncAdapter.java +++ b/src/com/owncloud/android/syncadapter/AbstractOwnCloudSyncAdapter.java @@@ -1,9 -1,10 +1,10 @@@ /* 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 as published by - * the Free Software Foundation, either version 3 of the License, or + * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, @@@ -142,7 -143,7 +143,7 @@@ public abstract class AbstractOwnCloudS return null; } - protected void initClientForCurrentAccount() throws UnknownHostException { + protected void initClientForCurrentAccount() throws OperationCanceledException, AuthenticatorException, IOException { if (AccountUtils.constructFullURLForAccount(getContext(), account) == null) { throw new UnknownHostException(); } diff --combined src/com/owncloud/android/syncadapter/FileSyncAdapter.java index 4eafeac5,6da3e81d..ed9e84ae --- a/src/com/owncloud/android/syncadapter/FileSyncAdapter.java +++ b/src/com/owncloud/android/syncadapter/FileSyncAdapter.java @@@ -1,9 -1,10 +1,10 @@@ /* 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 as published by - * the Free Software Foundation, either version 3 of the License, or + * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, @@@ -19,6 -20,7 +20,6 @@@ package com.owncloud.android.syncadapter; import java.io.IOException; -import java.net.UnknownHostException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@@ -34,11 -36,8 +35,11 @@@ import com.owncloud.android.operations. import com.owncloud.android.operations.SynchronizeFolderOperation; import com.owncloud.android.operations.UpdateOCVersionOperation; import com.owncloud.android.operations.RemoteOperationResult.ResultCode; +import com.owncloud.android.ui.activity.AuthenticatorActivity; import com.owncloud.android.ui.activity.ErrorsWhileCopyingHandlerActivity; + import android.accounts.Account; +import android.accounts.AccountsException; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; @@@ -104,12 -103,7 +105,12 @@@ public class FileSyncAdapter extends Ab this.setStorageManager(new FileDataStorageManager(account, getContentProvider())); try { this.initClientForCurrentAccount(); - } catch (UnknownHostException e) { + } catch (IOException e) { + /// the account is unknown for the Synchronization Manager, or unreachable for this context; don't try this again + mSyncResult.tooManyRetries = true; + notifyFailedSynchronization(); + return; + } catch (AccountsException e) { /// the account is unknown for the Synchronization Manager, or unreachable for this context; don't try this again mSyncResult.tooManyRetries = true; notifyFailedSynchronization(); @@@ -248,7 -242,6 +249,7 @@@ RemoteOperationResult.ResultCode code = failedResult.getCode(); return (code.equals(RemoteOperationResult.ResultCode.SSL_ERROR) || code.equals(RemoteOperationResult.ResultCode.SSL_RECOVERABLE_PEER_UNVERIFIED) || + code.equals(RemoteOperationResult.ResultCode.UNAUTHORIZED) || code.equals(RemoteOperationResult.ResultCode.BAD_OC_VERSION) || code.equals(RemoteOperationResult.ResultCode.INSTANCE_NOT_CONFIGURED)); } @@@ -299,29 -292,12 +300,29 @@@ private void notifyFailedSynchronization() { Notification notification = new Notification(R.drawable.icon, getContext().getString(R.string.sync_fail_ticker), System.currentTimeMillis()); notification.flags |= Notification.FLAG_AUTO_CANCEL; - // TODO put something smart in the contentIntent below + boolean needsToUpdateCredentials = (mLastFailedResult != null && mLastFailedResult.getCode() == ResultCode.UNAUTHORIZED); + // TODO put something smart in the contentIntent below for all the possible errors notification.contentIntent = PendingIntent.getActivity(getContext().getApplicationContext(), (int)System.currentTimeMillis(), new Intent(), 0); - notification.setLatestEventInfo(getContext().getApplicationContext(), - getContext().getString(R.string.sync_fail_ticker), - String.format(getContext().getString(R.string.sync_fail_content), getAccount().name), - notification.contentIntent); + if (needsToUpdateCredentials) { + // let the user update credentials with one click + Intent updateAccountCredentials = new Intent(getContext(), AuthenticatorActivity.class); + updateAccountCredentials.putExtra(AuthenticatorActivity.EXTRA_ACCOUNT, getAccount()); + updateAccountCredentials.putExtra(AuthenticatorActivity.EXTRA_ACTION, AuthenticatorActivity.ACTION_UPDATE_TOKEN); + updateAccountCredentials.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + updateAccountCredentials.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + updateAccountCredentials.addFlags(Intent.FLAG_FROM_BACKGROUND); + notification.contentIntent = PendingIntent.getActivity(getContext(), (int)System.currentTimeMillis(), updateAccountCredentials, PendingIntent.FLAG_ONE_SHOT); + notification.setLatestEventInfo(getContext().getApplicationContext(), + getContext().getString(R.string.sync_fail_ticker), + String.format(getContext().getString(R.string.sync_fail_content_unauthorized), getAccount().name), + notification.contentIntent); + Log.e(TAG, "NEEDS TO UPDATE CREDENTIALS"); + } else { + notification.setLatestEventInfo(getContext().getApplicationContext(), + getContext().getString(R.string.sync_fail_ticker), + String.format(getContext().getString(R.string.sync_fail_content), getAccount().name), + notification.contentIntent); + } ((NotificationManager) getContext().getSystemService(Context.NOTIFICATION_SERVICE)).notify(R.string.sync_fail_ticker, notification); } diff --combined src/com/owncloud/android/ui/activity/AccountSelectActivity.java index f6b2a79d,31b59d49..9a04501c --- a/src/com/owncloud/android/ui/activity/AccountSelectActivity.java +++ b/src/com/owncloud/android/ui/activity/AccountSelectActivity.java @@@ -1,9 -1,10 +1,10 @@@ /* 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 as published by - * the Free Software Foundation, either version 3 of the License, or + * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, @@@ -94,10 -95,10 +95,10 @@@ public class AccountSelectActivity exte /// the account set as default changed since this activity was created // trigger synchronization - ContentResolver.cancelSync(null, AccountAuthenticator.AUTH_TOKEN_TYPE); + ContentResolver.cancelSync(null, AccountAuthenticator.AUTHORITY); Bundle bundle = new Bundle(); bundle.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); - ContentResolver.requestSync(AccountUtils.getCurrentOwnCloudAccount(this), AccountAuthenticator.AUTH_TOKEN_TYPE, bundle); + ContentResolver.requestSync(AccountUtils.getCurrentOwnCloudAccount(this), AccountAuthenticator.AUTHORITY, bundle); // restart the main activity Intent i = new Intent(this, FileDisplayActivity.class); @@@ -135,7 -136,7 +136,7 @@@ Intent intent = new Intent( android.provider.Settings.ACTION_ADD_ACCOUNT); intent.putExtra("authorities", - new String[] { AccountAuthenticator.AUTH_TOKEN_TYPE }); + new String[] { AccountAuthenticator.AUTHORITY }); startActivity(intent); return true; } diff --combined src/com/owncloud/android/ui/activity/AuthenticatorActivity.java index 8dce3bc2,d2770406..011abfc7 --- a/src/com/owncloud/android/ui/activity/AuthenticatorActivity.java +++ b/src/com/owncloud/android/ui/activity/AuthenticatorActivity.java @@@ -1,10 -1,10 +1,10 @@@ /* ownCloud Android client application * Copyright (C) 2012 Bartek Przybylski - * Copyright (C) 2012-2013 ownCloud Inc. + * 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 as published by - * the Free Software Foundation, either version 3 of the License, or + * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, @@@ -19,20 -19,21 +19,20 @@@ package com.owncloud.android.ui.activity; -import java.net.MalformedURLException; -import java.net.URL; - import com.owncloud.android.AccountUtils; import com.owncloud.android.authenticator.AccountAuthenticator; -import com.owncloud.android.authenticator.AuthenticationRunnable; -import com.owncloud.android.authenticator.OnAuthenticationResultListener; -import com.owncloud.android.authenticator.OnConnectCheckListener; +import com.owncloud.android.authenticator.oauth2.OAuth2Context; import com.owncloud.android.ui.dialog.SslValidatorDialog; import com.owncloud.android.ui.dialog.SslValidatorDialog.OnSslValidatorListener; +import com.owncloud.android.utils.OwnCloudVersion; import com.owncloud.android.network.OwnCloudClientUtils; -import com.owncloud.android.operations.ConnectionCheckOperation; +import com.owncloud.android.operations.OwnCloudServerCheckOperation; +import com.owncloud.android.operations.ExistenceCheckOperation; +import com.owncloud.android.operations.OAuth2GetAccessToken; import com.owncloud.android.operations.OnRemoteOperationListener; import com.owncloud.android.operations.RemoteOperation; import com.owncloud.android.operations.RemoteOperationResult; +import com.owncloud.android.operations.RemoteOperationResult.ResultCode; import android.accounts.Account; import android.accounts.AccountAuthenticatorActivity; @@@ -51,15 -52,13 +51,15 @@@ import android.preference.PreferenceMan import android.text.InputType; import android.util.Log; import android.view.View; -import android.view.View.OnClickListener; import android.view.View.OnFocusChangeListener; import android.view.Window; -import android.widget.Button; +import android.widget.CheckBox; import android.widget.EditText; +import android.widget.Button; import android.widget.ImageView; import android.widget.TextView; +import android.widget.Toast; + import com.owncloud.android.R; import eu.alefzero.webdav.WebdavClient; @@@ -68,610 -67,305 +68,610 @@@ * This Activity is used to add an ownCloud account to the App * * @author Bartek Przybylski - * + * @author David A. Velasco */ public class AuthenticatorActivity extends AccountAuthenticatorActivity - implements OnAuthenticationResultListener, OnConnectCheckListener, OnRemoteOperationListener, OnSslValidatorListener, - OnFocusChangeListener, OnClickListener { + implements OnRemoteOperationListener, OnSslValidatorListener, OnFocusChangeListener { + + private static final String TAG = AuthenticatorActivity.class.getSimpleName(); + + public static final String EXTRA_ACCOUNT = "ACCOUNT"; + public static final String EXTRA_USER_NAME = "USER_NAME"; + public static final String EXTRA_HOST_NAME = "HOST_NAME"; + public static final String EXTRA_ACTION = "ACTION"; + + private static final String KEY_HOST_URL_TEXT = "HOST_URL_TEXT"; + private static final String KEY_OC_VERSION = "OC_VERSION"; + private static final String KEY_ACCOUNT = "ACCOUNT"; + private static final String KEY_STATUS_TEXT = "STATUS_TEXT"; + private static final String KEY_STATUS_ICON = "STATUS_ICON"; + private static final String KEY_STATUS_CORRECT = "STATUS_CORRECT"; + private static final String KEY_IS_SSL_CONN = "IS_SSL_CONN"; + private static final String KEY_OAUTH2_STATUS_TEXT = "OAUTH2_STATUS_TEXT"; + private static final String KEY_OAUTH2_STATUS_ICON = "OAUTH2_STATUS_ICON"; private static final int DIALOG_LOGIN_PROGRESS = 0; private static final int DIALOG_SSL_VALIDATOR = 1; private static final int DIALOG_CERT_NOT_SAVED = 2; + private static final int DIALOG_OAUTH2_LOGIN_PROGRESS = 3; - private static final String TAG = "AuthActivity"; + public static final byte ACTION_CREATE = 0; + public static final byte ACTION_UPDATE_TOKEN = 1; - private Thread mAuthThread; - private AuthenticationRunnable mAuthRunnable; - //private ConnectionCheckerRunnable mConnChkRunnable = null; - private ConnectionCheckOperation mConnChkRunnable; - private final Handler mHandler = new Handler(); - private String mBaseUrl; - private static final String STATUS_TEXT = "STATUS_TEXT"; - private static final String STATUS_ICON = "STATUS_ICON"; - private static final String STATUS_CORRECT = "STATUS_CORRECT"; - private static final String IS_SSL_CONN = "IS_SSL_CONN"; + private String mHostBaseUrl; + private OwnCloudVersion mDiscoveredVersion; + private int mStatusText, mStatusIcon; private boolean mStatusCorrect, mIsSslConn; + private int mOAuth2StatusText, mOAuth2StatusIcon; + + private final Handler mHandler = new Handler(); + private Thread mOperationThread; + private OwnCloudServerCheckOperation mOcServerChkOperation; + private ExistenceCheckOperation mAuthCheckOperation; private RemoteOperationResult mLastSslUntrustedServerResult; - public static final String PARAM_USERNAME = "param_Username"; - public static final String PARAM_HOSTNAME = "param_Hostname"; - + //private Thread mOAuth2GetCodeThread; + //private OAuth2GetAuthorizationToken mOAuth2GetCodeRunnable; + //private TokenReceiver tokenReceiver; + //private JSONObject mCodeResponseJson; + private Uri mNewCapturedUriFromOAuth2Redirection; + + private AccountManager mAccountMgr; + private boolean mJustCreated; + private byte mAction; + private Account mAccount; + + private ImageView mRefreshButton; + private ImageView mViewPasswordButton; + private EditText mHostUrlInput; + private EditText mUsernameInput; + private EditText mPasswordInput; + private CheckBox mOAuth2Check; + private String mOAuthAccessToken; + private View mOkButton; + private TextView mAuthStatusLayout; + + private TextView mOAuthAuthEndpointText; + private TextView mOAuthTokenEndpointText; + + + /** + * {@inheritDoc} + * + * IMPORTANT ENTRY POINT 1: activity is shown to the user + */ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getWindow().requestFeature(Window.FEATURE_NO_TITLE); + + /// set view and get references to view elements setContentView(R.layout.account_setup); - ImageView iv = (ImageView) findViewById(R.id.refreshButton); - ImageView iv2 = (ImageView) findViewById(R.id.viewPassword); - TextView tv = (TextView) findViewById(R.id.host_URL); - TextView tv2 = (TextView) findViewById(R.id.account_password); - - if (savedInstanceState != null) { - mStatusIcon = savedInstanceState.getInt(STATUS_ICON); - mStatusText = savedInstanceState.getInt(STATUS_TEXT); - mStatusCorrect = savedInstanceState.getBoolean(STATUS_CORRECT); - mIsSslConn = savedInstanceState.getBoolean(IS_SSL_CONN); - setResultIconAndText(mStatusIcon, mStatusText); - findViewById(R.id.buttonOK).setEnabled(mStatusCorrect); - if (!mStatusCorrect) - iv.setVisibility(View.VISIBLE); - else - iv.setVisibility(View.INVISIBLE); + mRefreshButton = (ImageView) findViewById(R.id.refreshButton); + mViewPasswordButton = (ImageView) findViewById(R.id.viewPasswordButton); + mHostUrlInput = (EditText) findViewById(R.id.hostUrlInput); + mUsernameInput = (EditText) findViewById(R.id.account_username); + mPasswordInput = (EditText) findViewById(R.id.account_password); + mOAuthAuthEndpointText = (TextView)findViewById(R.id.oAuthEntryPoint_1); + mOAuthTokenEndpointText = (TextView)findViewById(R.id.oAuthEntryPoint_2); + mOAuth2Check = (CheckBox) findViewById(R.id.oauth_onOff_check); + mOkButton = findViewById(R.id.buttonOK); + mAuthStatusLayout = (TextView) findViewById(R.id.auth_status_text); + - } else { + /// complete label for 'register account' button + Button b = (Button) findViewById(R.id.account_register); + if (b != null) { + b.setText(String.format(getString(R.string.auth_register), getString(R.string.app_name))); + } + + /// bind view elements to listeners + mHostUrlInput.setOnFocusChangeListener(this); + mPasswordInput.setOnFocusChangeListener(this); + + /// initialization + mAccountMgr = AccountManager.get(this); + mNewCapturedUriFromOAuth2Redirection = null; // TODO save? + mAction = getIntent().getByteExtra(EXTRA_ACTION, ACTION_CREATE); + mAccount = null; + + if (savedInstanceState == null) { + /// connection state and info mStatusText = mStatusIcon = 0; mStatusCorrect = false; mIsSslConn = false; - } - iv.setOnClickListener(this); - iv2.setOnClickListener(this); - tv.setOnFocusChangeListener(this); - tv2.setOnFocusChangeListener(this); + + /// retrieve extras from intent + String tokenType = getIntent().getExtras().getString(AccountAuthenticator.KEY_AUTH_TOKEN_TYPE); + boolean oAuthRequired = AccountAuthenticator.AUTH_TOKEN_TYPE_ACCESS_TOKEN.equals(tokenType); + + mAccount = getIntent().getExtras().getParcelable(EXTRA_ACCOUNT); + if (mAccount != null) { + String ocVersion = mAccountMgr.getUserData(mAccount, AccountAuthenticator.KEY_OC_VERSION); + if (ocVersion != null) { + mDiscoveredVersion = new OwnCloudVersion(ocVersion); + } + mHostBaseUrl = mAccountMgr.getUserData(mAccount, AccountAuthenticator.KEY_OC_BASE_URL); + mHostUrlInput.setText(mHostBaseUrl); + String userName = mAccount.name.substring(0, mAccount.name.lastIndexOf('@')); + mUsernameInput.setText(userName); + oAuthRequired = (mAccountMgr.getUserData(mAccount, AccountAuthenticator.KEY_SUPPORTS_OAUTH2) != null); + } + mOAuth2Check.setChecked(oAuthRequired); + changeViewByOAuth2Check(oAuthRequired); + - Button b = (Button) findViewById(R.id.account_register); - if (b != null) { - b.setText(String.format(getString(R.string.auth_register), getString(R.string.app_name))); + } else { + loadSavedInstanceState(savedInstanceState); + } + + if (mAction == ACTION_UPDATE_TOKEN) { + /// lock things that should not change + mHostUrlInput.setEnabled(false); + mUsernameInput.setEnabled(false); + mOAuth2Check.setVisibility(View.GONE); + checkOcServer(); } + + mPasswordInput.setText(""); // clean password to avoid social hacking (disadvantage: password in removed if the device is turned aside) + mJustCreated = true; } + + /** + * Saves relevant state before {@link #onPause()} + * + * Do NOT save {@link #mNewCapturedUriFromOAuth2Redirection}; it keeps a temporal flag, intended to defer the + * processing of the redirection caught in {@link #onNewIntent(Intent)} until {@link #onResume()} + * + * See {@link #loadSavedInstanceState(Bundle)} + */ @Override protected void onSaveInstanceState(Bundle outState) { - outState.putInt(STATUS_ICON, mStatusIcon); - outState.putInt(STATUS_TEXT, mStatusText); - outState.putBoolean(STATUS_CORRECT, mStatusCorrect); super.onSaveInstanceState(outState); + + /// connection state and info + outState.putInt(KEY_STATUS_TEXT, mStatusText); + outState.putInt(KEY_STATUS_ICON, mStatusIcon); + outState.putBoolean(KEY_STATUS_CORRECT, mStatusCorrect); + outState.putBoolean(KEY_IS_SSL_CONN, mIsSslConn); + + /// server data + if (mDiscoveredVersion != null) + outState.putString(KEY_OC_VERSION, mDiscoveredVersion.toString()); + outState.putString(KEY_HOST_URL_TEXT, mHostBaseUrl); + + /// account data, if updating + if (mAccount != null) + outState.putParcelable(KEY_ACCOUNT, mAccount); + + // Saving the state of oAuth2 components. + outState.putInt(KEY_OAUTH2_STATUS_ICON, mOAuth2StatusIcon); + outState.putInt(KEY_OAUTH2_STATUS_TEXT, mOAuth2StatusText); + + /* Leave old OAuth flow + if (codeResponseJson != null){ + outState.putString(KEY_OAUTH2_CODE_RESULT, codeResponseJson.toString()); + } + */ } + + /** + * Loads saved state + * + * See {@link #onSaveInstanceState(Bundle)}. + * + * @param savedInstanceState Saved state, as received in {@link #onCreate(Bundle)}. + */ + private void loadSavedInstanceState(Bundle savedInstanceState) { + /// connection state and info + mStatusCorrect = savedInstanceState.getBoolean(KEY_STATUS_CORRECT); + mIsSslConn = savedInstanceState.getBoolean(KEY_IS_SSL_CONN); + mStatusText = savedInstanceState.getInt(KEY_STATUS_TEXT); + mStatusIcon = savedInstanceState.getInt(KEY_STATUS_ICON); + updateConnStatus(); + + /// UI settings depending upon connection + mOkButton.setEnabled(mStatusCorrect); // TODO really necessary? + if (!mStatusCorrect) + mRefreshButton.setVisibility(View.VISIBLE); // seems that setting visibility is necessary + else + mRefreshButton.setVisibility(View.INVISIBLE); + + /// server data + String ocVersion = savedInstanceState.getString(KEY_OC_VERSION); + if (ocVersion != null) + mDiscoveredVersion = new OwnCloudVersion(ocVersion); + mHostBaseUrl = savedInstanceState.getString(KEY_HOST_URL_TEXT); + + // account data, if updating + mAccount = savedInstanceState.getParcelable(KEY_ACCOUNT); + + // state of oAuth2 components + mOAuth2StatusIcon = savedInstanceState.getInt(KEY_OAUTH2_STATUS_ICON); + mOAuth2StatusText = savedInstanceState.getInt(KEY_OAUTH2_STATUS_TEXT); + + /* Leave old OAuth flow + // We store a JSon object with all the data returned from oAuth2 server when we get user_code. + // Is better than store variable by variable. We use String object to serialize from/to it. + try { + if (savedInstanceState.containsKey(KEY_OAUTH2_CODE_RESULT)) { + codeResponseJson = new JSONObject(savedInstanceState.getString(KEY_OAUTH2_CODE_RESULT)); + } + } catch (JSONException e) { + Log.e(TAG, "onCreate->JSONException: " + e.toString()); + }*/ + // END of getting the state of oAuth2 components. + + } + + + /** + * The redirection triggered by the OAuth authentication server as response to the GET AUTHORIZATION request + * is caught here. + * + * To make this possible, this activity needs to be qualified with android:launchMode = "singleTask" in the + * AndroidManifest.xml file. + */ @Override - protected Dialog onCreateDialog(int id) { - Dialog dialog = null; - switch (id) { - case DIALOG_LOGIN_PROGRESS: { - ProgressDialog working_dialog = new ProgressDialog(this); - working_dialog.setMessage(getResources().getString( - R.string.auth_trying_to_login)); - working_dialog.setIndeterminate(true); - working_dialog.setCancelable(true); - working_dialog - .setOnCancelListener(new DialogInterface.OnCancelListener() { - @Override - public void onCancel(DialogInterface dialog) { - Log.i(TAG, "Login canceled"); - if (mAuthThread != null) { - mAuthThread.interrupt(); - finish(); - } - } - }); - dialog = working_dialog; - break; - } - case DIALOG_SSL_VALIDATOR: { - dialog = SslValidatorDialog.newInstance(this, mLastSslUntrustedServerResult, this); - break; - } - case DIALOG_CERT_NOT_SAVED: { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setMessage(getResources().getString(R.string.ssl_validator_not_saved)); - builder.setCancelable(false); - builder.setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - dialog.dismiss(); - }; - }); - dialog = builder.create(); - break; + protected void onNewIntent (Intent intent) { + Log.d(TAG, "onNewIntent()"); + Uri data = intent.getData(); + if (data != null && data.toString().startsWith(OAuth2Context.MY_REDIRECT_URI)) { + mNewCapturedUriFromOAuth2Redirection = data; } - default: - Log.e(TAG, "Incorrect dialog called with id = " + id); - } - return dialog; } + + /** + * The redirection triggered by the OAuth authentication server as response to the GET AUTHORIZATION, and + * deferred in {@link #onNewIntent(Intent)}, is processed here. + */ @Override - protected void onPrepareDialog(int id, Dialog dialog, Bundle args) { - switch (id) { - case DIALOG_LOGIN_PROGRESS: - case DIALOG_CERT_NOT_SAVED: - break; - case DIALOG_SSL_VALIDATOR: { - ((SslValidatorDialog)dialog).updateResult(mLastSslUntrustedServerResult); - break; + protected void onResume() { + super.onResume(); + // the state of mOAuth2Check is automatically recovered between configuration changes, but not before onCreate() finishes; so keep the next lines here + changeViewByOAuth2Check(mOAuth2Check.isChecked()); + if (mAction == ACTION_UPDATE_TOKEN && mJustCreated) { + if (mOAuth2Check.isChecked()) + Toast.makeText(this, R.string.auth_expired_oauth_token_toast, Toast.LENGTH_LONG).show(); + else + Toast.makeText(this, R.string.auth_expired_basic_auth_toast, Toast.LENGTH_LONG).show(); } - default: - Log.e(TAG, "Incorrect dialog called with id = " + id); + + + /* LEAVE OLD OAUTH FLOW ; + // (old oauth code) Registering token receiver. We must listening to the service that is pooling to the oAuth server for a token. + if (tokenReceiver == null) { + IntentFilter tokenFilter = new IntentFilter(OAuth2GetTokenService.TOKEN_RECEIVED_MESSAGE); + tokenReceiver = new TokenReceiver(); + this.registerReceiver(tokenReceiver,tokenFilter); + } */ + // (new oauth code) + if (mNewCapturedUriFromOAuth2Redirection != null) { + getOAuth2AccessTokenFromCapturedRedirection(); } + + mJustCreated = false; } + + + @Override protected void onDestroy() { + super.onDestroy(); - public void onAuthenticationResult(boolean success, String message) { - if (success) { - TextView username_text = (TextView) findViewById(R.id.account_username), password_text = (TextView) findViewById(R.id.account_password); + /* LEAVE OLD OAUTH FLOW + // We must stop the service thats it's pooling to oAuth2 server for a token. + Intent tokenService = new Intent(this, OAuth2GetTokenService.class); + stopService(tokenService); + + // We stop listening the result of the pooling service. + if (tokenReceiver != null) { + unregisterReceiver(tokenReceiver); + tokenReceiver = null; + }*/ - URL url; - try { - url = new URL(message); - } catch (MalformedURLException e) { - // should never happen - Log.e(getClass().getName(), "Malformed URL: " + message); - return; - } + } + + + /** + * Parses the redirection with the response to the GET AUTHORIZATION request to the + * oAuth server and requests for the access token (GET ACCESS TOKEN) + */ + private void getOAuth2AccessTokenFromCapturedRedirection() { + /// Parse data from OAuth redirection + String queryParameters = mNewCapturedUriFromOAuth2Redirection.getQuery(); + mNewCapturedUriFromOAuth2Redirection = null; + + /// Showing the dialog with instructions for the user. + showDialog(DIALOG_OAUTH2_LOGIN_PROGRESS); - String username = username_text.getText().toString().trim(); - String accountName = username + "@" + url.getHost(); - if (url.getPort() >= 0) { - accountName += ":" + url.getPort(); - } - Account account = new Account(accountName, - AccountAuthenticator.ACCOUNT_TYPE); - AccountManager accManager = AccountManager.get(this); - accManager.addAccountExplicitly(account, password_text.getText() - .toString(), null); - - // Add this account as default in the preferences, if there is none - // already - Account defaultAccount = AccountUtils - .getCurrentOwnCloudAccount(this); - if (defaultAccount == null) { - SharedPreferences.Editor editor = PreferenceManager - .getDefaultSharedPreferences(this).edit(); - editor.putString("select_oc_account", accountName); - editor.commit(); - } + /// GET ACCESS TOKEN to the oAuth server + RemoteOperation operation = new OAuth2GetAccessToken(queryParameters); + WebdavClient client = OwnCloudClientUtils.createOwnCloudClient(Uri.parse(getString(R.string.oauth_url_endpoint_access)), getApplicationContext()); + operation.execute(client, this, mHandler); + } + - final Intent intent = new Intent(); - intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, - AccountAuthenticator.ACCOUNT_TYPE); - intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, account.name); - intent.putExtra(AccountManager.KEY_AUTHTOKEN, - AccountAuthenticator.ACCOUNT_TYPE); - intent.putExtra(AccountManager.KEY_USERDATA, username); - - accManager.setUserData(account, - AccountAuthenticator.KEY_OC_VERSION, mConnChkRunnable - .getDiscoveredVersion().toString()); + + /** + * Handles the change of focus on the text inputs for the server URL and the password + */ + public void onFocusChange(View view, boolean hasFocus) { + if (view.getId() == R.id.hostUrlInput) { + onUrlInputFocusChanged((TextView) view, hasFocus); - accManager.setUserData(account, - AccountAuthenticator.KEY_OC_BASE_URL, mBaseUrl); - - setAccountAuthenticatorResult(intent.getExtras()); - setResult(RESULT_OK, intent); - Bundle bundle = new Bundle(); - bundle.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); - //getContentResolver().startSync(ProviderTableMeta.CONTENT_URI, - // bundle); - ContentResolver.requestSync(account, "org.owncloud", bundle); - - /* - * if - * (mConnChkRunnable.getDiscoveredVersion().compareTo(OwnCloudVersion - * .owncloud_v2) >= 0) { Intent i = new Intent(this, - * ExtensionsAvailableActivity.class); startActivity(i); } - */ + } else if (view.getId() == R.id.account_password) { + onPasswordFocusChanged((TextView) view, hasFocus); + } + } + - finish(); + /** + * Handles changes in focus on the text input for the server URL. + * + * IMPORTANT ENTRY POINT 2: When (!hasFocus), user wrote the server URL and changed to + * other field. The operation to check the existence of the server in the entered URL is + * started. + * + * When hasFocus: user 'comes back' to write again the server URL. + * + * @param hostInput TextView with the URL input field receiving the change of focus. + * @param hasFocus 'True' if focus is received, 'false' if is lost + */ + private void onUrlInputFocusChanged(TextView hostInput, boolean hasFocus) { + if (!hasFocus) { + checkOcServer(); + } else { - try { - dismissDialog(DIALOG_LOGIN_PROGRESS); - } catch (IllegalArgumentException e) { - // NOTHING TO DO ; can't find out what situation that leads to the exception in this code, but user logs signal that it happens - } - TextView tv = (TextView) findViewById(R.id.account_username); - tv.setError(message + " "); // the extra spaces are a workaround for an ugly bug: - // 1. insert wrong credentials and connect - // 2. put the focus on the user name field with using hardware controls (don't touch the screen); the error is shown UNDER the field - // 3. touch the user name field; the software keyboard appears; the error popup is moved OVER the field and SHRINKED in width, losing the last word - // Seen, at least, in Android 2.x devices + // avoids that the 'connect' button can be clicked if the test was previously passed + mOkButton.setEnabled(false); + } + } + + + private void checkOcServer() { + String uri = mHostUrlInput.getText().toString().trim(); + if (uri.length() != 0) { + mStatusText = R.string.auth_testing_connection; + mStatusIcon = R.drawable.progress_small; + updateConnStatus(); + /** TODO cancel previous connection check if the user tries to ammend a wrong URL + if(mConnChkOperation != null) { + mConnChkOperation.cancel(); + } */ + mOcServerChkOperation = new OwnCloudServerCheckOperation(uri, this); + WebdavClient client = OwnCloudClientUtils.createOwnCloudClient(Uri.parse(uri), this); + mHostBaseUrl = ""; + mDiscoveredVersion = null; + mOperationThread = mOcServerChkOperation.execute(client, this, mHandler); + } else { + mRefreshButton.setVisibility(View.INVISIBLE); + mStatusText = 0; + mStatusIcon = 0; + updateConnStatus(); } } + + + /** + * Handles changes in focus on the text input for the password (basic authorization). + * + * When (hasFocus), the button to toggle password visibility is shown. + * + * When (!hasFocus), the button is made invisible and the password is hidden. + * + * @param passwordInput TextView with the password input field receiving the change of focus. + * @param hasFocus 'True' if focus is received, 'false' if is lost + */ + private void onPasswordFocusChanged(TextView passwordInput, boolean hasFocus) { + if (hasFocus) { + mViewPasswordButton.setVisibility(View.VISIBLE); + } else { + int input_type = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD; + passwordInput.setInputType(input_type); + mViewPasswordButton.setVisibility(View.INVISIBLE); + } + } + + + + /** + * Cancels the authenticator activity + * + * IMPORTANT ENTRY POINT 3: Never underestimate the importance of cancellation + * + * This method is bound in the layout/acceoun_setup.xml resource file. + * + * @param view Cancel button + */ public void onCancelClick(View view) { - setResult(RESULT_CANCELED); + setResult(RESULT_CANCELED); // TODO review how is this related to AccountAuthenticator finish(); } + + + /** + * Checks the credentials of the user in the root of the ownCloud server + * before creating a new local account. + * + * For basic authorization, a check of existence of the root folder is + * performed. + * + * For OAuth, starts the flow to get an access token; the credentials test + * is postponed until it is available. + * + * IMPORTANT ENTRY POINT 4 + * + * @param view OK button + */ public void onOkClick(View view) { - String prefix = ""; - String url = ((TextView) findViewById(R.id.host_URL)).getText() - .toString().trim(); - if (mIsSslConn) { - prefix = "https://"; - } else { - prefix = "http://"; + // this check should be unnecessary + if (mDiscoveredVersion == null || !mDiscoveredVersion.isVersionValid() || mHostBaseUrl == null || mHostBaseUrl.length() == 0) { + mStatusIcon = R.drawable.common_error; + mStatusText = R.string.auth_wtf_reenter_URL; + updateConnStatus(); + mOkButton.setEnabled(false); + Log.wtf(TAG, "The user was allowed to click 'connect' to an unchecked server!!"); + return; } - if (url.toLowerCase().startsWith("http://") - || url.toLowerCase().startsWith("https://")) { - prefix = ""; + + if (mOAuth2Check.isChecked()) { + startOauthorization(); + + } else { + checkBasicAuthorization(); } - continueConnection(prefix); } - public void onRegisterClick(View view) { - Intent register = new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.url_account_register))); - setResult(RESULT_CANCELED); - startActivity(register); + + /** + * Tests the credentials entered by the user performing a check of existence on + * the root folder of the ownCloud server. + */ + private void checkBasicAuthorization() { + /// get the path to the root folder through WebDAV from the version server + String webdav_path = AccountUtils.getWebdavPath(mDiscoveredVersion, false); + + /// get basic credentials entered by user + String username = mUsernameInput.getText().toString(); + String password = mPasswordInput.getText().toString(); + + /// be gentle with the user + showDialog(DIALOG_LOGIN_PROGRESS); + + /// test credentials accessing the root folder + mAuthCheckOperation = new ExistenceCheckOperation("", this, false); + WebdavClient client = OwnCloudClientUtils.createOwnCloudClient(Uri.parse(mHostBaseUrl + webdav_path), this); + client.setBasicCredentials(username, password); + mOperationThread = mAuthCheckOperation.execute(client, this, mHandler); } - private void continueConnection(String prefix) { - String url = ((TextView) findViewById(R.id.host_URL)).getText() - .toString().trim(); - String username = ((TextView) findViewById(R.id.account_username)) - .getText().toString(); - String password = ((TextView) findViewById(R.id.account_password)) - .getText().toString(); - if (url.endsWith("/")) - url = url.substring(0, url.length() - 1); - - URL uri = null; - String webdav_path = AccountUtils.getWebdavPath(mConnChkRunnable - .getDiscoveredVersion()); - - if (webdav_path == null) { - onAuthenticationResult(false, getString(R.string.auth_bad_oc_version_title)); - return; - } + + /** + * Starts the OAuth 'grant type' flow to get an access token, with + * a GET AUTHORIZATION request to the BUILT-IN authorization server. + */ + private void startOauthorization() { + // be gentle with the user + mStatusIcon = R.drawable.progress_small; + mStatusText = R.string.oauth_login_connection; + updateAuthStatus(); - try { - mBaseUrl = prefix + url; - String url_str = prefix + url + webdav_path; - uri = new URL(url_str); - } catch (MalformedURLException e) { - // should never happen - onAuthenticationResult(false, getString(R.string.auth_incorrect_address_title)); - return; + // GET AUTHORIZATION request + /* + mOAuth2GetCodeRunnable = new OAuth2GetAuthorizationToken(, this); + mOAuth2GetCodeRunnable.setListener(this, mHandler); + mOAuth2GetCodeThread = new Thread(mOAuth2GetCodeRunnable); + mOAuth2GetCodeThread.start(); + */ + + //if (mGrantType.equals(OAuth2Context.OAUTH2_AUTH_CODE_GRANT_TYPE)) { + Uri uri = Uri.parse(getString(R.string.oauth_url_endpoint_auth)); + Uri.Builder uriBuilder = uri.buildUpon(); + uriBuilder.appendQueryParameter(OAuth2Context.CODE_RESPONSE_TYPE, OAuth2Context.OAUTH2_CODE_RESPONSE_TYPE); + uriBuilder.appendQueryParameter(OAuth2Context.CODE_REDIRECT_URI, OAuth2Context.MY_REDIRECT_URI); + uriBuilder.appendQueryParameter(OAuth2Context.CODE_CLIENT_ID, OAuth2Context.OAUTH2_F_CLIENT_ID); + uriBuilder.appendQueryParameter(OAuth2Context.CODE_SCOPE, OAuth2Context.OAUTH2_F_SCOPE); + //uriBuilder.appendQueryParameter(OAuth2Context.CODE_STATE, whateverwewant); + uri = uriBuilder.build(); + Log.d(TAG, "Starting browser to view " + uri.toString()); + Intent i = new Intent(Intent.ACTION_VIEW, uri); + startActivity(i); + //} + } + + + /** + * Callback method invoked when a RemoteOperation executed by this Activity finishes. + * + * Dispatches the operation flow to the right method. + */ + @Override + public void onRemoteOperationFinish(RemoteOperation operation, RemoteOperationResult result) { + + if (operation instanceof OwnCloudServerCheckOperation) { + onOcServerCheckFinish((OwnCloudServerCheckOperation) operation, result); + + } else if (operation instanceof OAuth2GetAccessToken) { + onGetOAuthAccessTokenFinish((OAuth2GetAccessToken)operation, result); + + } else if (operation instanceof ExistenceCheckOperation) { + onAuthorizationCheckFinish((ExistenceCheckOperation)operation, result); + } + } + - showDialog(DIALOG_LOGIN_PROGRESS); - mAuthRunnable = new AuthenticationRunnable(uri, username, password, this); - mAuthRunnable.setOnAuthenticationResultListener(this, mHandler); - mAuthThread = new Thread(mAuthRunnable); - mAuthThread.start(); + /** + * Processes the result of the server check performed when the user finishes the enter of the + * server URL. + * + * @param operation Server check performed. + * @param result Result of the check. + */ + private void onOcServerCheckFinish(OwnCloudServerCheckOperation operation, RemoteOperationResult result) { + /// update status icon and text + updateStatusIconAndText(result); + updateConnStatus(); + + /// save result state + mStatusCorrect = result.isSuccess(); + mIsSslConn = (result.getCode() == ResultCode.OK_SSL); + + /// very special case (TODO: move to a common place for all the remote operations) + if (result.getCode() == ResultCode.SSL_RECOVERABLE_PEER_UNVERIFIED) { + mLastSslUntrustedServerResult = result; + showDialog(DIALOG_SSL_VALIDATOR); + } + + /// update the visibility of the 'retry connection' button + if (!mStatusCorrect) + mRefreshButton.setVisibility(View.VISIBLE); + else + mRefreshButton.setVisibility(View.INVISIBLE); + + /// retrieve discovered version and normalize server URL + mDiscoveredVersion = operation.getDiscoveredVersion(); + mHostBaseUrl = mHostUrlInput.getText().toString().trim(); + if (!mHostBaseUrl.toLowerCase().startsWith("http://") && + !mHostBaseUrl.toLowerCase().startsWith("https://")) { + + if (mIsSslConn) { + mHostBaseUrl = "https://" + mHostBaseUrl; + } else { + mHostBaseUrl = "http://" + mHostBaseUrl; + } + + } + if (mHostBaseUrl.endsWith("/")) + mHostBaseUrl = mHostBaseUrl.substring(0, mHostBaseUrl.length() - 1); + + /// allow or not the user try to access the server + mOkButton.setEnabled(mStatusCorrect); } - @Override - public void onConnectionCheckResult(ResultType type) { + + /** + * Chooses the right icon and text to show to the user for the received operation result. + * + * @param result Result of a remote operation performed in this activity + */ + private void updateStatusIconAndText(RemoteOperationResult result) { mStatusText = mStatusIcon = 0; - mStatusCorrect = false; - String t_url = ((TextView) findViewById(R.id.host_URL)).getText() - .toString().trim().toLowerCase(); - switch (type) { + switch (result.getCode()) { case OK_SSL: - mIsSslConn = true; mStatusIcon = android.R.drawable.ic_secure; mStatusText = R.string.auth_secure_connection; - mStatusCorrect = true; break; + case OK_NO_SSL: - mIsSslConn = false; - mStatusCorrect = true; - if (t_url.startsWith("http://") ) { + case OK: + if (mHostUrlInput.getText().toString().trim().toLowerCase().startsWith("http://") ) { mStatusText = R.string.auth_connection_established; mStatusIcon = R.drawable.ic_ok; } else { @@@ -679,12 -373,6 +679,12 @@@ mStatusIcon = android.R.drawable.ic_partial_secure; } break; + + case SSL_RECOVERABLE_PEER_UNVERIFIED: + mStatusIcon = R.drawable.common_error; + mStatusText = R.string.auth_ssl_unverified_server_title; + break; + case BAD_OC_VERSION: mStatusIcon = R.drawable.common_error; mStatusText = R.string.auth_bad_oc_version_title; @@@ -701,15 -389,13 +701,15 @@@ mStatusIcon = R.drawable.common_error; mStatusText = R.string.auth_incorrect_address_title; break; - case SSL_UNVERIFIED_SERVER: + + case SSL_ERROR: mStatusIcon = R.drawable.common_error; - mStatusText = R.string.auth_ssl_unverified_server_title; + mStatusText = R.string.auth_ssl_general_error_title; break; - case SSL_INIT_ERROR: + + case UNAUTHORIZED: mStatusIcon = R.drawable.common_error; - mStatusText = R.string.auth_ssl_general_error_title; + mStatusText = R.string.auth_unauthorized; break; case HOST_NOT_AVAILABLE: mStatusIcon = R.drawable.common_error; @@@ -723,533 -409,201 +723,533 @@@ mStatusIcon = R.drawable.common_error; mStatusText = R.string.auth_not_configured_title; break; + case FILE_NOT_FOUND: + mStatusIcon = R.drawable.common_error; + mStatusText = R.string.auth_incorrect_path_title; + break; + case OAUTH2_ERROR: + mStatusIcon = R.drawable.common_error; + mStatusText = R.string.auth_oauth_error; + break; + case OAUTH2_ERROR_ACCESS_DENIED: + mStatusIcon = R.drawable.common_error; + mStatusText = R.string.auth_oauth_error_access_denied; + break; + case UNHANDLED_HTTP_CODE: case UNKNOWN_ERROR: mStatusIcon = R.drawable.common_error; mStatusText = R.string.auth_unknown_error_title; break; - case FILE_NOT_FOUND: - mStatusIcon = R.drawable.common_error; - mStatusText = R.string.auth_incorrect_path_title; + + default: break; + } + } + + + /** + * Processes the result of the request for and access token send + * to an OAuth authorization server. + * + * @param operation Operation performed requesting the access token. + * @param result Result of the operation. + */ + private void onGetOAuthAccessTokenFinish(OAuth2GetAccessToken operation, RemoteOperationResult result) { + try { + dismissDialog(DIALOG_OAUTH2_LOGIN_PROGRESS); + } catch (IllegalArgumentException e) { + // NOTHING TO DO ; can't find out what situation that leads to the exception in this code, but user logs signal that it happens + } + + String webdav_path = AccountUtils.getWebdavPath(mDiscoveredVersion, true); + if (result.isSuccess() && webdav_path != null) { + /// be gentle with the user + showDialog(DIALOG_LOGIN_PROGRESS); + + /// time to test the retrieved access token on the ownCloud server + mOAuthAccessToken = ((OAuth2GetAccessToken)operation).getResultTokenMap().get(OAuth2Context.KEY_ACCESS_TOKEN); + Log.d(TAG, "Got ACCESS TOKEN: " + mOAuthAccessToken); + mAuthCheckOperation = new ExistenceCheckOperation("", this, false); + WebdavClient client = OwnCloudClientUtils.createOwnCloudClient(Uri.parse(mHostBaseUrl + webdav_path), this); + client.setBearerCredentials(mOAuthAccessToken); + mAuthCheckOperation.execute(client, this, mHandler); + + } else { + updateStatusIconAndText(result); + updateAuthStatus(); + Log.d(TAG, "Access failed: " + result.getLogMessage()); + } + } + + + /** + * Processes the result of the access check performed to try the user credentials. + * + * Creates a new account through the AccountManager. + * + * @param operation Access check performed. + * @param result Result of the operation. + */ + private void onAuthorizationCheckFinish(ExistenceCheckOperation operation, RemoteOperationResult result) { + try { + dismissDialog(DIALOG_LOGIN_PROGRESS); + } catch (IllegalArgumentException e) { + // NOTHING TO DO ; can't find out what situation that leads to the exception in this code, but user logs signal that it happens + } + + if (result.isSuccess()) { + Log.d(TAG, "Successful access - time to save the account"); + + if (mAction == ACTION_CREATE) { + createAccount(); + + } else { + updateToken(); + } + + finish(); + + } else { + updateStatusIconAndText(result); + updateAuthStatus(); + Log.d(TAG, "Access failed: " + result.getLogMessage()); + } + } + + + /** + * Sets the proper response to get that the Account Authenticator that started this activity saves + * a new authorization token for mAccount. + */ + private void updateToken() { + Bundle response = new Bundle(); + response.putString(AccountManager.KEY_ACCOUNT_NAME, mAccount.name); + response.putString(AccountManager.KEY_ACCOUNT_TYPE, mAccount.type); + boolean isOAuth = mOAuth2Check.isChecked(); + if (isOAuth) { + response.putString(AccountManager.KEY_AUTHTOKEN, mOAuthAccessToken); + // the next line is necessary; by now, notifications are calling directly to the AuthenticatorActivity to update, without AccountManager intervention + mAccountMgr.setAuthToken(mAccount, AccountAuthenticator.AUTH_TOKEN_TYPE_ACCESS_TOKEN, mOAuthAccessToken); + } else { + response.putString(AccountManager.KEY_AUTHTOKEN, mPasswordInput.getText().toString()); + mAccountMgr.setPassword(mAccount, mPasswordInput.getText().toString()); + } + setAccountAuthenticatorResult(response); + } + + + /** + * Creates a new account through the Account Authenticator that started this activity. + * + * This makes the account permanent. + * + * TODO Decide how to name the OAuth accounts + * TODO Minimize the direct interactions with the account manager; seems that not all the operations + * in the current code are really necessary, provided that right extras are returned to the Account + * Authenticator through setAccountAuthenticatorResult + */ + private void createAccount() { + /// create and save new ownCloud account + boolean isOAuth = mOAuth2Check.isChecked(); + + Uri uri = Uri.parse(mHostBaseUrl); + String username = mUsernameInput.getText().toString().trim(); + if (isOAuth) { + username = "OAuth_user" + (new java.util.Random(System.currentTimeMillis())).nextLong(); // TODO change this to something readable + } + String accountName = username + "@" + uri.getHost(); + if (uri.getPort() >= 0) { + accountName += ":" + uri.getPort(); + } + mAccount = new Account(accountName, AccountAuthenticator.ACCOUNT_TYPE); + if (isOAuth) { + mAccountMgr.addAccountExplicitly(mAccount, "", null); // with our implementation, the password is never input in the app + } else { + mAccountMgr.addAccountExplicitly(mAccount, mPasswordInput.getText().toString(), null); + } + + /// add the new account as default in preferences, if there is none already + Account defaultAccount = AccountUtils.getCurrentOwnCloudAccount(this); + if (defaultAccount == null) { + SharedPreferences.Editor editor = PreferenceManager + .getDefaultSharedPreferences(this).edit(); + editor.putString("select_oc_account", accountName); + editor.commit(); + } + + /// prepare result to return to the Authenticator + // TODO check again what the Authenticator makes with it; probably has the same effect as addAccountExplicitly, but it's not well done + final Intent intent = new Intent(); + intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, AccountAuthenticator.ACCOUNT_TYPE); + intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, mAccount.name); + if (!isOAuth) + intent.putExtra(AccountManager.KEY_AUTHTOKEN, AccountAuthenticator.ACCOUNT_TYPE); // TODO check this; not sure it's right; maybe + intent.putExtra(AccountManager.KEY_USERDATA, username); + if (isOAuth) { + mAccountMgr.setAuthToken(mAccount, AccountAuthenticator.AUTH_TOKEN_TYPE_ACCESS_TOKEN, mOAuthAccessToken); + } + /// add user data to the new account; TODO probably can be done in the last parameter addAccountExplicitly, or in KEY_USERDATA + mAccountMgr.setUserData(mAccount, AccountAuthenticator.KEY_OC_VERSION, mDiscoveredVersion.toString()); + mAccountMgr.setUserData(mAccount, AccountAuthenticator.KEY_OC_BASE_URL, mHostBaseUrl); + if (isOAuth) + mAccountMgr.setUserData(mAccount, AccountAuthenticator.KEY_SUPPORTS_OAUTH2, "TRUE"); // TODO this flag should be unnecessary + + setAccountAuthenticatorResult(intent.getExtras()); + setResult(RESULT_OK, intent); + + /// immediately request for the synchronization of the new account + Bundle bundle = new Bundle(); + bundle.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); + ContentResolver.requestSync(mAccount, AccountAuthenticator.AUTHORITY, bundle); + } + + + /** + * {@inheritDoc} + * + * Necessary to update the contents of the SSL Dialog + * + * TODO move to some common place for all possible untrusted SSL failures + */ + @Override + protected void onPrepareDialog(int id, Dialog dialog, Bundle args) { + switch (id) { + case DIALOG_LOGIN_PROGRESS: + case DIALOG_CERT_NOT_SAVED: + case DIALOG_OAUTH2_LOGIN_PROGRESS: + break; + case DIALOG_SSL_VALIDATOR: { + ((SslValidatorDialog)dialog).updateResult(mLastSslUntrustedServerResult); + break; + } default: - Log.e(TAG, "Incorrect connection checker result type: " + type); + Log.e(TAG, "Incorrect dialog called with id = " + id); } - setResultIconAndText(mStatusIcon, mStatusText); - if (!mStatusCorrect) - findViewById(R.id.refreshButton).setVisibility(View.VISIBLE); - else - findViewById(R.id.refreshButton).setVisibility(View.INVISIBLE); - findViewById(R.id.buttonOK).setEnabled(mStatusCorrect); } + + /** + * {@inheritDoc} + */ @Override - public void onFocusChange(View view, boolean hasFocus) { - if (view.getId() == R.id.host_URL) { - if (!hasFocus) { - TextView tv = ((TextView) findViewById(R.id.host_URL)); - String uri = tv.getText().toString().trim(); - if (uri.length() != 0) { - setResultIconAndText(R.drawable.progress_small, - R.string.auth_testing_connection); - //mConnChkRunnable = new ConnectionCheckerRunnable(uri, this); - mConnChkRunnable = new ConnectionCheckOperation(uri, this); - //mConnChkRunnable.setListener(this, mHandler); - //mAuthThread = new Thread(mConnChkRunnable); - //mAuthThread.start(); - WebdavClient client = OwnCloudClientUtils.createOwnCloudClient(Uri.parse(uri), this); - mAuthThread = mConnChkRunnable.execute(client, this, mHandler); - } else { - findViewById(R.id.refreshButton).setVisibility( - View.INVISIBLE); - setResultIconAndText(0, 0); + protected Dialog onCreateDialog(int id) { + Dialog dialog = null; + switch (id) { + case DIALOG_LOGIN_PROGRESS: { + /// simple progress dialog + ProgressDialog working_dialog = new ProgressDialog(this); + working_dialog.setMessage(getResources().getString(R.string.auth_trying_to_login)); + working_dialog.setIndeterminate(true); + working_dialog.setCancelable(true); + working_dialog + .setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + /// TODO study if this is enough + Log.i(TAG, "Login canceled"); + if (mOperationThread != null) { + mOperationThread.interrupt(); + finish(); + } + } + }); + dialog = working_dialog; + break; + } + case DIALOG_OAUTH2_LOGIN_PROGRESS: { + /// oAuth2 dialog. We show here to the user the URL and user_code that the user must validate in a web browser. - OLD! + // TODO optimize this dialog + ProgressDialog working_dialog = new ProgressDialog(this); + /* Leave the old OAuth flow + try { + if (mCodeResponseJson != null && mCodeResponseJson.has(OAuth2GetCodeRunnable.CODE_VERIFICATION_URL)) { + working_dialog.setMessage(String.format(getString(R.string.oauth_code_validation_message), + mCodeResponseJson.getString(OAuth2GetCodeRunnable.CODE_VERIFICATION_URL), + mCodeResponseJson.getString(OAuth2GetCodeRunnable.CODE_USER_CODE))); + } else {*/ + working_dialog.setMessage(String.format("Getting authorization")); + /*} + } catch (JSONException e) { + Log.e(TAG, "onCreateDialog->JSONException: " + e.toString()); + }*/ + working_dialog.setIndeterminate(true); + working_dialog.setCancelable(true); + working_dialog + .setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + Log.i(TAG, "Login canceled"); + /*if (mOAuth2GetCodeThread != null) { + mOAuth2GetCodeThread.interrupt(); + finish(); + } */ + /*if (tokenReceiver != null) { + unregisterReceiver(tokenReceiver); + tokenReceiver = null; + finish(); + }*/ + finish(); } - } else { - // avoids that the 'connect' button can be clicked if the test was previously passed - findViewById(R.id.buttonOK).setEnabled(false); - } - } else if (view.getId() == R.id.account_password) { - ImageView iv = (ImageView) findViewById(R.id.viewPassword); - if (hasFocus) { - iv.setVisibility(View.VISIBLE); - } else { - TextView v = (TextView) findViewById(R.id.account_password); - int input_type = InputType.TYPE_CLASS_TEXT - | InputType.TYPE_TEXT_VARIATION_PASSWORD; - v.setInputType(input_type); - iv.setVisibility(View.INVISIBLE); - } + }); + dialog = working_dialog; + break; } + case DIALOG_SSL_VALIDATOR: { + /// TODO start to use new dialog interface, at least for this (it is a FragmentDialog already) + dialog = SslValidatorDialog.newInstance(this, mLastSslUntrustedServerResult, this); + break; + } + case DIALOG_CERT_NOT_SAVED: { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setMessage(getResources().getString(R.string.ssl_validator_not_saved)); + builder.setCancelable(false); + builder.setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + }; + }); + dialog = builder.create(); + break; + } + default: + Log.e(TAG, "Incorrect dialog called with id = " + id); + } + return dialog; + } + + + /** + * Starts and activity to open the 'new account' page in the ownCloud web site + * + * @param view 'Account register' button + */ + public void onRegisterClick(View view) { + Intent register = new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.url_account_register))); + setResult(RESULT_CANCELED); + startActivity(register); } - private void setResultIconAndText(int drawable_id, int text_id) { + + /** + * Updates the content and visibility state of the icon and text associated + * to the last check on the ownCloud server. + */ + private void updateConnStatus() { ImageView iv = (ImageView) findViewById(R.id.action_indicator); TextView tv = (TextView) findViewById(R.id.status_text); - if (drawable_id == 0 && text_id == 0) { + if (mStatusIcon == 0 && mStatusText == 0) { iv.setVisibility(View.INVISIBLE); tv.setVisibility(View.INVISIBLE); } else { - iv.setImageResource(drawable_id); - tv.setText(text_id); + iv.setImageResource(mStatusIcon); + tv.setText(mStatusText); iv.setVisibility(View.VISIBLE); tv.setVisibility(View.VISIBLE); } } + + + /** + * Updates the content and visibility state of the icon and text associated + * to the interactions with the OAuth authorization server. + */ + private void updateAuthStatus() { + /*ImageView iv = (ImageView) findViewById(R.id.auth_status_icon); + TextView tv = (TextView) findViewById(R.id.auth_status_text);*/ + + if (mStatusIcon == 0 && mStatusText == 0) { + mAuthStatusLayout.setVisibility(View.INVISIBLE); + /*iv.setVisibility(View.INVISIBLE); + tv.setVisibility(View.INVISIBLE);*/ + } else { + mAuthStatusLayout.setText(mStatusText); + mAuthStatusLayout.setCompoundDrawablesWithIntrinsicBounds(mStatusIcon, 0, 0, 0); + /*iv.setImageResource(mStatusIcon); + tv.setText(mStatusText); + /*iv.setVisibility(View.VISIBLE); + tv.setVisibility(View.VISIBLE);^*/ + mAuthStatusLayout.setVisibility(View.VISIBLE); + } + } + + /** + * Called when the refresh button in the input field for ownCloud host is clicked. + * + * Performs a new check on the URL in the input field. + * + * @param view Refresh 'button' + */ + public void onRefreshClick(View view) { + onFocusChange(mRefreshButton, false); + } + + + /** + * Called when the eye icon in the password field is clicked. + * + * Toggles the visibility of the password in the field. + * + * @param view 'View password' 'button' + */ + public void onViewPasswordClick(View view) { + int selectionStart = mPasswordInput.getSelectionStart(); + int selectionEnd = mPasswordInput.getSelectionEnd(); + int input_type = mPasswordInput.getInputType(); + if ((input_type & InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) == InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) { + input_type = InputType.TYPE_CLASS_TEXT + | InputType.TYPE_TEXT_VARIATION_PASSWORD; + } else { + input_type = InputType.TYPE_CLASS_TEXT + | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD; + } + mPasswordInput.setInputType(input_type); + mPasswordInput.setSelection(selectionStart, selectionEnd); + } + + + /** + * Called when the checkbox for OAuth authorization is clicked. + * + * Hides or shows the input fields for user & password. + * + * @param view 'View password' 'button' + */ + public void onCheckClick(View view) { + CheckBox oAuth2Check = (CheckBox)view; + changeViewByOAuth2Check(oAuth2Check.isChecked()); + + } + + /** + * Changes the visibility of input elements depending upon the kind of authorization + * chosen by the user: basic or OAuth + * + * @param checked 'True' when OAuth is selected. + */ + public void changeViewByOAuth2Check(Boolean checked) { + + if (checked) { + mOAuthAuthEndpointText.setVisibility(View.VISIBLE); + mOAuthTokenEndpointText.setVisibility(View.VISIBLE); + mUsernameInput.setVisibility(View.GONE); + mPasswordInput.setVisibility(View.GONE); + mViewPasswordButton.setVisibility(View.GONE); + } else { + mOAuthAuthEndpointText.setVisibility(View.GONE); + mOAuthTokenEndpointText.setVisibility(View.GONE); + mUsernameInput.setVisibility(View.VISIBLE); + mPasswordInput.setVisibility(View.VISIBLE); + mViewPasswordButton.setVisibility(View.INVISIBLE); + } + + } + + /* Leave the old OAuth flow + // Results from the first call to oAuth2 server : getting the user_code and verification_url. @Override - public void onClick(View v) { - if (v.getId() == R.id.refreshButton) { - onFocusChange(findViewById(R.id.host_URL), false); - } else if (v.getId() == R.id.viewPassword) { - EditText view = (EditText) findViewById(R.id.account_password); - int selectionStart = view.getSelectionStart(); - int selectionEnd = view.getSelectionEnd(); - int input_type = view.getInputType(); - if ((input_type & InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) == InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) { - input_type = InputType.TYPE_CLASS_TEXT - | InputType.TYPE_TEXT_VARIATION_PASSWORD; - } else { - input_type = InputType.TYPE_CLASS_TEXT - | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD; - } - view.setInputType(input_type); - view.setSelection(selectionStart, selectionEnd); + public void onOAuth2GetCodeResult(ResultOAuthType type, JSONObject responseJson) { + if ((type == ResultOAuthType.OK_SSL)||(type == ResultOAuthType.OK_NO_SSL)) { + mCodeResponseJson = responseJson; + if (mCodeResponseJson != null) { + getOAuth2AccessTokenFromJsonResponse(); + } // else - nothing to do here - wait for callback !!! + + } else if (type == ResultOAuthType.HOST_NOT_AVAILABLE) { + updateOAuth2IconAndText(R.drawable.common_error, R.string.oauth_connection_url_unavailable); } } - @Override - public void onRemoteOperationFinish(RemoteOperation operation, RemoteOperationResult result) { - if (operation.equals(mConnChkRunnable)) { - - mStatusText = mStatusIcon = 0; - mStatusCorrect = false; - String t_url = ((TextView) findViewById(R.id.host_URL)).getText() - .toString().trim().toLowerCase(); - - switch (result.getCode()) { - case OK_SSL: - mIsSslConn = true; - mStatusIcon = android.R.drawable.ic_secure; - mStatusText = R.string.auth_secure_connection; - mStatusCorrect = true; - break; - - case OK_NO_SSL: - case OK: - mIsSslConn = false; - mStatusCorrect = true; - if (t_url.startsWith("http://") ) { - mStatusText = R.string.auth_connection_established; - mStatusIcon = R.drawable.ic_ok; - } else { - mStatusText = R.string.auth_nossl_plain_ok_title; - mStatusIcon = android.R.drawable.ic_partial_secure; - } - break; - - - case BAD_OC_VERSION: - mStatusIcon = R.drawable.common_error; - mStatusText = R.string.auth_bad_oc_version_title; - break; - case WRONG_CONNECTION: - mStatusIcon = R.drawable.common_error; - mStatusText = R.string.auth_wrong_connection_title; - break; - case TIMEOUT: - mStatusIcon = R.drawable.common_error; - mStatusText = R.string.auth_timeout_title; - break; - case INCORRECT_ADDRESS: - mStatusIcon = R.drawable.common_error; - mStatusText = R.string.auth_incorrect_address_title; - break; - - case SSL_RECOVERABLE_PEER_UNVERIFIED: - mStatusIcon = R.drawable.common_error; - mStatusText = R.string.auth_ssl_unverified_server_title; - mLastSslUntrustedServerResult = result; - showDialog(DIALOG_SSL_VALIDATOR); - break; - - case SSL_ERROR: - mStatusIcon = R.drawable.common_error; - mStatusText = R.string.auth_ssl_general_error_title; - break; - - case HOST_NOT_AVAILABLE: - mStatusIcon = R.drawable.common_error; - mStatusText = R.string.auth_unknown_host_title; - break; - case NO_NETWORK_CONNECTION: - mStatusIcon = R.drawable.no_network; - mStatusText = R.string.auth_no_net_conn_title; - break; - case INSTANCE_NOT_CONFIGURED: - mStatusIcon = R.drawable.common_error; - mStatusText = R.string.auth_not_configured_title; - break; - case FILE_NOT_FOUND: - mStatusIcon = R.drawable.common_error; - mStatusText = R.string.auth_incorrect_path_title; - break; - case UNHANDLED_HTTP_CODE: - case UNKNOWN_ERROR: - mStatusIcon = R.drawable.common_error; - mStatusText = R.string.auth_unknown_error_title; - break; - default: - Log.e(TAG, "Incorrect connection checker result type: " + result.getHttpCode()); - } - setResultIconAndText(mStatusIcon, mStatusText); - if (!mStatusCorrect) - findViewById(R.id.refreshButton).setVisibility(View.VISIBLE); - else - findViewById(R.id.refreshButton).setVisibility(View.INVISIBLE); - findViewById(R.id.buttonOK).setEnabled(mStatusCorrect); - } - } - - + // If the results of getting the user_code and verification_url are OK, we get the received data and we start + // the polling service to oAuth2 server to get a valid token. + private void getOAuth2AccessTokenFromJsonResponse() { + String deviceCode = null; + String verificationUrl = null; + String userCode = null; + int expiresIn = -1; + int interval = -1; + + Log.d(TAG, "ResponseOAuth2->" + mCodeResponseJson.toString()); + + try { + // We get data that we must show to the user or we will use internally. + verificationUrl = mCodeResponseJson.getString(OAuth2GetAuthorizationToken.CODE_VERIFICATION_URL); + userCode = mCodeResponseJson.getString(OAuth2GetAuthorizationToken.CODE_USER_CODE); + expiresIn = mCodeResponseJson.getInt(OAuth2GetAuthorizationToken.CODE_EXPIRES_IN); + + // And we get data that we must use to get a token. + deviceCode = mCodeResponseJson.getString(OAuth2GetAuthorizationToken.CODE_DEVICE_CODE); + interval = mCodeResponseJson.getInt(OAuth2GetAuthorizationToken.CODE_INTERVAL); + + } catch (JSONException e) { + Log.e(TAG, "Exception accesing data in Json object" + e.toString()); + } + + // Updating status widget to OK. + updateOAuth2IconAndText(R.drawable.ic_ok, R.string.auth_connection_established); + + // Showing the dialog with instructions for the user. + showDialog(DIALOG_OAUTH2_LOGIN_PROGRESS); + + // Loggin all the data. + Log.d(TAG, "verificationUrl->" + verificationUrl); + Log.d(TAG, "userCode->" + userCode); + Log.d(TAG, "deviceCode->" + deviceCode); + Log.d(TAG, "expiresIn->" + expiresIn); + Log.d(TAG, "interval->" + interval); + + // Starting the pooling service. + try { + Intent tokenService = new Intent(this, OAuth2GetTokenService.class); + tokenService.putExtra(OAuth2GetTokenService.TOKEN_URI, OAuth2Context.OAUTH2_G_DEVICE_GETTOKEN_URL); + tokenService.putExtra(OAuth2GetTokenService.TOKEN_DEVICE_CODE, deviceCode); + tokenService.putExtra(OAuth2GetTokenService.TOKEN_INTERVAL, interval); + + startService(tokenService); + } + catch (Exception e) { + Log.e(TAG, "tokenService creation problem :", e); + } + + } + */ + + /* Leave the old OAuth flow + // We get data from the oAuth2 token service with this broadcast receiver. + private class TokenReceiver extends BroadcastReceiver { + /** + * The token is received. + * @author + * {@link BroadcastReceiver} to enable oAuth2 token receiving. + *-/ + @Override + public void onReceive(Context context, Intent intent) { + @SuppressWarnings("unchecked") + HashMap tokenResponse = (HashMap)intent.getExtras().get(OAuth2GetTokenService.TOKEN_RECEIVED_DATA); + Log.d(TAG, "TokenReceiver->" + tokenResponse.get(OAuth2GetTokenService.TOKEN_ACCESS_TOKEN)); + dismissDialog(DIALOG_OAUTH2_LOGIN_PROGRESS); + + } + } + */ + + + /** + * Called from SslValidatorDialog when a new server certificate was correctly saved. + */ public void onSavedCertificate() { - mAuthThread = mConnChkRunnable.retry(this, mHandler); + mOperationThread = mOcServerChkOperation.retry(this, mHandler); } + /** + * Called from SslValidatorDialog when a new server certificate could not be saved + * when the user requested it. + */ @Override public void onFailedSavingCertificate() { showDialog(DIALOG_CERT_NOT_SAVED); } - + } diff --combined src/com/owncloud/android/ui/activity/FileDisplayActivity.java index 1d75e692,c8dbca98..eb218d08 --- a/src/com/owncloud/android/ui/activity/FileDisplayActivity.java +++ b/src/com/owncloud/android/ui/activity/FileDisplayActivity.java @@@ -1,9 -1,10 +1,10 @@@ /* 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 as published by - * the Free Software Foundation, either version 3 of the License, or + * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, @@@ -35,6 -36,7 +36,7 @@@ import android.content.Intent import android.content.IntentFilter; import android.content.ServiceConnection; import android.content.SharedPreferences; + import android.content.SharedPreferences.Editor; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources.NotFoundException; @@@ -73,7 -75,7 +75,7 @@@ import com.owncloud.android.files.servi import com.owncloud.android.files.services.FileObserverService; import com.owncloud.android.files.services.FileUploader; import com.owncloud.android.files.services.FileUploader.FileUploaderBinder; -import com.owncloud.android.network.OwnCloudClientUtils; +import com.owncloud.android.operations.CreateFolderOperation; import com.owncloud.android.operations.OnRemoteOperationListener; import com.owncloud.android.operations.RemoteOperation; import com.owncloud.android.operations.RemoteOperationResult; @@@ -82,12 -84,14 +84,13 @@@ import com.owncloud.android.operations. import com.owncloud.android.operations.SynchronizeFileOperation; import com.owncloud.android.operations.RemoteOperationResult.ResultCode; import com.owncloud.android.syncadapter.FileSyncService; + import com.owncloud.android.ui.dialog.ChangelogDialog; import com.owncloud.android.ui.dialog.SslValidatorDialog; import com.owncloud.android.ui.dialog.SslValidatorDialog.OnSslValidatorListener; import com.owncloud.android.ui.fragment.FileDetailFragment; import com.owncloud.android.ui.fragment.OCFileListFragment; import com.owncloud.android.R; -import eu.alefzero.webdav.WebdavClient; /** * Displays, what files the user has available in his ownCloud. @@@ -115,7 -119,6 +118,7 @@@ public class FileDisplayActivity extend private OCFileListFragment mFileList; private boolean mDualPane; + private Handler mHandler; private static final int DIALOG_SETUP_ACCOUNT = 0; private static final int DIALOG_CREATE_DIR = 1; @@@ -124,6 -127,7 +127,7 @@@ private static final int DIALOG_CHOOSE_UPLOAD_SOURCE = 4; private static final int DIALOG_SSL_VALIDATOR = 5; private static final int DIALOG_CERT_NOT_SAVED = 6; + private static final String DIALOG_CHANGELOG_TAG = "DIALOG_CHANGELOG"; private static final int ACTION_SELECT_CONTENT_FROM_APPS = 1; @@@ -137,14 -141,12 +141,14 @@@ public void onCreate(Bundle savedInstanceState) { Log.d(getClass().toString(), "onCreate() start"); super.onCreate(savedInstanceState); + + mHandler = new Handler(); /// Load of parameters from received intent - mCurrentDir = getIntent().getParcelableExtra(FileDetailFragment.EXTRA_FILE); // no check necessary, mCurrenDir == null if the parameter is not in the intent Account account = getIntent().getParcelableExtra(FileDetailFragment.EXTRA_ACCOUNT); - if (account != null) - AccountUtils.setCurrentOwnCloudAccount(this, account.name); + if (account != null && AccountUtils.setCurrentOwnCloudAccount(this, account.name)) { + mCurrentDir = getIntent().getParcelableExtra(FileDetailFragment.EXTRA_FILE); + } /// Load of saved instance state: keep this always before initDataFromCurrentAccount() if(savedInstanceState != null) { @@@ -185,7 -187,7 +189,7 @@@ // Drop-down navigation mDirectories = new CustomArrayAdapter(this, R.layout.sherlock_spinner_dropdown_item); OCFile currFile = mCurrentDir; - while(currFile != null && currFile.getFileName() != OCFile.PATH_SEPARATOR) { + while(mStorageManager != null && currFile != null && currFile.getFileName() != OCFile.PATH_SEPARATOR) { mDirectories.add(currFile.getFileName()); currFile = mStorageManager.getFileById(currFile.getParentId()); } @@@ -208,16 -210,46 +212,46 @@@ actionBar.setListNavigationCallbacks(mDirectories, this); setSupportProgressBarIndeterminateVisibility(false); // always AFTER setContentView(...) ; to workaround bug in its implementation + + // show changelog, if needed + //showChangeLog(); + Log.d(getClass().toString(), "onCreate() end"); } /** + * Shows a dialog with the change log of the current version after each app update + * + * TODO make it permanent; by now, only to advice the workaround app for 4.1.x + */ + private void showChangeLog() { + if (android.os.Build.VERSION.SDK_INT == android.os.Build.VERSION_CODES.JELLY_BEAN) { + final String KEY_VERSION = "version"; + SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); + int currentVersionNumber = 0; + int savedVersionNumber = sharedPref.getInt(KEY_VERSION, 0); + try { + PackageInfo pi = getPackageManager().getPackageInfo(getPackageName(), 0); + currentVersionNumber = pi.versionCode; + } catch (Exception e) {} + + if (currentVersionNumber > savedVersionNumber) { + ChangelogDialog.newInstance(true).show(getSupportFragmentManager(), DIALOG_CHANGELOG_TAG); + Editor editor = sharedPref.edit(); + editor.putInt(KEY_VERSION, currentVersionNumber); + editor.commit(); + } + } + } + + + /** * Launches the account creation activity. To use when no ownCloud account is available */ private void createFirstAccount() { Intent intent = new Intent(android.provider.Settings.ACTION_ADD_ACCOUNT); - intent.putExtra(android.provider.Settings.EXTRA_AUTHORITIES, new String[] { AccountAuthenticator.AUTH_TOKEN_TYPE }); + intent.putExtra(android.provider.Settings.EXTRA_AUTHORITIES, new String[] { AccountAuthenticator.AUTHORITY }); startActivity(intent); // the new activity won't be created until this.onStart() and this.onResume() are finished; } @@@ -346,12 -378,12 +380,12 @@@ } private void startSynchronization() { - ContentResolver.cancelSync(null, AccountAuthenticator.AUTH_TOKEN_TYPE); // cancel the current synchronizations of any ownCloud account + ContentResolver.cancelSync(null, AccountAuthenticator.AUTHORITY); // cancel the current synchronizations of any ownCloud account Bundle bundle = new Bundle(); bundle.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); ContentResolver.requestSync( AccountUtils.getCurrentOwnCloudAccount(this), - AccountAuthenticator.AUTH_TOKEN_TYPE, bundle); + AccountAuthenticator.AUTHORITY, bundle); } @@@ -654,12 -686,8 +688,12 @@@ // Create directory path += directoryName + OCFile.PATH_SEPARATOR; - Thread thread = new Thread(new DirectoryCreator(path, AccountUtils.getCurrentOwnCloudAccount(FileDisplayActivity.this), new Handler())); - thread.start(); + RemoteOperation operation = new CreateFolderOperation(path, mCurrentDir.getFileId(), mStorageManager); + operation.execute( AccountUtils.getCurrentOwnCloudAccount(FileDisplayActivity.this), + FileDisplayActivity.this, + FileDisplayActivity.this, + mHandler, + FileDisplayActivity.this); dialog.dismiss(); @@@ -777,6 -805,56 +811,6 @@@ return !mDirectories.isEmpty(); } - private class DirectoryCreator implements Runnable { - private String mTargetPath; - private Account mAccount; - private Handler mHandler; - - public DirectoryCreator(String targetPath, Account account, Handler handler) { - mTargetPath = targetPath; - mAccount = account; - mHandler = handler; - } - - @Override - public void run() { - WebdavClient wdc = OwnCloudClientUtils.createOwnCloudClient(mAccount, getApplicationContext()); - boolean created = wdc.createDirectory(mTargetPath); - if (created) { - mHandler.post(new Runnable() { - @Override - public void run() { - dismissDialog(DIALOG_SHORT_WAIT); - - // Save new directory in local database - OCFile newDir = new OCFile(mTargetPath); - newDir.setMimetype("DIR"); - newDir.setParentId(mCurrentDir.getFileId()); - mStorageManager.saveFile(newDir); - - // Display the new folder right away - mFileList.listDirectory(); - } - }); - - } else { - mHandler.post(new Runnable() { - @Override - public void run() { - dismissDialog(DIALOG_SHORT_WAIT); - try { - Toast msg = Toast.makeText(FileDisplayActivity.this, R.string.create_dir_fail_msg, Toast.LENGTH_LONG); - msg.show(); - - } catch (NotFoundException e) { - Log.e(TAG, "Error while trying to show fail message " , e); - } - } - }); - } - } - - } // Custom array adapter to override text colors private class CustomArrayAdapter extends ArrayAdapter { @@@ -1077,32 -1155,6 +1111,32 @@@ } else if (operation instanceof SynchronizeFileOperation) { onSynchronizeFileOperationFinish((SynchronizeFileOperation)operation, result); + + } else if (operation instanceof CreateFolderOperation) { + onCreateFolderOperationFinish((CreateFolderOperation)operation, result); + } + } + + /** + * Updates the view associated to the activity after the finish of an operation trying create a new folder + * + * @param operation Creation operation performed. + * @param result Result of the creation. + */ + private void onCreateFolderOperationFinish(CreateFolderOperation operation, RemoteOperationResult result) { + if (result.isSuccess()) { + dismissDialog(DIALOG_SHORT_WAIT); + mFileList.listDirectory(); + + } else { + dismissDialog(DIALOG_SHORT_WAIT); + try { + Toast msg = Toast.makeText(FileDisplayActivity.this, R.string.create_dir_fail_msg, Toast.LENGTH_LONG); + msg.show(); + + } catch (NotFoundException e) { + Log.e(TAG, "Error while trying to show fail message " , e); + } } } diff --combined src/com/owncloud/android/ui/activity/LandingActivity.java index e90d36a7,32fae223..e63a0fe0 --- a/src/com/owncloud/android/ui/activity/LandingActivity.java +++ b/src/com/owncloud/android/ui/activity/LandingActivity.java @@@ -3,7 -3,7 +3,7 @@@ * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or + * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, @@@ -113,9 -113,9 +113,9 @@@ public class LandingActivity extends Sh dialog.dismiss(); switch (which) { case DialogInterface.BUTTON_POSITIVE: - Intent intent = new Intent("android.settings.ADD_ACCOUNT_SETTINGS"); + Intent intent = new Intent(android.provider.Settings.ACTION_ADD_ACCOUNT); intent.putExtra("authorities", - new String[] { AccountAuthenticator.AUTH_TOKEN_TYPE }); + new String[] { AccountAuthenticator.AUTHORITY }); startActivity(intent); break; case DialogInterface.BUTTON_NEGATIVE: diff --combined src/com/owncloud/android/ui/fragment/FileDetailFragment.java index 5595583e,2d46a55c..3668ff38 --- a/src/com/owncloud/android/ui/fragment/FileDetailFragment.java +++ b/src/com/owncloud/android/ui/fragment/FileDetailFragment.java @@@ -1,9 -1,10 +1,10 @@@ /* 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 as published by - * the Free Software Foundation, either version 3 of the License, or + * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, @@@ -18,8 -19,20 +19,8 @@@ package com.owncloud.android.ui.fragment; import java.io.File; -import java.util.ArrayList; -import java.util.List; - -import org.apache.commons.httpclient.methods.GetMethod; -import org.apache.commons.httpclient.methods.PostMethod; -import org.apache.commons.httpclient.methods.StringRequestEntity; -import org.apache.commons.httpclient.params.HttpConnectionManagerParams; -import org.apache.http.HttpStatus; -import org.apache.http.NameValuePair; -import org.apache.http.client.utils.URLEncodedUtils; -import org.apache.http.message.BasicNameValuePair; -import org.apache.http.protocol.HTTP; -import org.apache.jackrabbit.webdav.client.methods.PropFindMethod; -import org.json.JSONObject; + +import org.apache.commons.httpclient.Credentials; import android.accounts.Account; import android.accounts.AccountManager; @@@ -54,6 -67,7 +55,6 @@@ import android.widget.TextView import android.widget.Toast; import com.actionbarsherlock.app.SherlockFragment; -import com.owncloud.android.AccountUtils; import com.owncloud.android.DisplayUtils; import com.owncloud.android.authenticator.AccountAuthenticator; import com.owncloud.android.datamodel.FileDataStorageManager; @@@ -63,7 -77,7 +64,7 @@@ import com.owncloud.android.files.servi import com.owncloud.android.files.services.FileUploader; import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder; import com.owncloud.android.files.services.FileUploader.FileUploaderBinder; -import com.owncloud.android.network.OwnCloudClientUtils; +import com.owncloud.android.network.BearerCredentials; import com.owncloud.android.operations.OnRemoteOperationListener; import com.owncloud.android.operations.RemoteOperation; import com.owncloud.android.operations.RemoteOperationResult; @@@ -77,8 -91,10 +78,8 @@@ import com.owncloud.android.ui.activity import com.owncloud.android.ui.activity.TransferServiceGetter; import com.owncloud.android.ui.dialog.EditNameDialog; import com.owncloud.android.ui.dialog.EditNameDialog.EditNameDialogListener; -import com.owncloud.android.utils.OwnCloudVersion; import com.owncloud.android.R; -import eu.alefzero.webdav.WebdavClient; import eu.alefzero.webdav.WebdavUtils; /** @@@ -293,7 -309,8 +294,7 @@@ public class FileDetailFragment extend } else { mLastRemoteOperation = new SynchronizeFileOperation(mFile, null, mStorageManager, mAccount, true, false, getActivity()); - WebdavClient wc = OwnCloudClientUtils.createOwnCloudClient(mAccount, getSherlockActivity().getApplicationContext()); - mLastRemoteOperation.execute(wc, this, mHandler); + mLastRemoteOperation.execute(mAccount, getSherlockActivity(), this, mHandler, getSherlockActivity()); // update ui boolean inDisplayActivity = getActivity() instanceof FileDisplayActivity; @@@ -407,7 -424,9 +408,7 @@@ mLastRemoteOperation = new RemoveFileOperation( mFile, true, mStorageManager); - WebdavClient wc = OwnCloudClientUtils.createOwnCloudClient(mAccount, getSherlockActivity().getApplicationContext()); - mLastRemoteOperation.execute(wc, this, mHandler); - + mLastRemoteOperation.execute(mAccount, getSherlockActivity(), this, mHandler, getSherlockActivity()); boolean inDisplayActivity = getActivity() instanceof FileDisplayActivity; getActivity().showDialog((inDisplayActivity)? FileDisplayActivity.DIALOG_SHORT_WAIT : FileDetailActivity.DIALOG_SHORT_WAIT); } @@@ -489,8 -508,7 +490,7 @@@ // set file details setFilename(mFile.getFileName()); - setFiletype(DisplayUtils.convertMIMEtoPrettyPrint(mFile - .getMimetype())); + setFiletype(mFile.getMimetype()); setFilesize(mFile.getFileLength()); if(ocVersionSupportsTimeCreated()){ setTimeCreated(mFile.getCreationTimestamp()); @@@ -542,8 -560,14 +542,14 @@@ */ private void setFiletype(String mimetype) { TextView tv = (TextView) getView().findViewById(R.id.fdType); - if (tv != null) - tv.setText(mimetype); + if (tv != null) { + String printableMimetype = DisplayUtils.convertMIMEtoPrettyPrint(mimetype);; + tv.setText(printableMimetype); + } + ImageView iv = (ImageView) getView().findViewById(R.id.fdIcon); + if (iv != null) { + iv.setImageResource(DisplayUtils.getResourceId(mimetype)); + } } /** @@@ -718,7 -742,7 +724,7 @@@ if (mFile.getRemotePath().equals(uploadRemotePath) || renamedInUpload) { if (uploadWasFine) { - mFile = mStorageManager.getFileByPath(uploadRemotePath); + mFile = mStorageManager.getFileByPath(uploadRemotePath); } if (renamedInUpload) { String newName = (new File(uploadRemotePath)).getName(); @@@ -734,7 -758,6 +740,7 @@@ // this is a temporary class for sharing purposes, it need to be replaced in transfer service + /* @SuppressWarnings("unused") private class ShareRunnable implements Runnable { private String mPath; @@@ -837,7 -860,6 +843,7 @@@ } } } + */ public void onDismiss(EditNameDialog dialog) { if (dialog.getResult()) { @@@ -847,7 -869,8 +853,7 @@@ mAccount, newFilename, new FileDataStorageManager(mAccount, getActivity().getContentResolver())); - WebdavClient wc = OwnCloudClientUtils.createOwnCloudClient(mAccount, getSherlockActivity().getApplicationContext()); - mLastRemoteOperation.execute(wc, this, mHandler); + mLastRemoteOperation.execute(mAccount, getSherlockActivity(), this, mHandler, getSherlockActivity()); boolean inDisplayActivity = getActivity() instanceof FileDisplayActivity; getActivity().showDialog((inDisplayActivity)? FileDisplayActivity.DIALOG_SHORT_WAIT : FileDetailActivity.DIALOG_SHORT_WAIT); } @@@ -933,14 -956,16 +939,14 @@@ */ @Override public void onRemoteOperationFinish(RemoteOperation operation, RemoteOperationResult result) { - if (operation.equals(mLastRemoteOperation)) { - if (operation instanceof RemoveFileOperation) { - onRemoveFileOperationFinish((RemoveFileOperation)operation, result); + if (operation instanceof RemoveFileOperation) { + onRemoveFileOperationFinish((RemoveFileOperation)operation, result); - } else if (operation instanceof RenameFileOperation) { - onRenameFileOperationFinish((RenameFileOperation)operation, result); + } else if (operation instanceof RenameFileOperation) { + onRenameFileOperationFinish((RenameFileOperation)operation, result); - } else if (operation instanceof SynchronizeFileOperation) { - onSynchronizeFileOperationFinish((SynchronizeFileOperation)operation, result); - } + } else if (operation instanceof SynchronizeFileOperation) { + onSynchronizeFileOperationFinish((SynchronizeFileOperation)operation, result); } } diff --combined src/com/owncloud/android/ui/fragment/OCFileListFragment.java index 786d98a2,34e156d3..0e5e879a --- a/src/com/owncloud/android/ui/fragment/OCFileListFragment.java +++ b/src/com/owncloud/android/ui/fragment/OCFileListFragment.java @@@ -1,9 -1,10 +1,10 @@@ /* 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 as published by - * the Free Software Foundation, either version 3 of the License, or + * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, @@@ -27,6 -28,7 +28,6 @@@ import com.owncloud.android.datamodel.D import com.owncloud.android.datamodel.OCFile; import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder; import com.owncloud.android.files.services.FileUploader.FileUploaderBinder; -import com.owncloud.android.network.OwnCloudClientUtils; import com.owncloud.android.operations.OnRemoteOperationListener; import com.owncloud.android.operations.RemoteOperation; import com.owncloud.android.operations.RemoveFileOperation; @@@ -40,6 -42,7 +41,6 @@@ import com.owncloud.android.ui.dialog.E import com.owncloud.android.ui.dialog.EditNameDialog.EditNameDialogListener; import com.owncloud.android.ui.fragment.ConfirmationDialogFragment.ConfirmationDialogFragmentListener; -import eu.alefzero.webdav.WebdavClient; import eu.alefzero.webdav.WebdavUtils; import android.accounts.Account; @@@ -314,7 -317,8 +315,7 @@@ public class OCFileListFragment extend case R.id.download_file_item: { Account account = AccountUtils.getCurrentOwnCloudAccount(getSherlockActivity()); RemoteOperation operation = new SynchronizeFileOperation(mTargetFile, null, mContainerActivity.getStorageManager(), account, true, false, getSherlockActivity()); - WebdavClient wc = OwnCloudClientUtils.createOwnCloudClient(account, getSherlockActivity().getApplicationContext()); - operation.execute(wc, mContainerActivity, mHandler); + operation.execute(account, getSherlockActivity(), mContainerActivity, mHandler, getSherlockActivity()); getSherlockActivity().showDialog(FileDisplayActivity.DIALOG_SHORT_WAIT); return true; } @@@ -476,7 -480,8 +477,7 @@@ AccountUtils.getCurrentOwnCloudAccount(getActivity()), newFilename, mContainerActivity.getStorageManager()); - WebdavClient wc = OwnCloudClientUtils.createOwnCloudClient(AccountUtils.getCurrentOwnCloudAccount(getSherlockActivity()), getSherlockActivity().getApplicationContext()); - operation.execute(wc, mContainerActivity, mHandler); + operation.execute(AccountUtils.getCurrentOwnCloudAccount(getSherlockActivity()), getSherlockActivity(), mContainerActivity, mHandler, getSherlockActivity()); getActivity().showDialog(FileDisplayActivity.DIALOG_SHORT_WAIT); } } @@@ -489,7 -494,8 +490,7 @@@ RemoteOperation operation = new RemoveFileOperation( mTargetFile, true, mContainerActivity.getStorageManager()); - WebdavClient wc = OwnCloudClientUtils.createOwnCloudClient(AccountUtils.getCurrentOwnCloudAccount(getSherlockActivity()), getSherlockActivity().getApplicationContext()); - operation.execute(wc, mContainerActivity, mHandler); + operation.execute(AccountUtils.getCurrentOwnCloudAccount(getSherlockActivity()), getSherlockActivity(), mContainerActivity, mHandler, getSherlockActivity()); getActivity().showDialog(FileDisplayActivity.DIALOG_SHORT_WAIT); } diff --combined src/eu/alefzero/webdav/WebdavClient.java index a9d2e676,1319a2b0..7577152d --- a/src/eu/alefzero/webdav/WebdavClient.java +++ b/src/eu/alefzero/webdav/WebdavClient.java @@@ -1,9 -1,10 +1,10 @@@ /* 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 as published by - * the Free Software Foundation, either version 3 of the License, or + * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, @@@ -22,20 -23,14 +23,20 @@@ import java.io.File import java.io.FileOutputStream; 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.HostConfiguration; 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.HttpState; import org.apache.commons.httpclient.HttpVersion; 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.methods.GetMethod; import org.apache.commons.httpclient.methods.HeadMethod; @@@ -45,9 -40,7 +46,9 @@@ import org.apache.http.HttpStatus import org.apache.http.params.CoreProtocolPNames; import org.apache.jackrabbit.webdav.client.methods.DavMethod; import org.apache.jackrabbit.webdav.client.methods.DeleteMethod; -import org.apache.jackrabbit.webdav.client.methods.MkColMethod; + +import com.owncloud.android.network.BearerAuthScheme; +import com.owncloud.android.network.BearerCredentials; import android.net.Uri; import android.util.Log; @@@ -71,28 -64,18 +72,28 @@@ public class WebdavClient extends HttpC getParams().setParameter(CoreProtocolPNames.PROTOCOL_VERSION, HttpVersion.HTTP_1_1); } - public void setCredentials(String username, String password) { - getParams().setAuthenticationPreemptive(true); - getState().setCredentials(AuthScope.ANY, - getCredentials(username, password)); + public void setBearerCredentials(String accessToken) { + AuthPolicy.registerAuthScheme(BearerAuthScheme.AUTH_POLICY, BearerAuthScheme.class); + + List authPrefs = new ArrayList(1); + authPrefs.add(BearerAuthScheme.AUTH_POLICY); + getParams().setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY, authPrefs); + + mCredentials = new BearerCredentials(accessToken); + getState().setCredentials(AuthScope.ANY, mCredentials); } - private Credentials getCredentials(String username, String password) { - if (mCredentials == null) - mCredentials = new UsernamePasswordCredentials(username, password); - return mCredentials; + public void setBasicCredentials(String username, String password) { + List authPrefs = new ArrayList(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); } + /** * Downloads a file in remoteFilepath to the local targetPath. * @@@ -218,6 -201,34 +219,6 @@@ return status; } - /** - * Creates a remote directory with the received path. - * - * @param path Path of the directory to create, URL DECODED - * @return 'True' when the directory is successfully created - */ - public boolean createDirectory(String path) { - boolean result = false; - int status = -1; - MkColMethod mkcol = new MkColMethod(mUri.toString() + WebdavUtils.encodePath(path)); - try { - Log.d(TAG, "Creating directory " + path); - status = executeMethod(mkcol); - Log.d(TAG, "Status returned: " + status); - result = mkcol.succeeded(); - - Log.d(TAG, "MKCOL to " + path + " finished with HTTP status " + status + (!result?"(FAIL)":"")); - exhaustResponse(mkcol.getResponseBodyAsStream()); - - } catch (Exception e) { - logException(e, "creating directory " + path); - - } finally { - mkcol.releaseConnection(); // let the connection available for other methods - } - return result; - } - /** * Check if a file exists in the OC server @@@ -326,20 -337,5 +327,20 @@@ public Uri getBaseUri() { return mUri; } + + + @Override + public int executeMethod(HostConfiguration hostconfig, final HttpMethod method, final HttpState state) throws IOException, HttpException { + if (mCredentials instanceof BearerCredentials) { + method.getHostAuthState().setAuthScheme(AuthPolicy.getAuthScheme(BearerAuthScheme.AUTH_POLICY)); + method.getHostAuthState().setAuthAttempted(true); + } + return super.executeMethod(hostconfig, method, state); + } + + + public final Credentials getCredentials() { + return mCredentials; + } }