diff --git a/build.gradle b/build.gradle index ba46caada1..6bf56f0f22 100644 --- a/build.gradle +++ b/build.gradle @@ -234,6 +234,7 @@ dependencies { implementation 'com.afollestad:sectioned-recyclerview:0.5.0' implementation 'com.github.chrisbanes:PhotoView:2.3.0' implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.15' + implementation 'com.github.tobiaskaminsky:qrcodescanner:0.1.2.2' // 'com.github.blikoon:QRCodeScanner:0.1.2' implementation 'org.parceler:parceler-api:1.1.12' annotationProcessor 'org.parceler:parceler:1.1.12' diff --git a/drawable_resources/qrcode.svg b/drawable_resources/qrcode.svg new file mode 100644 index 0000000000..9850ebd7a7 --- /dev/null +++ b/drawable_resources/qrcode.svg @@ -0,0 +1,247 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/debug/AndroidManifest.xml b/src/debug/AndroidManifest.xml index 7de795c338..f921e3b3c0 100644 --- a/src/debug/AndroidManifest.xml +++ b/src/debug/AndroidManifest.xml @@ -14,6 +14,5 @@ diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 1f602a4610..04365047d4 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -52,6 +52,7 @@ + @@ -70,6 +71,7 @@ must request the FOREGROUND_SERVICE permission --> + + + + + - + + diff --git a/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java b/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java index 2c05c8e6b9..51feba6b3c 100644 --- a/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java +++ b/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java @@ -40,6 +40,7 @@ package com.owncloud.android.authentication; +import android.Manifest; import android.accounts.Account; import android.accounts.AccountManager; import android.annotation.SuppressLint; @@ -50,6 +51,7 @@ import android.content.Intent; import android.content.ServiceConnection; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.net.Uri; @@ -83,6 +85,7 @@ import android.widget.ProgressBar; import android.widget.TextView; import android.widget.TextView.OnEditorActionListener; +import com.blikoon.qrcodescanner.QrCodeActivity; import com.google.android.material.snackbar.Snackbar; import com.google.android.material.textfield.TextInputLayout; import com.owncloud.android.MainApp; @@ -110,6 +113,7 @@ import com.owncloud.android.operations.GetServerInfoOperation; import com.owncloud.android.operations.OAuth2GetAccessToken; import com.owncloud.android.services.OperationsService; import com.owncloud.android.services.OperationsService.OperationsServiceBinder; +import com.owncloud.android.ui.activity.FileDisplayActivity; import com.owncloud.android.ui.activity.FirstRunActivity; import com.owncloud.android.ui.components.CustomEditText; import com.owncloud.android.ui.dialog.CredentialsDialogFragment; @@ -119,6 +123,7 @@ import com.owncloud.android.ui.dialog.SslUntrustedCertDialog; import com.owncloud.android.ui.dialog.SslUntrustedCertDialog.OnSslUntrustedCertListener; import com.owncloud.android.utils.DisplayUtils; import com.owncloud.android.utils.ErrorMessageAdapter; +import com.owncloud.android.utils.PermissionUtil; import java.io.InputStream; import java.net.URLDecoder; @@ -127,6 +132,7 @@ import java.util.HashMap; import java.util.Locale; import java.util.Map; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.Fragment; @@ -191,6 +197,9 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity public static final int NO_ICON = 0; public static final String EMPTY_STRING = ""; + private static final int REQUEST_CODE_QR_SCAN = 101; + + /// parameters from EXTRAs in starter Intent private byte mAction; private Account mAccount; @@ -261,7 +270,9 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity //Log_OC.e(TAG, "onCreate init"); super.onCreate(savedInstanceState); - if (savedInstanceState == null) { + Uri data = getIntent().getData(); + boolean directLogin = data != null && data.toString().startsWith(getString(R.string.login_data_own_scheme)); + if (savedInstanceState == null && !directLogin) { FirstRunActivity.runIfNeeded(this); } @@ -320,21 +331,9 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity /// initialize general UI elements initOverallUi(); - findViewById(R.id.centeredRefreshButton).setOnClickListener(new View.OnClickListener() { + findViewById(R.id.centeredRefreshButton).setOnClickListener(v -> checkOcServer()); - @Override - public void onClick(View v) { - checkOcServer(); - } - }); - - findViewById(R.id.embeddedRefreshButton).setOnClickListener(new View.OnClickListener() { - - @Override - public void onClick(View v) { - checkOcServer(); - } - }); + findViewById(R.id.embeddedRefreshButton).setOnClickListener(v -> checkOcServer()); /// initialize block to be moved to single Fragment to check server and get info about it @@ -404,41 +403,35 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity // show snackbar after 60s to switch back to old login method if (showLegacyLogin) { - new Handler().postDelayed(new Runnable() { - @Override - public void run() { - DisplayUtils.createSnackbar(mLoginWebView, R.string.fallback_weblogin_text, Snackbar.LENGTH_INDEFINITE) - .setActionTextColor(getResources().getColor(R.color.primary_dark)) - .setAction(R.string.fallback_weblogin_back, new View.OnClickListener() { - @Override - public void onClick(View v) { - mLoginWebView.setVisibility(View.INVISIBLE); - webViewLoginMethod = false; + new Handler().postDelayed(() -> DisplayUtils.createSnackbar(mLoginWebView, + R.string.fallback_weblogin_text, + Snackbar.LENGTH_INDEFINITE) + .setActionTextColor(getResources().getColor(R.color.primary_dark)) + .setAction(R.string.fallback_weblogin_back, v -> { + mLoginWebView.setVisibility(View.INVISIBLE); + webViewLoginMethod = false; - setContentView(R.layout.account_setup); + setContentView(R.layout.account_setup); - // initialize general UI elements - initOverallUi(); + // initialize general UI elements + initOverallUi(); - mPasswordInputLayout.setVisibility(View.VISIBLE); - mUsernameInputLayout.setVisibility(View.VISIBLE); - mUsernameInput.requestFocus(); - mOAuth2Check.setVisibility(View.INVISIBLE); - mAuthStatusView.setVisibility(View.INVISIBLE); - mServerStatusView.setVisibility(View.INVISIBLE); - mTestServerButton.setVisibility(View.INVISIBLE); - forceOldLoginMethod = true; - mOkButton.setVisibility(View.VISIBLE); + mPasswordInputLayout.setVisibility(View.VISIBLE); + mUsernameInputLayout.setVisibility(View.VISIBLE); + mUsernameInput.requestFocus(); + mOAuth2Check.setVisibility(View.INVISIBLE); + mAuthStatusView.setVisibility(View.INVISIBLE); + mServerStatusView.setVisibility(View.INVISIBLE); + mTestServerButton.setVisibility(View.INVISIBLE); + forceOldLoginMethod = true; + mOkButton.setVisibility(View.VISIBLE); - initServerPreFragment(null); + initServerPreFragment(null); - mHostUrlInput.setText(baseURL); + mHostUrlInput.setText(baseURL); - checkOcServer(); - } - }).show(); - } - }, 60 * 1000); + checkOcServer(); + }).show(), 60 * 1000); } } @@ -512,9 +505,15 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity LoginUrlInfo loginUrlInfo = parseLoginDataUrl(prefix, dataString); if (loginUrlInfo != null) { - mServerInfo.mBaseUrl = AuthenticatorUrlUtils.normalizeUrlSuffix(loginUrlInfo.serverAddress); - webViewUser = loginUrlInfo.username; - webViewPassword = loginUrlInfo.password; + try { + mServerInfo.mBaseUrl = AuthenticatorUrlUtils.normalizeUrlSuffix(loginUrlInfo.serverAddress); + webViewUser = loginUrlInfo.username; + webViewPassword = loginUrlInfo.password; + } catch (Exception e) { + mServerStatusIcon = R.drawable.ic_alert; + mServerStatusText = "QR Code could not be read!"; + showServerStatus(); + } checkOcServer(); } } @@ -583,14 +582,19 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity } private void initAuthTokenType() { - mAuthTokenType = getIntent().getExtras().getString(AccountAuthenticator.KEY_AUTH_TOKEN_TYPE); + Bundle extras = getIntent().getExtras(); + mAuthTokenType = null; + + if (extras != null) { + extras.getString(AccountAuthenticator.KEY_AUTH_TOKEN_TYPE); + } + if (mAuthTokenType == null) { if (mAccount != null) { boolean oAuthRequired = mAccountMgr.getUserData(mAccount, Constants.KEY_SUPPORTS_OAUTH2) != null; boolean samlWebSsoRequired = mAccountMgr.getUserData (mAccount, Constants.KEY_SUPPORTS_SAML_WEB_SSO) != null; mAuthTokenType = chooseAuthTokenType(oAuthRequired, samlWebSsoRequired); - } else { boolean oAuthSupported = AUTH_ON.equals(getString(R.string.auth_method_oauth2)); boolean samlWebSsoSupported = AUTH_ON.equals(getString(R.string.auth_method_saml_web_sso)); @@ -627,6 +631,8 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity mOkButton = findViewById(R.id.buttonOK); mOkButton.setOnClickListener(v -> onOkClick()); + findViewById(R.id.scanQR).setOnClickListener(v -> onScan()); + setupInstructionMessage(); mTestServerButton.setVisibility(mAction == ACTION_CREATE ? View.VISIBLE : View.GONE); @@ -763,17 +769,14 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity // TODO find out if this is really necessary, or if it can done in a different way - findViewById(R.id.scroll).setOnTouchListener(new OnTouchListener() { - @Override - public boolean onTouch(View view, MotionEvent event) { - if (event.getAction() == MotionEvent.ACTION_DOWN && - AccountTypeUtils.getAuthTokenTypeSamlSessionCookie( - MainApp.getAccountType(getBaseContext())).equals(mAuthTokenType) && - mHostUrlInput.hasFocus()) { - checkOcServer(); - } - return false; + findViewById(R.id.scroll).setOnTouchListener((view, event) -> { + if (event.getAction() == MotionEvent.ACTION_DOWN && + AccountTypeUtils.getAuthTokenTypeSamlSessionCookie( + MainApp.getAccountType(getBaseContext())).equals(mAuthTokenType) && + mHostUrlInput.hasFocus()) { + checkOcServer(); } + return false; }); } } @@ -980,6 +983,10 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity mNewCapturedUriFromOAuth2Redirection = data; } + if (data != null && data.toString().startsWith(getString(R.string.login_data_own_scheme))) { + parseAndLoginFromWebView(data.toString()); + } + if (intent.getBooleanExtra(EXTRA_USE_PROVIDER_AS_WEBLOGIN, false)) { webViewLoginMethod = true; setContentView(R.layout.account_setup_webview); @@ -1757,6 +1764,14 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity if (success) { finish(); + + AccountUtils.setCurrentOwnCloudAccount(this, mAccount.name); + + Intent i = new Intent(this, FileDisplayActivity.class); + i.setAction(FileDisplayActivity.RESTART); + i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(i); + } else { // init webView again if (mLoginWebView != null) { @@ -1804,9 +1819,31 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity } else { // authorization fail due to client side - probably wrong credentials if (webViewLoginMethod) { mLoginWebView = findViewById(R.id.login_webview); - initWebViewLogin(mServerInfo.mBaseUrl + WEB_LOGIN, true, false); - DisplayUtils.showSnackMessage(this, mLoginWebView, R.string.auth_access_failed, result.getLogMessage()); + if (mLoginWebView != null) { + initWebViewLogin(mServerInfo.mBaseUrl + WEB_LOGIN, true, false); + DisplayUtils.showSnackMessage(this, mLoginWebView, R.string.auth_access_failed, + result.getLogMessage()); + } else { + DisplayUtils.showSnackMessage(this, R.string.auth_access_failed, result.getLogMessage()); + + // init webView again + if (mLoginWebView != null) { + mLoginWebView.setVisibility(View.GONE); + } + setContentView(R.layout.account_setup); + + initOverallUi(); + + CustomEditText serverAddressField = findViewById(R.id.hostUrlInput); + serverAddressField.setText(mServerInfo.mBaseUrl); + + findViewById(R.id.oauth_onOff_check).setVisibility(View.GONE); + findViewById(R.id.server_status_text).setVisibility(View.GONE); + mAuthStatusView = findViewById(R.id.auth_status_text); + + showAuthStatus(); + } } else { updateAuthStatusIconAndText(result); showAuthStatus(); @@ -1978,6 +2015,40 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity } } + public void onScan() { + if (PermissionUtil.checkSelfPermission(this, Manifest.permission.CAMERA)) { + startQRScanner(); + } else { + PermissionUtil.requestCameraPermission(this); + } + } + + private void startQRScanner() { + Intent i = new Intent(AuthenticatorActivity.this, QrCodeActivity.class); + startActivityForResult(i, REQUEST_CODE_QR_SCAN); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], + @NonNull int[] grantResults) { + switch (requestCode) { + case PermissionUtil.PERMISSIONS_CAMERA: { + // If request is cancelled, result arrays are empty. + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // permission was granted + startQRScanner(); + } else { + // permission denied + return; + } + return; + } + + default: + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + } + /** /** * Updates the content and visibility state of the icon and text associated @@ -2256,10 +2327,27 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity )) { mOperationsServiceBinder = (OperationsServiceBinder) service; - doOnResumeAndBound(); + Uri data = getIntent().getData(); + if (data != null && data.toString().startsWith(getString(R.string.login_data_own_scheme))) { + String prefix = getString(R.string.login_data_own_scheme) + PROTOCOL_SUFFIX + "login/"; + LoginUrlInfo loginUrlInfo = parseLoginDataUrl(prefix, data.toString()); + if (loginUrlInfo != null) { + try { + mServerInfo.mBaseUrl = AuthenticatorUrlUtils.normalizeUrlSuffix(loginUrlInfo.serverAddress); + webViewUser = loginUrlInfo.username; + webViewPassword = loginUrlInfo.password; + doOnResumeAndBound(); + } catch (Exception e) { + mServerStatusIcon = R.drawable.ic_alert; + mServerStatusText = "QR Code could not be read!"; + showServerStatus(); + } + } + } else { + doOnResumeAndBound(); + } } - } @Override @@ -2302,4 +2390,24 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity public void doNegativeAuthenticationDialogClick() { mIsFirstAuthAttempt = true; } + + @Override + protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + if (requestCode == REQUEST_CODE_QR_SCAN) { + if (data == null) { + return; + } + + String result = data.getStringExtra("com.blikoon.qrcodescanner.got_qr_scan_relult"); + + if (!result.startsWith(getString(R.string.login_data_own_scheme))) { + mServerStatusIcon = R.drawable.ic_alert; + mServerStatusText = "QR Code could not be read!"; + showServerStatus(); + return; + } + + parseAndLoginFromWebView(result); + } + } } diff --git a/src/main/java/com/owncloud/android/authentication/DeepLinkLoginActivity.java b/src/main/java/com/owncloud/android/authentication/DeepLinkLoginActivity.java new file mode 100644 index 0000000000..efb6f1a7a0 --- /dev/null +++ b/src/main/java/com/owncloud/android/authentication/DeepLinkLoginActivity.java @@ -0,0 +1,29 @@ +package com.owncloud.android.authentication; + +import android.net.Uri; +import android.os.Bundle; +import android.widget.TextView; + +import com.owncloud.android.R; +import com.owncloud.android.utils.ThemeUtils; + +public class DeepLinkLoginActivity extends AuthenticatorActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.deep_link_login); + + Uri data = getIntent().getData(); + + if (data != null) { + String prefix = getString(R.string.login_data_own_scheme) + PROTOCOL_SUFFIX + "login/"; + LoginUrlInfo loginUrlInfo = parseLoginDataUrl(prefix, data.toString()); + + TextView loginText = findViewById(R.id.loginInfo); + loginText.setTextColor(ThemeUtils.fontColor(this)); + loginText.setText(String.format("Login with %1$s to %2$s", loginUrlInfo.username, + loginUrlInfo.serverAddress)); + } + } +} diff --git a/src/main/java/com/owncloud/android/services/OperationsService.java b/src/main/java/com/owncloud/android/services/OperationsService.java index c967104eb6..77f8c805da 100644 --- a/src/main/java/com/owncloud/android/services/OperationsService.java +++ b/src/main/java/com/owncloud/android/services/OperationsService.java @@ -740,8 +740,7 @@ public class OperationsService extends Service { final RemoteOperation operation, final RemoteOperationResult result ) { int count = 0; - Iterator listeners = - mOperationsBinder.mBoundListeners.keySet().iterator(); + Iterator listeners = mOperationsBinder.mBoundListeners.keySet().iterator(); while (listeners.hasNext()) { final OnRemoteOperationListener listener = listeners.next(); final Handler handler = mOperationsBinder.mBoundListeners.get(listener); diff --git a/src/main/java/com/owncloud/android/utils/PermissionUtil.java b/src/main/java/com/owncloud/android/utils/PermissionUtil.java index edac635226..c276dffaa6 100644 --- a/src/main/java/com/owncloud/android/utils/PermissionUtil.java +++ b/src/main/java/com/owncloud/android/utils/PermissionUtil.java @@ -15,6 +15,7 @@ public final class PermissionUtil { public static final int PERMISSIONS_READ_CONTACTS_AUTOMATIC = 2; public static final int PERMISSIONS_READ_CONTACTS_MANUALLY = 3; public static final int PERMISSIONS_WRITE_CONTACTS = 4; + public static final int PERMISSIONS_CAMERA = 5; private PermissionUtil() { // utility class -> private constructor @@ -57,4 +58,15 @@ public final class PermissionUtil { new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSIONS_WRITE_EXTERNAL_STORAGE); } + + /** + * request camera permission. + * + * @param activity The target activity. + */ + public static void requestCameraPermission(Activity activity) { + ActivityCompat.requestPermissions(activity, + new String[]{Manifest.permission.CAMERA}, + PERMISSIONS_CAMERA); + } } diff --git a/src/main/res/drawable/ic_qrcode.xml b/src/main/res/drawable/ic_qrcode.xml new file mode 100644 index 0000000000..c957a1cfa0 --- /dev/null +++ b/src/main/res/drawable/ic_qrcode.xml @@ -0,0 +1,717 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/res/layout-land/account_setup.xml b/src/main/res/layout-land/account_setup.xml index 2854b07675..a3d375abab 100644 --- a/src/main/res/layout-land/account_setup.xml +++ b/src/main/res/layout-land/account_setup.xml @@ -296,4 +296,13 @@ app:cornerRadius="@dimen/button_corner_radius"/> + + diff --git a/src/main/res/layout/account_setup.xml b/src/main/res/layout/account_setup.xml index a990eb7f94..36ba2a1081 100644 --- a/src/main/res/layout/account_setup.xml +++ b/src/main/res/layout/account_setup.xml @@ -277,6 +277,15 @@ app:cornerRadius="@dimen/button_corner_radius"/> + + diff --git a/src/main/res/layout/deep_link_login.xml b/src/main/res/layout/deep_link_login.xml new file mode 100644 index 0000000000..3df2e40fb2 --- /dev/null +++ b/src/main/res/layout/deep_link_login.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 6a632691f5..55915d329a 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -849,4 +849,5 @@ Error starting camera source folder is read-only; file will only be uploaded kept in original folder, as it is readonly + Login via QR code diff --git a/src/test/java/com/owncloud/android/authentication/AuthenticatorDataUrlTest.java b/src/test/java/com/owncloud/android/authentication/AuthenticatorDataUrlTest.java index 58d88aa708..4a4d4dce8c 100644 --- a/src/test/java/com/owncloud/android/authentication/AuthenticatorDataUrlTest.java +++ b/src/test/java/com/owncloud/android/authentication/AuthenticatorDataUrlTest.java @@ -33,7 +33,7 @@ import org.junit.runners.BlockJUnit4ClassRunner; public class AuthenticatorDataUrlTest { private static final String URL_PARSING = " url parsing"; private static final String INCORRECT_USER_VALUE_IN = "Incorrect user value in "; - private String schemeUrl = "nextcloud://login/"; + private String schemeUrl = "nc://login/"; private String plus = "&"; private String userValue = "testuser123";