diff options
author | Marcos Zuriaga <wolfi@wolfi.es> | 2022-08-16 22:14:08 +0300 |
---|---|---|
committer | Marcos Zuriaga <wolfi@wolfi.es> | 2022-08-16 22:14:08 +0300 |
commit | ae462545e561fce71506bc78cdd23769c000d007 (patch) | |
tree | 7a484669fc8d5fd605c6433591b98cf1591512c1 | |
parent | 02f32bfd5a6e4fb5afd64eec9d128e03779d3715 (diff) | |
parent | 47cbed18475f1ec9b2cbb8e0882b5cbc26bc9e49 (diff) |
Merge branch 'binsky08-sso-3-backport'
29 files changed, 1196 insertions, 460 deletions
diff --git a/app/build.gradle b/app/build.gradle index d7116d7..3a55966 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -105,6 +105,9 @@ dependencies { androidTestImplementation('androidx.test.espresso:espresso-core:3.4.0', { exclude group: 'com.android.support', module: 'support-annotations' }) + // Nextcloud SSO + implementation "com.github.nextcloud:Android-SingleSignOn:0.5.6" + implementation 'androidx.appcompat:appcompat:1.4.1' implementation 'com.google.android.material:material:1.5.0' implementation 'com.jakewharton:butterknife:10.2.3' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 89b8eb1..50b0a58 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,6 +5,10 @@ <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> + <queries> + <package android:name="com.nextcloud.client" /> + <package android:name="com.nextcloud.android.beta" /> + </queries> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" diff --git a/app/src/main/java/es/wolfi/app/ResponseHandlers/AutofillCredentialSaveResponseHandler.java b/app/src/main/java/es/wolfi/app/ResponseHandlers/AutofillCredentialSaveResponseHandler.java index 0c139e9..da8b1d7 100644 --- a/app/src/main/java/es/wolfi/app/ResponseHandlers/AutofillCredentialSaveResponseHandler.java +++ b/app/src/main/java/es/wolfi/app/ResponseHandlers/AutofillCredentialSaveResponseHandler.java @@ -24,6 +24,8 @@ package es.wolfi.app.ResponseHandlers; import android.content.Context; import android.content.SharedPreferences; +import android.os.Handler; +import android.os.Looper; import android.preference.PreferenceManager; import android.util.Log; import android.widget.Toast; @@ -63,66 +65,71 @@ public class AutofillCredentialSaveResponseHandler extends AsyncHttpResponseHand @Override public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers, byte[] responseBody) { String result = new String(responseBody); - if (statusCode == 200) { - try { - JSONObject credentialObject = new JSONObject(result); - - if (credentialObject.has("credential_id") && credentialObject.getInt("vault_id") == vault.vault_id) { - Credential currentCredential = Credential.fromJSON(credentialObject, vault); - vault.addCredential(currentCredential); - ((HashMap<String, Vault>) ton.getExtra(SettingValues.VAULTS.toString())).put(vault.guid, vault); - Vault activeVault = (Vault) SingleTon.getTon().getExtra(SettingValues.ACTIVE_VAULT.toString()); - if (vault.guid.equals(activeVault.guid)) { - ton.addExtra(SettingValues.ACTIVE_VAULT.toString(), vault); + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + if (statusCode == 200) { + try { + JSONObject credentialObject = new JSONObject(result); + + if (credentialObject.has("credential_id") && credentialObject.getInt("vault_id") == vault.vault_id) { + Credential currentCredential = Credential.fromJSON(credentialObject, vault); + vault.addCredential(currentCredential); + ((HashMap<String, Vault>) ton.getExtra(SettingValues.VAULTS.toString())).put(vault.guid, vault); + Vault activeVault = (Vault) SingleTon.getTon().getExtra(SettingValues.ACTIVE_VAULT.toString()); + if (vault.guid.equals(activeVault.guid)) { + ton.addExtra(SettingValues.ACTIVE_VAULT.toString(), vault); + } + + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(baseContext); + Vault.updateAutofillVault(vault, settings); + + Toast.makeText(applicationContext, applicationContext.getString(R.string.successfully_saved), Toast.LENGTH_SHORT).show(); + Log.d(TAG, applicationContext.getString(R.string.successfully_saved)); + return; + } + } catch (JSONException e) { + e.printStackTrace(); } - SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(baseContext); - Vault.updateAutofillVault(vault, settings); - - Toast.makeText(applicationContext, applicationContext.getString(R.string.successfully_saved), Toast.LENGTH_SHORT).show(); - Log.d(TAG, applicationContext.getString(R.string.successfully_saved)); - return; + Toast.makeText(applicationContext, applicationContext.getString(R.string.error_occurred), Toast.LENGTH_SHORT).show(); + Log.d(TAG, "onSaveRequest(), failed to save: " + R.string.error_occurred); } - } catch (JSONException e) { - e.printStackTrace(); } - - Toast.makeText(applicationContext, applicationContext.getString(R.string.error_occurred), Toast.LENGTH_SHORT).show(); - Log.d(TAG, "onSaveRequest(), failed to save: " + R.string.error_occurred); - } + }); } @Override public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers, byte[] responseBody, Throwable error) { - String response = ""; - - if (responseBody != null && responseBody.length > 0) { - response = new String(responseBody); - } - - if (!response.equals("") && JSONUtils.isJSONObject(response)) { - try { - JSONObject o = new JSONObject(response); - if (o.has("message") && o.getString("message").equals("Current user is not logged in")) { + String response = new String(responseBody); + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + if (!response.equals("") && JSONUtils.isJSONObject(response)) { + try { + JSONObject o = new JSONObject(response); + if (o.has("message") && o.getString("message").equals("Current user is not logged in")) { + + Toast.makeText(applicationContext, o.getString("message"), Toast.LENGTH_LONG).show(); + Log.d(TAG, "onSaveRequest(), failed to save: " + o.getString("message")); + return; + } + } catch (JSONException e1) { + e1.printStackTrace(); + } + } - Toast.makeText(applicationContext, o.getString("message"), Toast.LENGTH_LONG).show(); - Log.d(TAG, "onSaveRequest(), failed to save: " + o.getString("message")); - return; + if (error != null && error.getMessage() != null && statusCode != 302) { + error.printStackTrace(); + Log.e("async http response", new String(responseBody)); + Toast.makeText(applicationContext, error.getMessage(), Toast.LENGTH_SHORT).show(); + Log.d(TAG, error.getMessage()); + } else { + Toast.makeText(applicationContext, applicationContext.getString(R.string.error_occurred), Toast.LENGTH_SHORT).show(); + Log.d(TAG, applicationContext.getString(R.string.error_occurred)); } - } catch (JSONException e1) { - e1.printStackTrace(); } - } - - if (error != null && error.getMessage() != null && statusCode != 302) { - error.printStackTrace(); - Log.e("async http response", response); - Toast.makeText(applicationContext, error.getMessage(), Toast.LENGTH_SHORT).show(); - Log.d(TAG, error.getMessage()); - } else { - Toast.makeText(applicationContext, applicationContext.getString(R.string.error_occurred), Toast.LENGTH_SHORT).show(); - Log.d(TAG, applicationContext.getString(R.string.error_occurred)); - } + }); } @Override diff --git a/app/src/main/java/es/wolfi/app/ResponseHandlers/CoreAPIGETResponseHandler.java b/app/src/main/java/es/wolfi/app/ResponseHandlers/CoreAPIGETResponseHandler.java index 31012ca..aa7e38f 100644 --- a/app/src/main/java/es/wolfi/app/ResponseHandlers/CoreAPIGETResponseHandler.java +++ b/app/src/main/java/es/wolfi/app/ResponseHandlers/CoreAPIGETResponseHandler.java @@ -22,6 +22,9 @@ package es.wolfi.app.ResponseHandlers; +import android.os.Handler; +import android.os.Looper; + import com.koushikdutta.async.future.FutureCallback; import com.loopj.android.http.AsyncHttpResponseHandler; @@ -43,34 +46,44 @@ public class CoreAPIGETResponseHandler extends AsyncHttpResponseHandler { @Override public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers, byte[] responseBody) { String result = new String(responseBody); - if (statusCode == 200) { - if (JSONUtils.isJSONObject(result)) { - try { - JSONObject o = new JSONObject(result); - if (o.has("message") && o.getString("message").equals("Current user is not logged in")) { - callback.onCompleted(new Exception("401"), null); - return; + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + if (statusCode == 200) { + if (JSONUtils.isJSONObject(result)) { + try { + JSONObject o = new JSONObject(result); + if (o.has("message") && o.getString("message").equals("Current user is not logged in")) { + callback.onCompleted(new Exception("401"), null); + return; + } + } catch (JSONException e1) { + e1.printStackTrace(); + } } - } catch (JSONException e1) { - e1.printStackTrace(); } + callback.onCompleted(null, result); } - } - callback.onCompleted(null, result); + }); } @Override public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers, byte[] responseBody, Throwable error) { - String errorMessage = error.getMessage(); - if (errorMessage == null) { - error.printStackTrace(); - errorMessage = "Unknown error"; - } - if (statusCode == 401) { - callback.onCompleted(new Exception("401"), null); - } else { - callback.onCompleted(new Exception(errorMessage), null); - } + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + String errorMessage = error.getMessage(); + if (errorMessage == null) { + error.printStackTrace(); + errorMessage = "Unknown error"; + } + if (statusCode == 401) { + callback.onCompleted(new Exception("401"), null); + } else { + callback.onCompleted(new Exception(errorMessage), null); + } + } + }); } @Override diff --git a/app/src/main/java/es/wolfi/app/ResponseHandlers/CredentialAddFileResponseHandler.java b/app/src/main/java/es/wolfi/app/ResponseHandlers/CredentialAddFileResponseHandler.java index c32e0a9..708aafd 100644 --- a/app/src/main/java/es/wolfi/app/ResponseHandlers/CredentialAddFileResponseHandler.java +++ b/app/src/main/java/es/wolfi/app/ResponseHandlers/CredentialAddFileResponseHandler.java @@ -23,6 +23,8 @@ package es.wolfi.app.ResponseHandlers; import android.app.ProgressDialog; +import android.os.Handler; +import android.os.Looper; import android.view.View; import android.widget.Toast; @@ -30,9 +32,9 @@ import com.loopj.android.http.AsyncHttpResponseHandler; import org.json.JSONObject; +import es.wolfi.app.passman.R; import es.wolfi.app.passman.adapters.CustomFieldEditAdapter; import es.wolfi.app.passman.adapters.FileEditAdapter; -import es.wolfi.app.passman.R; import es.wolfi.passman.API.CustomField; import es.wolfi.passman.API.File; import es.wolfi.utils.FileUtils; @@ -60,45 +62,55 @@ public class CredentialAddFileResponseHandler extends AsyncHttpResponseHandler { @Override public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers, byte[] responseBody) { String result = new String(responseBody); - if (statusCode == 200) { - try { - JSONObject fileObject = new JSONObject(result); - if (fileObject.has("message") && fileObject.getString("message").equals("Current user is not logged in")) { - throw new Exception(fileObject.getString("message")); - } - - fileObject.put("filename", fileName); - File file = new File(fileObject); - if (requestCode == FileUtils.activityRequestFileCode.credentialAddFile.ordinal()) { - fed.addFile(file); - fed.notifyDataSetChanged(); + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + if (statusCode == 200) { + try { + JSONObject fileObject = new JSONObject(result); + if (fileObject.has("message") && fileObject.getString("message").equals("Current user is not logged in")) { + throw new Exception(fileObject.getString("message")); + } + + fileObject.put("filename", fileName); + File file = new File(fileObject); + + if (requestCode == FileUtils.activityRequestFileCode.credentialAddFile.ordinal()) { + fed.addFile(file); + fed.notifyDataSetChanged(); + } + if (requestCode == FileUtils.activityRequestFileCode.credentialAddCustomFieldFile.ordinal()) { + CustomField cf = new CustomField(); + cf.setLabel("newLabel" + cfed.getItemCount() + 1); + cf.setSecret(false); + cf.setFieldType("file"); + cf.setJValue(file.getAsJSONObject()); + + cfed.addCustomField(cf); + cfed.notifyDataSetChanged(); + } + } catch (Exception e) { + e.printStackTrace(); + Toast.makeText(view.getContext(), e.getMessage() != null ? e.getMessage() : view.getContext().getString(R.string.error_occurred), Toast.LENGTH_LONG).show(); + } + } else { + Toast.makeText(view.getContext(), R.string.error_occurred, Toast.LENGTH_LONG).show(); } - if (requestCode == FileUtils.activityRequestFileCode.credentialAddCustomFieldFile.ordinal()) { - CustomField cf = new CustomField(); - cf.setLabel("newLabel" + cfed.getItemCount() + 1); - cf.setSecret(false); - cf.setFieldType("file"); - cf.setJValue(file.getAsJSONObject()); - - cfed.addCustomField(cf); - cfed.notifyDataSetChanged(); - } - } catch (Exception e) { - e.printStackTrace(); - Toast.makeText(view.getContext(), e.getMessage() != null ? e.getMessage() : view.getContext().getString(R.string.error_occurred), Toast.LENGTH_LONG).show(); + progress.dismiss(); } - } else { - Toast.makeText(view.getContext(), R.string.error_occurred, Toast.LENGTH_LONG).show(); - } - - progress.dismiss(); + }); } @Override public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers, byte[] responseBody, Throwable error) { error.printStackTrace(); - Toast.makeText(view.getContext(), R.string.error_occurred, Toast.LENGTH_LONG).show(); + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + Toast.makeText(view.getContext(), R.string.error_occurred, Toast.LENGTH_LONG).show(); + } + }); progress.dismiss(); } diff --git a/app/src/main/java/es/wolfi/app/ResponseHandlers/CredentialDeleteResponseHandler.java b/app/src/main/java/es/wolfi/app/ResponseHandlers/CredentialDeleteResponseHandler.java index a00f1f7..673c666 100644 --- a/app/src/main/java/es/wolfi/app/ResponseHandlers/CredentialDeleteResponseHandler.java +++ b/app/src/main/java/es/wolfi/app/ResponseHandlers/CredentialDeleteResponseHandler.java @@ -37,10 +37,10 @@ import org.json.JSONObject; import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; -import es.wolfi.app.passman.activities.PasswordListActivity; import es.wolfi.app.passman.R; import es.wolfi.app.passman.SettingValues; import es.wolfi.app.passman.SingleTon; +import es.wolfi.app.passman.activities.PasswordListActivity; import es.wolfi.passman.API.Credential; import es.wolfi.passman.API.Vault; import es.wolfi.utils.JSONUtils; @@ -73,7 +73,9 @@ public class CredentialDeleteResponseHandler extends AsyncHttpResponseHandler { if (credentialObject.has("credential_id") && credentialObject.getInt("vault_id") == v.vault_id) { Credential currentCredential = Credential.fromJSON(credentialObject, v); - Toast.makeText(view.getContext(), R.string.successfully_deleted, Toast.LENGTH_LONG).show(); + passwordListActivity.runOnUiThread(() -> { + Toast.makeText(view.getContext(), R.string.successfully_deleted, Toast.LENGTH_LONG).show(); + }); Objects.requireNonNull(passwordListActivity).deleteCredentialInCurrentLocalVaultList(currentCredential); Objects.requireNonNull(passwordListActivity).showLockVaultButton(); @@ -95,7 +97,9 @@ public class CredentialDeleteResponseHandler extends AsyncHttpResponseHandler { alreadySaving.set(false); progress.dismiss(); - Toast.makeText(view.getContext(), R.string.error_occurred, Toast.LENGTH_LONG).show(); + passwordListActivity.runOnUiThread(() -> { + Toast.makeText(view.getContext(), R.string.error_occurred, Toast.LENGTH_LONG).show(); + }); } @Override @@ -108,25 +112,28 @@ public class CredentialDeleteResponseHandler extends AsyncHttpResponseHandler { response = new String(responseBody); } - if (!response.equals("") && JSONUtils.isJSONObject(response)) { - try { - JSONObject o = new JSONObject(response); - if (o.has("message") && o.getString("message").equals("Current user is not logged in")) { - Toast.makeText(view.getContext(), o.getString("message"), Toast.LENGTH_LONG).show(); - return; + final String finalResponse = response; + passwordListActivity.runOnUiThread(() -> { + if (!finalResponse.equals("") && JSONUtils.isJSONObject(finalResponse)) { + try { + JSONObject o = new JSONObject(finalResponse); + if (o.has("message") && o.getString("message").equals("Current user is not logged in")) { + Toast.makeText(view.getContext(), o.getString("message"), Toast.LENGTH_LONG).show(); + return; + } + } catch (JSONException e1) { + e1.printStackTrace(); } - } catch (JSONException e1) { - e1.printStackTrace(); } - } - if (error != null && error.getMessage() != null && statusCode != 302) { - error.printStackTrace(); - Log.e("async http response", response); - Toast.makeText(view.getContext(), error.getMessage(), Toast.LENGTH_LONG).show(); - } else { - Toast.makeText(view.getContext(), R.string.error_occurred, Toast.LENGTH_LONG).show(); - } + if (error != null && error.getMessage() != null && statusCode != 302) { + error.printStackTrace(); + Log.e("async http response", new String(responseBody)); + Toast.makeText(view.getContext(), error.getMessage(), Toast.LENGTH_LONG).show(); + } else { + Toast.makeText(view.getContext(), R.string.error_occurred, Toast.LENGTH_LONG).show(); + } + }); } @Override diff --git a/app/src/main/java/es/wolfi/app/ResponseHandlers/CredentialSaveForNewVaultResponseHandler.java b/app/src/main/java/es/wolfi/app/ResponseHandlers/CredentialSaveForNewVaultResponseHandler.java index 71a6b3b..48d24c6 100644 --- a/app/src/main/java/es/wolfi/app/ResponseHandlers/CredentialSaveForNewVaultResponseHandler.java +++ b/app/src/main/java/es/wolfi/app/ResponseHandlers/CredentialSaveForNewVaultResponseHandler.java @@ -23,6 +23,8 @@ package es.wolfi.app.ResponseHandlers; import android.app.ProgressDialog; +import android.os.Handler; +import android.os.Looper; import android.util.Log; import android.view.View; import android.widget.Toast; @@ -68,45 +70,56 @@ public class CredentialSaveForNewVaultResponseHandler extends AsyncHttpResponseH @Override public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers, byte[] responseBody) { String result = new String(responseBody); - if (statusCode == 200) { - try { - JSONObject credentialObject = new JSONObject(result); - if (credentialObject.has("credential_id") && credentialObject.getInt("vault_id") == vault.vault_id) { - - AsyncHttpResponseHandler createInitialSharingKeysResponseHandler = new AsyncHttpResponseHandler() { - @Override - public void onSuccess(int statusCode, Header[] headers, byte[] responseBody) { - if (statusCode == 200) { - Toast.makeText(view.getContext(), R.string.successfully_saved, Toast.LENGTH_LONG).show(); - Objects.requireNonNull(passwordListActivity).addVaultToCurrentLocalVaultList(vault); - fragmentManager.popBackStack(); - } else { - Toast.makeText(view.getContext(), R.string.error_occurred, Toast.LENGTH_LONG).show(); - } - - alreadySaving.set(false); - progress.dismiss(); - } - - @Override - public void onFailure(int statusCode, Header[] headers, byte[] responseBody, Throwable error) { + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + if (statusCode == 200) { + try { + JSONObject credentialObject = new JSONObject(result); + if (credentialObject.has("credential_id") && credentialObject.getInt("vault_id") == vault.vault_id) { + + AsyncHttpResponseHandler createInitialSharingKeysResponseHandler = new AsyncHttpResponseHandler() { + @Override + public void onSuccess(int statusCode, Header[] headers, byte[] responseBody) { + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + if (statusCode == 200) { + Toast.makeText(view.getContext(), R.string.successfully_saved, Toast.LENGTH_LONG).show(); + Objects.requireNonNull(passwordListActivity).addVaultToCurrentLocalVaultList(vault); + fragmentManager.popBackStack(); + } else { + Toast.makeText(view.getContext(), R.string.error_occurred, Toast.LENGTH_LONG).show(); + } + + alreadySaving.set(false); + progress.dismiss(); + } + }); + } + + @Override + public void onFailure(int statusCode, Header[] headers, byte[] responseBody, Throwable error) { + + } + }; + + //create initial sharing keys + vault.updateSharingKeys(keyStrength, view.getContext(), createInitialSharingKeysResponseHandler); + + return; } - }; - - //create initial sharing keys - vault.updateSharingKeys(keyStrength, view.getContext(), createInitialSharingKeysResponseHandler); - - return; + } catch (JSONException e) { + e.printStackTrace(); + } } - } catch (JSONException e) { - e.printStackTrace(); - } - } - alreadySaving.set(false); - progress.dismiss(); - Toast.makeText(view.getContext(), R.string.error_occurred, Toast.LENGTH_LONG).show(); + alreadySaving.set(false); + progress.dismiss(); + Toast.makeText(view.getContext(), R.string.error_occurred, Toast.LENGTH_LONG).show(); + } + }); } @Override @@ -115,28 +128,33 @@ public class CredentialSaveForNewVaultResponseHandler extends AsyncHttpResponseH progress.dismiss(); String response = new String(responseBody); - if (!response.equals("") && JSONUtils.isJSONObject(response)) { - try { - JSONObject o = new JSONObject(response); - if (o.has("message") && o.getString("message").equals("Current user is not logged in")) { - Toast.makeText(view.getContext(), o.getString("message"), Toast.LENGTH_LONG).show(); - return; + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + if (!response.equals("") && JSONUtils.isJSONObject(response)) { + try { + JSONObject o = new JSONObject(response); + if (o.has("message") && o.getString("message").equals("Current user is not logged in")) { + Toast.makeText(view.getContext(), o.getString("message"), Toast.LENGTH_LONG).show(); + return; + } + } catch (JSONException e1) { + e1.printStackTrace(); + Toast.makeText(view.getContext(), + view.getContext().getString(R.string.error_occurred).concat(e1.getMessage() != null ? e1.getMessage() : ""), + Toast.LENGTH_LONG).show(); + } + } + + if (error != null && error.getMessage() != null && statusCode != 302) { + error.printStackTrace(); + Log.e("async http response", new String(responseBody)); + Toast.makeText(view.getContext(), view.getContext().getString(R.string.error_occurred).concat(error.getMessage()), Toast.LENGTH_LONG).show(); + } else { + Toast.makeText(view.getContext(), R.string.error_occurred, Toast.LENGTH_LONG).show(); } - } catch (JSONException e1) { - e1.printStackTrace(); - Toast.makeText(view.getContext(), - view.getContext().getString(R.string.error_occurred).concat(e1.getMessage() != null ? e1.getMessage() : ""), - Toast.LENGTH_LONG).show(); } - } - - if (error != null && error.getMessage() != null && statusCode != 302) { - error.printStackTrace(); - Log.e("async http response", new String(responseBody)); - Toast.makeText(view.getContext(), view.getContext().getString(R.string.error_occurred).concat(error.getMessage()), Toast.LENGTH_LONG).show(); - } else { - Toast.makeText(view.getContext(), R.string.error_occurred, Toast.LENGTH_LONG).show(); - } + }); } @Override diff --git a/app/src/main/java/es/wolfi/app/ResponseHandlers/CredentialSaveResponseHandler.java b/app/src/main/java/es/wolfi/app/ResponseHandlers/CredentialSaveResponseHandler.java index b93acc5..7559005 100644 --- a/app/src/main/java/es/wolfi/app/ResponseHandlers/CredentialSaveResponseHandler.java +++ b/app/src/main/java/es/wolfi/app/ResponseHandlers/CredentialSaveResponseHandler.java @@ -37,10 +37,10 @@ import org.json.JSONObject; import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; -import es.wolfi.app.passman.activities.PasswordListActivity; import es.wolfi.app.passman.R; import es.wolfi.app.passman.SettingValues; import es.wolfi.app.passman.SingleTon; +import es.wolfi.app.passman.activities.PasswordListActivity; import es.wolfi.passman.API.Credential; import es.wolfi.passman.API.Vault; import es.wolfi.utils.JSONUtils; @@ -75,7 +75,9 @@ public class CredentialSaveResponseHandler extends AsyncHttpResponseHandler { if (credentialObject.has("credential_id") && credentialObject.getInt("vault_id") == v.vault_id) { Credential currentCredential = Credential.fromJSON(credentialObject, v); - Toast.makeText(view.getContext(), R.string.successfully_saved, Toast.LENGTH_LONG).show(); + passwordListActivity.runOnUiThread(() -> { + Toast.makeText(view.getContext(), R.string.successfully_saved, Toast.LENGTH_LONG).show(); + }); if (updateCredential) { Objects.requireNonNull(passwordListActivity).editCredentialInCurrentLocalVaultList(currentCredential); @@ -95,7 +97,9 @@ public class CredentialSaveResponseHandler extends AsyncHttpResponseHandler { alreadySaving.set(false); progress.dismiss(); - Toast.makeText(view.getContext(), R.string.error_occurred, Toast.LENGTH_LONG).show(); + passwordListActivity.runOnUiThread(() -> { + Toast.makeText(view.getContext(), R.string.error_occurred, Toast.LENGTH_LONG).show(); + }); } @Override @@ -108,25 +112,28 @@ public class CredentialSaveResponseHandler extends AsyncHttpResponseHandler { response = new String(responseBody); } - if (!response.equals("") && JSONUtils.isJSONObject(response)) { - try { - JSONObject o = new JSONObject(response); - if (o.has("message") && o.getString("message").equals("Current user is not logged in")) { - Toast.makeText(view.getContext(), o.getString("message"), Toast.LENGTH_LONG).show(); - return; + final String finalResponse = response; + passwordListActivity.runOnUiThread(() -> { + if (!finalResponse.equals("") && JSONUtils.isJSONObject(finalResponse)) { + try { + JSONObject o = new JSONObject(finalResponse); + if (o.has("message") && o.getString("message").equals("Current user is not logged in")) { + Toast.makeText(view.getContext(), o.getString("message"), Toast.LENGTH_LONG).show(); + return; + } + } catch (JSONException e1) { + e1.printStackTrace(); } - } catch (JSONException e1) { - e1.printStackTrace(); } - } - if (error != null && error.getMessage() != null && statusCode != 302) { - error.printStackTrace(); - Log.e("async http response", response); - Toast.makeText(view.getContext(), error.getMessage(), Toast.LENGTH_LONG).show(); - } else { - Toast.makeText(view.getContext(), R.string.error_occurred, Toast.LENGTH_LONG).show(); - } + if (error != null && error.getMessage() != null && statusCode != 302) { + error.printStackTrace(); + Log.e("async http response", new String(responseBody)); + Toast.makeText(view.getContext(), error.getMessage(), Toast.LENGTH_LONG).show(); + } else { + Toast.makeText(view.getContext(), R.string.error_occurred, Toast.LENGTH_LONG).show(); + } + }); } @Override diff --git a/app/src/main/java/es/wolfi/app/ResponseHandlers/CustomFieldFileDeleteResponseHandler.java b/app/src/main/java/es/wolfi/app/ResponseHandlers/CustomFieldFileDeleteResponseHandler.java index 72c34d6..638912b 100644 --- a/app/src/main/java/es/wolfi/app/ResponseHandlers/CustomFieldFileDeleteResponseHandler.java +++ b/app/src/main/java/es/wolfi/app/ResponseHandlers/CustomFieldFileDeleteResponseHandler.java @@ -23,6 +23,8 @@ package es.wolfi.app.ResponseHandlers; import android.app.ProgressDialog; +import android.os.Handler; +import android.os.Looper; import android.widget.Toast; import com.loopj.android.http.AsyncHttpResponseHandler; @@ -53,7 +55,12 @@ public class CustomFieldFileDeleteResponseHandler extends AsyncHttpResponseHandl public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers, byte[] responseBody) { if (statusCode == 200) { mValues.remove(holder.mItem); - customFieldEditAdapter.notifyDataSetChanged(); + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + customFieldEditAdapter.notifyDataSetChanged(); + } + }); } progress.dismiss(); } @@ -61,7 +68,12 @@ public class CustomFieldFileDeleteResponseHandler extends AsyncHttpResponseHandl @Override public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers, byte[] responseBody, Throwable error) { error.printStackTrace(); - Toast.makeText(progress.getContext(), R.string.error_occurred, Toast.LENGTH_LONG).show(); + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + Toast.makeText(progress.getContext(), R.string.error_occurred, Toast.LENGTH_LONG).show(); + } + }); progress.dismiss(); } diff --git a/app/src/main/java/es/wolfi/app/ResponseHandlers/FileDeleteResponseHandler.java b/app/src/main/java/es/wolfi/app/ResponseHandlers/FileDeleteResponseHandler.java index e562112..eabea2e 100644 --- a/app/src/main/java/es/wolfi/app/ResponseHandlers/FileDeleteResponseHandler.java +++ b/app/src/main/java/es/wolfi/app/ResponseHandlers/FileDeleteResponseHandler.java @@ -23,6 +23,8 @@ package es.wolfi.app.ResponseHandlers; import android.app.ProgressDialog; +import android.os.Handler; +import android.os.Looper; import android.view.View; import android.widget.Toast; @@ -55,8 +57,13 @@ public class FileDeleteResponseHandler extends AsyncHttpResponseHandler { if (statusCode == 200) { mValues.remove(holder.mItem); - holder.mContentView.setTextColor(view.getResources().getColor(R.color.disabled)); - holder.deleteButton.setVisibility(View.INVISIBLE); + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + holder.mContentView.setTextColor(view.getResources().getColor(R.color.disabled)); + holder.deleteButton.setVisibility(View.INVISIBLE); + } + }); } progress.dismiss(); } @@ -64,7 +71,12 @@ public class FileDeleteResponseHandler extends AsyncHttpResponseHandler { @Override public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers, byte[] responseBody, Throwable error) { error.printStackTrace(); - Toast.makeText(view.getContext(), R.string.error_occurred, Toast.LENGTH_LONG).show(); + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + Toast.makeText(view.getContext(), R.string.error_occurred, Toast.LENGTH_LONG).show(); + } + }); progress.dismiss(); } diff --git a/app/src/main/java/es/wolfi/app/ResponseHandlers/VaultDeleteResponseHandler.java b/app/src/main/java/es/wolfi/app/ResponseHandlers/VaultDeleteResponseHandler.java index 17d5ae7..384c849 100644 --- a/app/src/main/java/es/wolfi/app/ResponseHandlers/VaultDeleteResponseHandler.java +++ b/app/src/main/java/es/wolfi/app/ResponseHandlers/VaultDeleteResponseHandler.java @@ -23,6 +23,8 @@ package es.wolfi.app.ResponseHandlers; import android.app.ProgressDialog; +import android.os.Handler; +import android.os.Looper; import android.util.Log; import android.view.View; import android.widget.Toast; @@ -67,32 +69,37 @@ public class VaultDeleteResponseHandler extends AsyncHttpResponseHandler { @Override public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers, byte[] responseBody) { String result = new String(responseBody); - if (statusCode == 200) { - try { - JSONObject responseObject = new JSONObject(result); - if (responseObject.has("ok") && responseObject.getBoolean("ok")) { - if (isDeleteVaultContentRequest) { - final AsyncHttpResponseHandler responseHandler = new VaultDeleteResponseHandler(alreadySaving, vault, false, progress, view, passwordListActivity, fragmentManager); - vault.delete(view.getContext(), responseHandler); - } else { - Toast.makeText(view.getContext(), R.string.successfully_deleted, Toast.LENGTH_LONG).show(); - - Objects.requireNonNull(passwordListActivity).deleteVaultInCurrentLocalVaultList(vault); - - alreadySaving.set(false); - progress.dismiss(); - fragmentManager.popBackStack(); + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + if (statusCode == 200) { + try { + JSONObject responseObject = new JSONObject(result); + if (responseObject.has("ok") && responseObject.getBoolean("ok")) { + if (isDeleteVaultContentRequest) { + final AsyncHttpResponseHandler responseHandler = new VaultDeleteResponseHandler(alreadySaving, vault, false, progress, view, passwordListActivity, fragmentManager); + vault.delete(view.getContext(), responseHandler); + } else { + Toast.makeText(view.getContext(), R.string.successfully_deleted, Toast.LENGTH_LONG).show(); + + Objects.requireNonNull(passwordListActivity).deleteVaultInCurrentLocalVaultList(vault); + + alreadySaving.set(false); + progress.dismiss(); + fragmentManager.popBackStack(); + } + return; + } + } catch (JSONException e) { + e.printStackTrace(); } - return; } - } catch (JSONException e) { - e.printStackTrace(); - } - } - alreadySaving.set(false); - progress.dismiss(); - Toast.makeText(view.getContext(), R.string.error_occurred, Toast.LENGTH_LONG).show(); + alreadySaving.set(false); + progress.dismiss(); + Toast.makeText(view.getContext(), R.string.error_occurred, Toast.LENGTH_LONG).show(); + } + }); } @Override @@ -105,25 +112,31 @@ public class VaultDeleteResponseHandler extends AsyncHttpResponseHandler { response = new String(responseBody); } - if (!response.equals("") && JSONUtils.isJSONObject(response)) { - try { - JSONObject o = new JSONObject(response); - if (o.has("message") && o.getString("message").equals("Current user is not logged in")) { - Toast.makeText(view.getContext(), o.getString("message"), Toast.LENGTH_LONG).show(); - return; + final String finalResponse = response; + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + if (!finalResponse.equals("") && JSONUtils.isJSONObject(finalResponse)) { + try { + JSONObject o = new JSONObject(finalResponse); + if (o.has("message") && o.getString("message").equals("Current user is not logged in")) { + Toast.makeText(view.getContext(), o.getString("message"), Toast.LENGTH_LONG).show(); + return; + } + } catch (JSONException e1) { + e1.printStackTrace(); + } } - } catch (JSONException e1) { - e1.printStackTrace(); - } - } - if (error != null && error.getMessage() != null && statusCode != 302) { - error.printStackTrace(); - Log.e("async http response", response); - Toast.makeText(view.getContext(), error.getMessage(), Toast.LENGTH_LONG).show(); - } else { - Toast.makeText(view.getContext(), R.string.error_occurred, Toast.LENGTH_LONG).show(); - } + if (error != null && error.getMessage() != null && statusCode != 302) { + error.printStackTrace(); + Log.e("async http response", finalResponse); + Toast.makeText(view.getContext(), error.getMessage(), Toast.LENGTH_LONG).show(); + } else { + Toast.makeText(view.getContext(), R.string.error_occurred, Toast.LENGTH_LONG).show(); + } + } + }); } @Override diff --git a/app/src/main/java/es/wolfi/app/ResponseHandlers/VaultSaveResponseHandler.java b/app/src/main/java/es/wolfi/app/ResponseHandlers/VaultSaveResponseHandler.java index a59e4c4..7c8ee0d 100644 --- a/app/src/main/java/es/wolfi/app/ResponseHandlers/VaultSaveResponseHandler.java +++ b/app/src/main/java/es/wolfi/app/ResponseHandlers/VaultSaveResponseHandler.java @@ -23,6 +23,8 @@ package es.wolfi.app.ResponseHandlers; import android.app.ProgressDialog; +import android.os.Handler; +import android.os.Looper; import android.util.Log; import android.view.View; import android.widget.Toast; @@ -70,57 +72,62 @@ public class VaultSaveResponseHandler extends AsyncHttpResponseHandler { @Override public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers, byte[] responseBody) { String result = new String(responseBody); - if (statusCode == 200) { - try { - if (updateVault) { - Vault localVaultInstance = Vault.getVaultByGuid(vault.guid); - if (localVaultInstance != null) { - localVaultInstance.setName(vault.getName()); - } - alreadySaving.set(false); - progress.dismiss(); - fragmentManager.popBackStack(); - return; - } else { - JSONObject vaultObject = new JSONObject(result); - Vault v = Vault.fromJSON(vaultObject); - if (vaultObject.has("vault_id") && vaultObject.has("name") && vaultObject.getString("name").equals(vault.getName())) { - v.setEncryptionKey(vault.getEncryptionKey()); - - Toast.makeText(view.getContext(), "Vault created", Toast.LENGTH_LONG).show(); - - //create test credential - Credential testCred = new Credential(); - testCred.setVault(v); - - testCred.setLabel(labelPrefixForFirstVaultConsistencyCredential + v.getName()); - testCred.setPassword("lorem ipsum"); - testCred.setOtp("{}"); - testCred.setTags(""); - testCred.setFavicon(""); - testCred.setUsername(""); - testCred.setEmail(""); - testCred.setUrl(""); - testCred.setDescription(""); - testCred.setFiles("[]"); - testCred.setCustomFields("[]"); - testCred.setCompromised(false); - testCred.setHidden(true); - - final AsyncHttpResponseHandler responseHandler = new CredentialSaveForNewVaultResponseHandler(alreadySaving, v, keyStrength, progress, view, passwordListActivity, fragmentManager); - testCred.save(view.getContext(), responseHandler); - - return; + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + if (statusCode == 200) { + try { + if (updateVault) { + Vault localVaultInstance = Vault.getVaultByGuid(vault.guid); + if (localVaultInstance != null) { + localVaultInstance.setName(vault.getName()); + } + alreadySaving.set(false); + progress.dismiss(); + fragmentManager.popBackStack(); + return; + } else { + JSONObject vaultObject = new JSONObject(result); + Vault v = Vault.fromJSON(vaultObject); + if (vaultObject.has("vault_id") && vaultObject.has("name") && vaultObject.getString("name").equals(vault.getName())) { + v.setEncryptionKey(vault.getEncryptionKey()); + + Toast.makeText(view.getContext(), "Vault created", Toast.LENGTH_LONG).show(); + + //create test credential + Credential testCred = new Credential(); + testCred.setVault(v); + + testCred.setLabel(labelPrefixForFirstVaultConsistencyCredential + v.getName()); + testCred.setPassword("lorem ipsum"); + testCred.setOtp("{}"); + testCred.setTags(""); + testCred.setFavicon(""); + testCred.setUsername(""); + testCred.setEmail(""); + testCred.setUrl(""); + testCred.setDescription(""); + testCred.setFiles("[]"); + testCred.setCustomFields("[]"); + testCred.setCompromised(false); + testCred.setHidden(true); + + final AsyncHttpResponseHandler responseHandler = new CredentialSaveForNewVaultResponseHandler(alreadySaving, v, keyStrength, progress, view, passwordListActivity, fragmentManager); + testCred.save(view.getContext(), responseHandler); + + return; + } + } + } catch (JSONException e) { + e.printStackTrace(); } } - } catch (JSONException e) { - e.printStackTrace(); - } - } - alreadySaving.set(false); - progress.dismiss(); - Toast.makeText(view.getContext(), R.string.error_occurred, Toast.LENGTH_LONG).show(); + alreadySaving.set(false); + progress.dismiss(); + Toast.makeText(view.getContext(), R.string.error_occurred, Toast.LENGTH_LONG).show(); + } + }); } @Override @@ -133,25 +140,31 @@ public class VaultSaveResponseHandler extends AsyncHttpResponseHandler { response = new String(responseBody); } - if (!response.equals("") && JSONUtils.isJSONObject(response)) { - try { - JSONObject o = new JSONObject(response); - if (o.has("message") && o.getString("message").equals("Current user is not logged in")) { - Toast.makeText(view.getContext(), o.getString("message"), Toast.LENGTH_LONG).show(); - return; + final String finalResponse = response; + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + if (!finalResponse.equals("") && JSONUtils.isJSONObject(finalResponse)) { + try { + JSONObject o = new JSONObject(finalResponse); + if (o.has("message") && o.getString("message").equals("Current user is not logged in")) { + Toast.makeText(view.getContext(), o.getString("message"), Toast.LENGTH_LONG).show(); + return; + } + } catch (JSONException e1) { + e1.printStackTrace(); + } } - } catch (JSONException e1) { - e1.printStackTrace(); - } - } - if (error != null && error.getMessage() != null && statusCode != 302) { - error.printStackTrace(); - Log.e("async http response", response); - Toast.makeText(view.getContext(), error.getMessage(), Toast.LENGTH_LONG).show(); - } else { - Toast.makeText(view.getContext(), R.string.error_occurred, Toast.LENGTH_LONG).show(); - } + if (error != null && error.getMessage() != null && statusCode != 302) { + error.printStackTrace(); + Log.e("async http response", finalResponse); + Toast.makeText(view.getContext(), error.getMessage(), Toast.LENGTH_LONG).show(); + } else { + Toast.makeText(view.getContext(), R.string.error_occurred, Toast.LENGTH_LONG).show(); + } + } + }); } @Override diff --git a/app/src/main/java/es/wolfi/app/passman/activities/LoginActivity.java b/app/src/main/java/es/wolfi/app/passman/activities/LoginActivity.java index c6f3ceb..e9a840a 100644 --- a/app/src/main/java/es/wolfi/app/passman/activities/LoginActivity.java +++ b/app/src/main/java/es/wolfi/app/passman/activities/LoginActivity.java @@ -20,12 +20,22 @@ */ package es.wolfi.app.passman.activities; +import android.content.Context; +import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.preference.PreferenceManager; import android.util.Log; +import android.view.View; +import android.view.inputmethod.InputMethodManager; import android.widget.Button; import android.widget.EditText; +import android.widget.ImageView; +import android.widget.LinearLayout; import android.widget.Spinner; import android.widget.Toast; @@ -33,16 +43,27 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; import com.koushikdutta.async.future.FutureCallback; +import com.nextcloud.android.sso.AccountImporter; +import com.nextcloud.android.sso.Constants; +import com.nextcloud.android.sso.exceptions.AccountImportCancelledException; +import com.nextcloud.android.sso.exceptions.AndroidGetAccountsPermissionNotGranted; +import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; +import com.nextcloud.android.sso.exceptions.NextcloudFilesAppNotInstalledException; +import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException; +import com.nextcloud.android.sso.helper.SingleAccountHelper; +import com.nextcloud.android.sso.model.SingleSignOnAccount; +import com.nextcloud.android.sso.ui.UiExceptionManager; import java.net.MalformedURLException; import java.net.URL; +import java.util.Arrays; +import java.util.List; import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnClick; import es.wolfi.app.passman.R; import es.wolfi.app.passman.SettingValues; -import es.wolfi.app.passman.SettingsCache; import es.wolfi.app.passman.SingleTon; import es.wolfi.passman.API.Core; import es.wolfi.utils.KeyStoreUtils; @@ -63,15 +84,42 @@ public class LoginActivity extends AppCompatActivity { SharedPreferences settings; SingleTon ton; + boolean isLegacyOnly = false; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + + Log.d("LoginActivity", "in onCreate"); + setContentView(R.layout.activity_login); ButterKnife.bind(this); - new SettingsCache().loadSharedPreferences(getBaseContext()); - settings = SettingsCache.getSharedPreferences(); + if (!isNextcloudFilesAppInstalled(this)) { + isLegacyOnly = true; + loadLegacyLogin(); + } + } + + @OnClick(R.id.load_legacy_login_button) + public void loadLegacyLogin() { + hideLoginOptions(); + + ImageView login_options_logo = findViewById(R.id.login_options_logo); + login_options_logo.setVisibility(View.INVISIBLE); + + LinearLayout content_legacy_login = findViewById(R.id.content_legacy_login); + content_legacy_login.setVisibility(View.VISIBLE); + + EditText hostForm = findViewById(R.id.host); + hostForm.requestFocus(); + + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); + } + + settings = PreferenceManager.getDefaultSharedPreferences(this); KeyStoreUtils.initialize(settings); ton = SingleTon.getTon(); @@ -98,6 +146,58 @@ public class LoginActivity extends AppCompatActivity { setSupportActionBar(toolbar); } + @OnClick(R.id.load_sso_login_button) + public void loadSSOLogin() { + hideLoginOptions(); + + try { + AccountImporter.pickNewAccount(this); + Log.w("SSO@LoginActivity", "try AccountImporter was successful"); + } catch (NextcloudFilesAppNotInstalledException e1) { + UiExceptionManager.showDialogForException(this, e1); + Log.w("SSO@LoginActivity", "Nextcloud app is not installed. Cannot choose account. Use legacy login method."); + + loadLegacyLogin(); + } catch (AndroidGetAccountsPermissionNotGranted e2) { + AccountImporter.requestAndroidAccountPermissionsAndPickAccount(this); + } + } + + private void showLoginOptions() { + LinearLayout content_legacy_login = findViewById(R.id.content_legacy_login); + content_legacy_login.setVisibility(View.INVISIBLE); + + LinearLayout login_options = findViewById(R.id.login_options); + login_options.setVisibility(View.VISIBLE); + ImageView login_options_logo = findViewById(R.id.login_options_logo); + login_options_logo.setVisibility(View.VISIBLE); + } + + private void hideLoginOptions() { + LinearLayout login_options = findViewById(R.id.login_options); + login_options.setVisibility(View.INVISIBLE); + } + + private static boolean isNextcloudFilesAppInstalled(Context context) { + List<String> APPS = Arrays.asList(Constants.PACKAGE_NAME_PROD, Constants.PACKAGE_NAME_DEV); + + boolean returnValue = false; + PackageManager pm = context.getPackageManager(); + for (String app : APPS) { + try { + PackageInfo pi = pm.getPackageInfo(app, PackageManager.GET_ACTIVITIES); + // check if Nextcloud Files App version with the required PATCH request fix is installed + if ((pi.versionCode >= 30180052 && pi.packageName.equals("com.nextcloud.client")) || + pi.versionCode >= 20211027 && pi.packageName.equals("com.nextcloud.android.beta")) { + returnValue = true; + break; + } + } catch (PackageManager.NameNotFoundException ignored) { + } + } + return returnValue; + } + @OnClick(R.id.next) public void onNextClick() { Log.e("Login", "begin"); @@ -129,4 +229,78 @@ public class LoginActivity extends AppCompatActivity { } }); } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this); + Context c = this; + try { + AccountImporter.onActivityResult(requestCode, resultCode, data, this, new AccountImporter.IAccountAccessGranted() { + + @Override + public void accountAccessGranted(SingleSignOnAccount account) { + Context l_context = getApplicationContext(); + + // As this library supports multiple accounts we created some helper methods if you only want to use one. + // The following line stores the selected account as the "default" account which can be queried by using + // the SingleAccountHelper.getCurrentSingleSignOnAccount(context) method + SingleAccountHelper.setCurrentAccount(l_context, account.name); + + // Get the "default" account + SingleSignOnAccount ssoAccount; + try { + ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(l_context); + } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) { + UiExceptionManager.showDialogForException(l_context, e); + return; + } + + SingleTon ton = SingleTon.getTon(); + ton.addString(SettingValues.HOST.toString(), ssoAccount.url); + ton.addString(SettingValues.USER.toString(), ssoAccount.userId); + ton.addString(SettingValues.PASSWORD.toString(), ssoAccount.token); + + SingleSignOnAccount finalSsoAccount = ssoAccount; + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + Core.checkLogin(c, true, new FutureCallback<Boolean>() { + @Override + public void onCompleted(Exception e, Boolean result) { + if (result) { + settings.edit() + .putString(SettingValues.HOST.toString(), finalSsoAccount.url) + .putString(SettingValues.USER.toString(), finalSsoAccount.userId) + .putString(SettingValues.PASSWORD.toString(), finalSsoAccount.token) + .apply(); + + setResult(RESULT_OK); + LoginActivity.this.finish(); + } else { + ton.removeString(SettingValues.HOST.toString()); + ton.removeString(SettingValues.USER.toString()); + ton.removeString(SettingValues.PASSWORD.toString()); + } + } + }); + } + }); + } + }); + } catch (AccountImportCancelledException e) { + showLoginOptions(); + } + } + + @Override + public void onBackPressed() { + LinearLayout login_options = findViewById(R.id.login_options); + if (login_options.getVisibility() == View.INVISIBLE && !isLegacyOnly) { + showLoginOptions(); + } else { + PasswordListActivity.running = false; + super.onBackPressed(); + } + } } diff --git a/app/src/main/java/es/wolfi/app/passman/activities/PasswordListActivity.java b/app/src/main/java/es/wolfi/app/passman/activities/PasswordListActivity.java index d74039b..61bad4b 100644 --- a/app/src/main/java/es/wolfi/app/passman/activities/PasswordListActivity.java +++ b/app/src/main/java/es/wolfi/app/passman/activities/PasswordListActivity.java @@ -262,29 +262,32 @@ public class PasswordListActivity extends AppCompatActivity implements .commitAllowingStateLoss(); Log.d("PL", "committed transaction"); } else { - final ProgressDialog progress = ProgressUtils.showLoadingSequence(this); - progress.show(); - Vault.getVaults(this, (e, result) -> { - progress.dismiss(); - if (e != null) { - // Not logged in, restart activity - if (Objects.equals(e.getMessage(), "401")) { - recreate(); - } + this.runOnUiThread(() -> { + final ProgressDialog progress = ProgressUtils.showLoadingSequence(this); + progress.show(); - Toast.makeText(getApplicationContext(), getString(R.string.net_error), Toast.LENGTH_LONG).show(); - - new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { - @Override - public void run() { - showVaults(); + Vault.getVaults(this, (e, result) -> { + progress.dismiss(); + if (e != null) { + // Not logged in, restart activity + if (Objects.equals(e.getMessage(), "401")) { + recreate(); } - }, 30000); - return; - } - ton.addExtra(SettingValues.VAULTS.toString(), result); - showVaults(); + Toast.makeText(getApplicationContext(), getString(R.string.net_error), Toast.LENGTH_LONG).show(); + + new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { + @Override + public void run() { + showVaults(); + } + }, 30000); + return; + } + + ton.addExtra(SettingValues.VAULTS.toString(), result); + showVaults(); + }); }); } } @@ -309,6 +312,7 @@ public class PasswordListActivity extends AppCompatActivity implements } progress.dismiss(); } else { + final AppCompatActivity self = this; Vault.getVault(this, vault.guid, new FutureCallback<Vault>() { @Override public void onCompleted(Exception e, Vault result) { @@ -338,9 +342,10 @@ public class PasswordListActivity extends AppCompatActivity implements } ton.addExtra(SettingValues.ACTIVE_VAULT.toString(), result); - showActiveVault(); - - Vault.updateAutofillVault(result, settings); + self.runOnUiThread(() -> { + showActiveVault(); + Vault.updateAutofillVault(result, settings); + }); } }); } @@ -521,7 +526,12 @@ public class PasswordListActivity extends AppCompatActivity implements CredentialItemFragment credentialItems = (CredentialItemFragment) getSupportFragmentManager().findFragmentById(R.id.content_password_list); assert credentialItems != null; - credentialItems.loadCredentialList(findViewById(R.id.content_password_list)); + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + credentialItems.loadCredentialList(findViewById(R.id.content_password_list)); + } + }); } } }); @@ -636,7 +646,7 @@ public class PasswordListActivity extends AppCompatActivity implements } public void showLockVaultButton() { - this.VaultLockButton.setVisibility(View.VISIBLE); + this.runOnUiThread(() -> this.VaultLockButton.setVisibility(View.VISIBLE)); } private void showNotImplementedMessage() { @@ -700,7 +710,7 @@ public class PasswordListActivity extends AppCompatActivity implements try { JSONObject o = new JSONObject(result); if (o.has("file_data")) { - progress.setMessage(getString(R.string.wait_while_decrypting)); + runOnUiThread(() -> progress.setMessage(getString(R.string.wait_while_decrypting))); String[] decryptedSplitString = v.decryptString(o.getString("file_data")).split(","); if (decryptedSplitString.length == 2) { Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); diff --git a/app/src/main/java/es/wolfi/app/passman/fragments/CredentialAddFragment.java b/app/src/main/java/es/wolfi/app/passman/fragments/CredentialAddFragment.java index d97f69a..3fd454c 100644 --- a/app/src/main/java/es/wolfi/app/passman/fragments/CredentialAddFragment.java +++ b/app/src/main/java/es/wolfi/app/passman/fragments/CredentialAddFragment.java @@ -26,6 +26,7 @@ import android.content.Context; import android.os.Bundle; import android.os.Handler; import android.os.Looper; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; diff --git a/app/src/main/java/es/wolfi/app/passman/fragments/SettingsFragment.java b/app/src/main/java/es/wolfi/app/passman/fragments/SettingsFragment.java index 29fd1da..45bb1b6 100644 --- a/app/src/main/java/es/wolfi/app/passman/fragments/SettingsFragment.java +++ b/app/src/main/java/es/wolfi/app/passman/fragments/SettingsFragment.java @@ -3,6 +3,7 @@ * * @copyright Copyright (c) 2016, Sander Brand (brantje@gmail.com) * @copyright Copyright (c) 2016, Marcos Zuriaga Miguel (wolfi@wolfi.es) + * @copyright Copyright (c) 2022, Timo Triebensky (timo@binsky.org) * @license GNU AGPL version 3 or any later version * <p> * This program is free software: you can redistribute it and/or modify @@ -22,11 +23,14 @@ package es.wolfi.app.passman.fragments; import android.annotation.SuppressLint; +import android.app.AlertDialog; import android.content.Context; +import android.content.DialogInterface; import android.content.SharedPreferences; import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -34,6 +38,7 @@ import android.view.ViewManager; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.EditText; +import android.widget.RelativeLayout; import android.widget.Spinner; import android.widget.TextView; @@ -43,7 +48,14 @@ import androidx.fragment.app.Fragment; import com.google.android.material.checkbox.MaterialCheckBox; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.koushikdutta.async.future.FutureCallback; - +import com.nextcloud.android.sso.AccountImporter; +import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; +import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException; +import com.nextcloud.android.sso.helper.SingleAccountHelper; +import com.nextcloud.android.sso.model.SingleSignOnAccount; + +import java.net.MalformedURLException; +import java.net.URL; import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -63,6 +75,11 @@ import es.wolfi.utils.PasswordGenerator; public class SettingsFragment extends Fragment { + RelativeLayout manual_server_connection_settings; + RelativeLayout sso_settings; + + TextView sso_user_server; + EditText settings_nextcloud_url; EditText settings_nextcloud_user; EditText settings_nextcloud_password; @@ -90,6 +107,7 @@ public class SettingsFragment extends Fragment { Button clear_offline_cache_button; SharedPreferences settings; + SingleSignOnAccount ssoAccount; PasswordGenerator passwordGenerator; public SettingsFragment() { @@ -102,9 +120,7 @@ public class SettingsFragment extends Fragment { * @return A new instance of fragment SettingsFragment. */ public static SettingsFragment newInstance() { - SettingsFragment fragment = new SettingsFragment(); - - return fragment; + return new SettingsFragment(); } @Override @@ -113,9 +129,17 @@ public class SettingsFragment extends Fragment { // Inflate the layout for this fragment View view = inflater.inflate(R.layout.fragment_settings, container, false); + manual_server_connection_settings = view.findViewById(R.id.manual_server_connection_settings); + sso_settings = view.findViewById(R.id.sso_settings); + FloatingActionButton settingsSaveButton = view.findViewById(R.id.settings_save_button); settingsSaveButton.setOnClickListener(this.getSaveButtonListener()); + Button sso_user_server_logout_button = view.findViewById(R.id.sso_user_server_logout_button); + sso_user_server_logout_button.setOnClickListener(this.getSSOLogoutButtonListener()); + + sso_user_server = view.findViewById(R.id.sso_user_server); + settings_nextcloud_url = view.findViewById(R.id.settings_nextcloud_url); settings_nextcloud_user = view.findViewById(R.id.settings_nextcloud_user); settings_nextcloud_password = view.findViewById(R.id.settings_nextcloud_password); @@ -156,6 +180,24 @@ public class SettingsFragment extends Fragment { super.onViewCreated(view, savedInstanceState); ButterKnife.bind(this, view); + try { + ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(getContext()); + manual_server_connection_settings.removeAllViews(); + sso_settings.setVisibility(View.VISIBLE); + + String hostname = ""; + try { + URL uri = new URL(ssoAccount.url); + hostname = uri.getHost(); + } catch (MalformedURLException e) { + Log.d("SettingsFragment", "Error parsing host from sso account"); + } + sso_user_server.setText(String.format("%s@%s", ssoAccount.userId, hostname)); + } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) { + manual_server_connection_settings.setVisibility(View.VISIBLE); + sso_settings.removeAllViews(); + } + settings_nextcloud_url.setText(KeyStoreUtils.getString(SettingValues.HOST.toString(), null)); settings_nextcloud_user.setText(KeyStoreUtils.getString(SettingValues.USER.toString(), null)); settings_nextcloud_password.setText(KeyStoreUtils.getString(SettingValues.PASSWORD.toString(), null)); @@ -230,6 +272,33 @@ public class SettingsFragment extends Fragment { super.onDetach(); } + public View.OnClickListener getSSOLogoutButtonListener() { + return new View.OnClickListener() { + @Override + public void onClick(View view) { + AlertDialog.Builder builder = new AlertDialog.Builder(view.getContext()); + builder.setMessage(R.string.confirm_account_logout); + builder.setCancelable(false); + builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + AccountImporter.clearAllAuthTokens(getContext()); + SingleAccountHelper.setCurrentAccount(getContext(), null); + + settings.edit().remove(SettingValues.HOST.toString()).commit(); + settings.edit().remove(SettingValues.USER.toString()).commit(); + settings.edit().remove(SettingValues.PASSWORD.toString()).commit(); + + dialogInterface.dismiss(); + PasswordListActivity.triggerRebirth(Objects.requireNonNull(((PasswordListActivity) getActivity()))); + } + }); + builder.setNegativeButton(R.string.cancel, null); + builder.show(); + } + }; + } + public View.OnClickListener getSaveButtonListener() { return new View.OnClickListener() { @SuppressLint("ApplySharedPref") @@ -290,9 +359,9 @@ public class SettingsFragment extends Fragment { } SettingsCache.clear(); - if (!KeyStoreUtils.getString(SettingValues.HOST.toString(), null).equals(settings_nextcloud_url.getText().toString()) || + if (ssoAccount == null && (!KeyStoreUtils.getString(SettingValues.HOST.toString(), null).equals(settings_nextcloud_url.getText().toString()) || !KeyStoreUtils.getString(SettingValues.USER.toString(), null).equals(settings_nextcloud_user.getText().toString()) || - !KeyStoreUtils.getString(SettingValues.PASSWORD.toString(), null).equals(settings_nextcloud_password.getText().toString())) { + !KeyStoreUtils.getString(SettingValues.PASSWORD.toString(), null).equals(settings_nextcloud_password.getText().toString()))) { ton.removeString(SettingValues.HOST.toString()); ton.removeString(SettingValues.USER.toString()); ton.removeString(SettingValues.PASSWORD.toString()); diff --git a/app/src/main/java/es/wolfi/passman/API/Core.java b/app/src/main/java/es/wolfi/passman/API/Core.java index 5acf020..01572e0 100644 --- a/app/src/main/java/es/wolfi/passman/API/Core.java +++ b/app/src/main/java/es/wolfi/passman/API/Core.java @@ -3,6 +3,7 @@ * * @copyright Copyright (c) 2016, Sander Brand (brantje@gmail.com) * @copyright Copyright (c) 2016, Marcos Zuriaga Miguel (wolfi@wolfi.es) + * @copyright Copyright (c) 2022, Timo Triebensky (timo@binsky.org) * @license GNU AGPL version 3 or any later version * <p> * This program is free software: you can redistribute it and/or modify @@ -23,21 +24,46 @@ package es.wolfi.passman.API; import android.app.AlertDialog; import android.content.Context; +import android.net.Uri; +import android.os.AsyncTask; import android.util.Log; import android.view.View; import android.widget.Toast; +import androidx.annotation.NonNull; + +import com.google.gson.GsonBuilder; import com.koushikdutta.async.future.FutureCallback; import com.loopj.android.http.AsyncHttpClient; import com.loopj.android.http.AsyncHttpResponseHandler; +import com.nextcloud.android.sso.aidl.NextcloudRequest; +import com.nextcloud.android.sso.api.AidlNetworkRequest; +import com.nextcloud.android.sso.api.NextcloudAPI; +import com.nextcloud.android.sso.api.Response; +import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; +import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException; +import com.nextcloud.android.sso.helper.SingleAccountHelper; +import com.nextcloud.android.sso.model.SingleSignOnAccount; import org.json.JSONException; import org.json.JSONObject; +import java.io.BufferedReader; +import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; - +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import cz.msebera.android.httpclient.Header; +import cz.msebera.android.httpclient.HeaderElement; +import cz.msebera.android.httpclient.ParseException; import cz.msebera.android.httpclient.entity.StringEntity; import es.wolfi.app.ResponseHandlers.CoreAPIGETResponseHandler; import es.wolfi.app.passman.OfflineStorage; @@ -52,18 +78,27 @@ public abstract class Core { protected static final String LOG_TAG = "API_LIB"; protected static final String JSON_CONTENT_TYPE = "application/json"; + protected static SingleSignOnAccount ssoAccount; protected static String host; protected static String host_internal; protected static String username; protected static String password; protected static String version_name; + protected static String API_URL = "/index.php/apps/passman/api/v2/"; + protected static String API_URL_INTERNAL = "/index.php/apps/passman/api/internal/"; protected static int version_number = 0; - public static void setUpAPI(String host, String username, String password) { + public static void setUpAPI(Context c, String host, String username, String password) { Core.setAPIHost(host); Core.username = username; Core.password = password; + + try { + Core.ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(c); + } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) { + e.printStackTrace(); + } } public static String getAPIHost() { @@ -71,8 +106,8 @@ public abstract class Core { } public static void setAPIHost(String host) { - Core.host = host.concat("/index.php/apps/passman/api/v2/"); - Core.host_internal = host.concat("/index.php/apps/passman/api/internal/"); + Core.host = host.concat(API_URL); + Core.host_internal = host.concat(API_URL_INTERNAL); } public static String getAPIUsername() { @@ -105,64 +140,99 @@ public abstract class Core { public static void requestInternalAPIGET(Context c, String endpoint, final FutureCallback<String> callback) { final AsyncHttpResponseHandler responseHandler = new CoreAPIGETResponseHandler(callback); - AsyncHttpClient client = new AsyncHttpClient(); - client.setBasicAuth(username, password); - client.setConnectTimeout(getConnectTimeout(c)); - client.setResponseTimeout(getResponseTimeout(c)); - client.setMaxRetriesAndTimeout(getConnectRetries(c), getConnectTimeout(c)); - client.addHeader("Content-Type", JSON_CONTENT_TYPE); - - try { - client.get(host_internal.concat(endpoint), responseHandler); - } catch (Exception e) { - responseHandler.onFailure(0, null, null, e); + if (ssoAccount != null) { + final Map<String, List<String>> header = new HashMap<>(); + header.put("Content-Type", Collections.singletonList(JSON_CONTENT_TYPE)); + + NextcloudRequest nextcloudRequest = new NextcloudRequest.Builder() + .setMethod("GET") + .setUrl(Uri.encode(API_URL_INTERNAL.concat(endpoint), "/")) + .build(); + new SyncedRequestTask(nextcloudRequest, ssoAccount, responseHandler, c).execute(); + } else { + AsyncHttpClient client = new AsyncHttpClient(); + client.setBasicAuth(username, password); + client.setConnectTimeout(getConnectTimeout(c)); + client.setResponseTimeout(getResponseTimeout(c)); + client.setMaxRetriesAndTimeout(getConnectRetries(c), getConnectTimeout(c)); + client.addHeader("Content-Type", JSON_CONTENT_TYPE); + + try { + client.get(host_internal.concat(endpoint), responseHandler); + } catch (Exception e) { + responseHandler.onFailure(0, null, null, e); + } } } public static void requestAPIGET(Context c, String endpoint, final FutureCallback<String> callback) { final AsyncHttpResponseHandler responseHandler = new CoreAPIGETResponseHandler(callback); - AsyncHttpClient client = new AsyncHttpClient(); - client.setBasicAuth(username, password); - client.setConnectTimeout(getConnectTimeout(c)); - client.setResponseTimeout(getResponseTimeout(c)); - client.setMaxRetriesAndTimeout(getConnectRetries(c), getConnectTimeout(c)); - client.addHeader("Content-Type", JSON_CONTENT_TYPE); - - try { - client.get(host.concat(endpoint), responseHandler); - } catch (Exception e) { - responseHandler.onFailure(0, null, null, e); + if (ssoAccount != null) { + final Map<String, List<String>> header = new HashMap<>(); + header.put("Content-Type", Collections.singletonList(JSON_CONTENT_TYPE)); + + NextcloudRequest nextcloudRequest = new NextcloudRequest.Builder() + .setMethod("GET") + .setUrl(Uri.encode(API_URL.concat(endpoint), "/")) + .build(); + new SyncedRequestTask(nextcloudRequest, ssoAccount, responseHandler, c).execute(); + } else { + AsyncHttpClient client = new AsyncHttpClient(); + client.setBasicAuth(username, password); + client.setConnectTimeout(getConnectTimeout(c)); + client.setResponseTimeout(getResponseTimeout(c)); + client.setMaxRetriesAndTimeout(getConnectRetries(c), getConnectTimeout(c)); + client.addHeader("Content-Type", JSON_CONTENT_TYPE); + + try { + client.get(host.concat(endpoint), responseHandler); + } catch (Exception e) { + responseHandler.onFailure(0, null, null, e); + } } } public static void requestAPI(Context c, String endpoint, JSONObject jsonPostData, String requestType, final AsyncHttpResponseHandler responseHandler) throws MalformedURLException, UnsupportedEncodingException { - URL url = new URL(host.concat(endpoint)); - - AsyncHttpClient client = new AsyncHttpClient(); - client.setBasicAuth(username, password); - client.setConnectTimeout(getConnectTimeout(c)); - client.setResponseTimeout(getResponseTimeout(c)); - client.setMaxRetriesAndTimeout(getConnectRetries(c), getConnectTimeout(c)); - client.addHeader("Accept", "application/json, text/plain, */*"); - - StringEntity entity = new StringEntity(jsonPostData.toString()); - - try { - if (requestType.equals("POST")) { - client.post(c, url.toString(), entity, JSON_CONTENT_TYPE, responseHandler); - } else if (requestType.equals("PATCH")) { - client.patch(c, url.toString(), entity, JSON_CONTENT_TYPE, responseHandler); - } else if (requestType.equals("DELETE")) { - client.delete(c, url.toString(), entity, JSON_CONTENT_TYPE, responseHandler); + if (ssoAccount != null) { + final Map<String, List<String>> header = new HashMap<>(); + header.put("Accept", Collections.singletonList("application/json, text/plain, */*")); + //header.put("Content-Type", Collections.singletonList(JSON_CONTENT_TYPE)); + + NextcloudRequest nextcloudRequest = new NextcloudRequest.Builder() + .setMethod(requestType) + .setUrl(Uri.encode(API_URL.concat(endpoint), "/")) + .setRequestBody(jsonPostData.toString()) + .setHeader(header) + .build(); + + new SyncedRequestTask(nextcloudRequest, ssoAccount, responseHandler, c).execute(); + } else { + URL url = new URL(host.concat(endpoint)); + AsyncHttpClient client = new AsyncHttpClient(); + client.setBasicAuth(username, password); + client.setConnectTimeout(getConnectTimeout(c)); + client.setResponseTimeout(getResponseTimeout(c)); + client.setMaxRetriesAndTimeout(getConnectRetries(c), getConnectTimeout(c)); + client.addHeader("Accept", "application/json, text/plain, */*"); + + StringEntity entity = new StringEntity(jsonPostData.toString()); + + try { + if (requestType.equals("POST")) { + client.post(c, url.toString(), entity, JSON_CONTENT_TYPE, responseHandler); + } else if (requestType.equals("PATCH")) { + client.patch(c, url.toString(), entity, JSON_CONTENT_TYPE, responseHandler); + } else if (requestType.equals("DELETE")) { + client.delete(c, url.toString(), entity, JSON_CONTENT_TYPE, responseHandler); + } + } catch (Exception e) { + responseHandler.onFailure(0, null, null, e); } - } catch (Exception e) { - responseHandler.onFailure(0, null, null, e); } } - // TODO Test this method once the server response works! public static void getAPIVersion(final Context c, FutureCallback<String> cb) { if (version_name != null) { cb.onCompleted(null, version_name); @@ -264,7 +334,7 @@ public abstract class Core { //Log.d(LOG_TAG, "Pass: " + pass); Log.d(LOG_TAG, "Pass: " + pass.replaceAll("(?s).", "*")); - setUpAPI(host, user, pass); + setUpAPI(c, host, user, pass); getAPIVersion(c, new FutureCallback<String>() { @Override public void onCompleted(Exception e, String result) { @@ -294,4 +364,106 @@ public abstract class Core { } }); } + + private static class NCHeader implements Header { + String name, value; + + public NCHeader(String name, String value) { + this.name = name; + this.value = value; + } + + public NCHeader(ArrayList<AidlNetworkRequest.PlainHeader> plainHeaders, cz.msebera.android.httpclient.Header[] headers) { + Iterator<AidlNetworkRequest.PlainHeader> it = plainHeaders.iterator(); + for (int i = 0; it.hasNext(); i++) { + AidlNetworkRequest.PlainHeader plainHeader = it.next(); + headers[i] = new NCHeader(plainHeader.getName(), plainHeader.getValue()); + } + } + + @Override + public HeaderElement[] getElements() throws ParseException { + return new HeaderElement[0]; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getValue() { + return value; + } + } + + private static class SyncedRequestTask extends AsyncTask<Void, Void, Boolean> { + private final NextcloudRequest nextcloudRequest; + private final NextcloudAPI mNextcloudAPI; + private final AsyncHttpResponseHandler responseHandler; + private final FutureCallback<String> callback; + + SyncedRequestTask(@NonNull NextcloudRequest nextcloudRequest, @NonNull SingleSignOnAccount ssoAccount, final AsyncHttpResponseHandler responseHandler, Context c) { + this.nextcloudRequest = nextcloudRequest; + this.mNextcloudAPI = new NextcloudAPI(c.getApplicationContext(), ssoAccount, new GsonBuilder().create(), apiCallback); + this.responseHandler = responseHandler; + this.callback = null; + Log.d("SyncedRequestTask", ssoAccount.name + " → " + nextcloudRequest.getMethod() + " " + nextcloudRequest.getUrl() + " "); + } + + SyncedRequestTask(@NonNull NextcloudRequest nextcloudRequest, @NonNull SingleSignOnAccount ssoAccount, final FutureCallback<String> callback, Context c) { + this.nextcloudRequest = nextcloudRequest; + this.mNextcloudAPI = new NextcloudAPI(c.getApplicationContext(), ssoAccount, new GsonBuilder().create(), apiCallback); + this.responseHandler = null; + this.callback = callback; + Log.d("SyncedRequestTask", ssoAccount.name + " → " + nextcloudRequest.getMethod() + " " + nextcloudRequest.getUrl() + " "); + } + + @Override + protected Boolean doInBackground(Void... voids) { + try { + Response response = mNextcloudAPI.performNetworkRequestV2(nextcloudRequest); + + StringBuilder textBuilder = new StringBuilder(); + final BufferedReader rd = new BufferedReader(new InputStreamReader(response.getBody())); + String line; + while ((line = rd.readLine()) != null) { + textBuilder.append(line); + } + response.getBody().close(); + + cz.msebera.android.httpclient.Header[] headers = new cz.msebera.android.httpclient.Header[response.getPlainHeaders().size()]; + new NCHeader(response.getPlainHeaders(), headers); + + if (responseHandler != null) { + responseHandler.onSuccess(200, headers, textBuilder.toString().getBytes(StandardCharsets.UTF_8)); + } else if (callback != null) { + callback.onCompleted(null, textBuilder.toString()); + } + } catch (Exception e) { + e.printStackTrace(); + Log.e("error msg:", e.getMessage()); + if (responseHandler != null) { + responseHandler.onFailure(400, null, "".getBytes(), e); + } + } + + return true; + } + + private NextcloudAPI.ApiConnectedListener apiCallback = new NextcloudAPI.ApiConnectedListener() { + @Override + public void onConnected() { + // ignore this one… see 5) + Log.i("NextcloudAPI", "SSO API connected"); + } + + @Override + public void onError(Exception ex) { + Log.i("NextcloudAPI", "SSO API ERROR"); + ex.printStackTrace(); + // TODO handle errors + } + }; + } } diff --git a/app/src/main/java/es/wolfi/passman/API/Credential.java b/app/src/main/java/es/wolfi/passman/API/Credential.java index 09db5fa..405498b 100644 --- a/app/src/main/java/es/wolfi/passman/API/Credential.java +++ b/app/src/main/java/es/wolfi/passman/API/Credential.java @@ -3,6 +3,7 @@ * * @copyright Copyright (c) 2016, Sander Brand (brantje@gmail.com) * @copyright Copyright (c) 2016, Marcos Zuriaga Miguel (wolfi@wolfi.es) + * @copyright Copyright (c) 2022, Timo Triebensky (timo@binsky.org) * @license GNU AGPL version 3 or any later version * <p> * This program is free software: you can redistribute it and/or modify diff --git a/app/src/main/res/drawable/ic_baseline_logout_24_grey.xml b/app/src/main/res/drawable/ic_baseline_logout_24_grey.xml new file mode 100644 index 0000000..57d92cf --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_logout_24_grey.xml @@ -0,0 +1,5 @@ +<vector android:autoMirrored="true" android:height="24dp" + android:tint="#757575" android:viewportHeight="24" + android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="@android:color/white" android:pathData="M17,7l-1.41,1.41L18.17,11H8v2h10.17l-2.58,2.58L17,17l5,-5zM4,5h8V3H4c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h8v-2H4V5z"/> +</vector> diff --git a/app/src/main/res/drawable/login_decision_button.xml b/app/src/main/res/drawable/login_decision_button.xml new file mode 100644 index 0000000..2d78d94 --- /dev/null +++ b/app/src/main/res/drawable/login_decision_button.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_pressed="false"> + <shape android:shape="rectangle"> + <corners android:radius="1000dp" /> + <solid android:color="#FFFFFF" /> + <padding + android:bottom="16dp" + android:left="16dp" + android:right="16dp" + android:top="16dp" /> + </shape> + </item> + <item android:state_pressed="true"> + <shape android:shape="rectangle"> + <corners android:radius="1000dp" /> + <solid android:color="@color/colorPrimary" /> + <stroke + android:width="1dip" + android:color="#FFFFFF" /> + <padding + android:bottom="16dp" + android:left="16dp" + android:right="16dp" + android:top="16dp" /> + </shape> + </item> +</selector> diff --git a/app/src/main/res/drawable/nc_background.png b/app/src/main/res/drawable/nc_background.png Binary files differnew file mode 100644 index 0000000..67e1a4b --- /dev/null +++ b/app/src/main/res/drawable/nc_background.png diff --git a/app/src/main/res/layout-land/content_login.xml b/app/src/main/res/layout-land/content_legacy_login.xml index 31f46c9..31f46c9 100644 --- a/app/src/main/res/layout-land/content_login.xml +++ b/app/src/main/res/layout-land/content_legacy_login.xml diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index b79443b..202717c 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -1,5 +1,4 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- +<?xml version="1.0" encoding="utf-8"?><!-- * Passman Android App * * @copyright Copyright (c) 2017, Andy Scherzinger @@ -28,6 +27,48 @@ android:fitsSystemWindows="true" tools:context="es.wolfi.app.passman.activities.LoginActivity"> - <include layout="@layout/content_login" /> + <ImageView + android:layout_width="match_parent" + android:layout_height="match_parent" + android:scaleType="center" + android:src="@drawable/nc_background" /> + + <ImageView + android:id="@+id/login_options_logo" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="50dp" + android:layout_marginBottom="@dimen/activity_vertical_margin" + android:contentDescription="@string/app_name_release" + android:src="@drawable/logo_horizontal" /> + + <include layout="@layout/content_legacy_login" /> + + <LinearLayout + android:id="@+id/login_options" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:gravity="center" + android:orientation="vertical"> + + <Button + android:id="@+id/load_sso_login_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="12dp" + android:background="@drawable/login_decision_button" + android:text="@string/nextcloud_single_sign_on" + android:textColor="@color/colorPrimary" /> + + <Button + android:id="@+id/load_legacy_login_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="12dp" + android:background="@drawable/login_decision_button" + android:text="@string/manual_login" + android:textColor="@color/colorPrimary" /> + + </LinearLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/app/src/main/res/layout/content_login.xml b/app/src/main/res/layout/content_legacy_login.xml index 5fb993a..34ba4f4 100644 --- a/app/src/main/res/layout/content_login.xml +++ b/app/src/main/res/layout/content_legacy_login.xml @@ -22,17 +22,17 @@ * --> <ScrollView android:id="@+id/scroll" - xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - xmlns:tools="http://schemas.android.com/tools" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_gravity="center" - android:fillViewport="true" - android:orientation="vertical"> + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:fillViewport="true" + android:orientation="vertical"> <LinearLayout - android:id="@+id/content_login" + android:id="@+id/content_legacy_login" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" @@ -40,13 +40,14 @@ android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" + android:visibility="invisible" app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:context="es.wolfi.app.passman.activities.LoginActivity" tools:showIn="@layout/activity_login" android:orientation="vertical"> <ImageView - android:id="@+id/logo" + android:id="@+id/legacy_login_logo" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="@dimen/activity_vertical_margin" @@ -84,8 +85,6 @@ android:inputType="textPersonName" android:textColor="@color/login_text_color" android:textColorHint="@color/login_text_hint_color"> - - <requestFocus /> </EditText> </LinearLayout> diff --git a/app/src/main/res/layout/content_manual_server_connection_settings.xml b/app/src/main/res/layout/content_manual_server_connection_settings.xml new file mode 100644 index 0000000..9b1d4c4 --- /dev/null +++ b/app/src/main/res/layout/content_manual_server_connection_settings.xml @@ -0,0 +1,92 @@ +<!-- + * Passman Android App + * + * @copyright Copyright (c) 2016, Sander Brand (brantje@gmail.com) + * @copyright Copyright (c) 2016, Marcos Zuriaga Miguel (wolfi@wolfi.es) + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * +--> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <LinearLayout + android:id="@+id/linear_layout_settings" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <TextView + style="@style/TextAppearance.AppCompat.Medium" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/nextcloud_connection_settings" /> + + <View + android:layout_width="match_parent" + android:layout_height="2dp" + android:layout_marginTop="4dp" + android:background="#8A8A8A" /> + + <TextView + style="@style/Label" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/URL" /> + + <EditText + android:id="@+id/settings_nextcloud_url" + style="@style/FormText" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:autofillHints="@string/URL" + android:inputType="textUri" + tools:ignore="LabelFor" /> + + <TextView + style="@style/Label" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/username" /> + + <EditText + android:id="@+id/settings_nextcloud_user" + style="@style/FormText" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:autofillHints="@string/username" + android:inputType="textNoSuggestions" + tools:ignore="LabelFor" /> + + <TextView + style="@style/Label" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/password" /> + + <EditText + android:id="@+id/settings_nextcloud_password" + style="@style/FormText" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:autofillHints="@string/password" + android:inputType="textPassword" + tools:ignore="LabelFor" /> + + </LinearLayout> + +</RelativeLayout> diff --git a/app/src/main/res/layout/content_sso_settings.xml b/app/src/main/res/layout/content_sso_settings.xml new file mode 100644 index 0000000..4a48bdb --- /dev/null +++ b/app/src/main/res/layout/content_sso_settings.xml @@ -0,0 +1,68 @@ +<!-- + * Passman Android App + * + * @copyright Copyright (c) 2016, Sander Brand (brantje@gmail.com) + * @copyright Copyright (c) 2016, Marcos Zuriaga Miguel (wolfi@wolfi.es) + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * +--> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <LinearLayout + android:id="@+id/linear_layout_settings" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <TextView + style="@style/TextAppearance.AppCompat.Medium" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/nextcloud_single_sign_on" /> + + <View + android:layout_width="match_parent" + android:layout_height="2dp" + android:layout_marginTop="4dp" + android:background="#8A8A8A" /> + + <RelativeLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="5dp"> + + <TextView + android:id="@+id/sso_user_server" + style="@style/TextAppearance.AppCompat.Small" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="@drawable/login_decision_button" /> + + <Button + android:id="@+id/sso_user_server_logout_button" + android:layout_width="30dp" + android:layout_height="30dp" + android:layout_alignEnd="@id/sso_user_server" + android:layout_centerInParent="true" + android:layout_marginEnd="5dp" + android:background="@drawable/ic_baseline_logout_24_grey" /> + </RelativeLayout> + + </LinearLayout> + +</RelativeLayout> diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml index daada53..e63a722 100644 --- a/app/src/main/res/layout/fragment_settings.xml +++ b/app/src/main/res/layout/fragment_settings.xml @@ -37,67 +37,17 @@ android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:paddingRight="@dimen/activity_horizontal_margin" - android:paddingBottom="@dimen/activity_vertical_margin" - tools:context="es.wolfi.app.passman.fragments.CredentialDisplayFragment"> + android:paddingBottom="@dimen/activity_vertical_margin"> <!--Nextcloud connection settings--> - <TextView - style="@style/TextAppearance.AppCompat.Medium" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="@string/nextcloud_connection_settings" /> - - <View - android:layout_width="match_parent" - android:layout_height="2dp" - android:layout_marginTop="4dp" - android:background="#8A8A8A" /> - - <TextView - style="@style/Label" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="@string/URL" /> - - <EditText - android:id="@+id/settings_nextcloud_url" - style="@style/FormText" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:autofillHints="@string/URL" - android:inputType="textUri" - tools:ignore="LabelFor" /> + <include + android:id="@+id/manual_server_connection_settings" + layout="@layout/content_manual_server_connection_settings" /> - <TextView - style="@style/Label" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="@string/username" /> - - <EditText - android:id="@+id/settings_nextcloud_user" - style="@style/FormText" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:autofillHints="@string/username" - android:inputType="textNoSuggestions" - tools:ignore="LabelFor" /> - - <TextView - style="@style/Label" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="@string/password" /> - - <EditText - android:id="@+id/settings_nextcloud_password" - style="@style/FormText" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:autofillHints="@string/password" - android:inputType="textPassword" - tools:ignore="LabelFor" /> + <include + android:id="@+id/sso_settings" + layout="@layout/content_sso_settings" /> <!--App settings--> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 38af446..bc0d6ed 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -84,4 +84,7 @@ <string name="offline_cache_description">Caches the encrypted vaults if they were loaded manually</string> <string name="enable_offline_cache">Enable offline cache</string> <string name="clear_offline_cache">Clear offline cache</string> + <string name="nextcloud_single_sign_on">Nextcloud Single Sign On</string> + <string name="manual_login">Manual Login</string> + <string name="confirm_account_logout">Are you sure you want to log out?</string> </resources> diff --git a/build.gradle b/build.gradle index ad34db2..016e46a 100644 --- a/build.gradle +++ b/build.gradle @@ -6,6 +6,7 @@ buildscript { mavenCentral() } dependencies { + apply plugin: 'maven-publish' classpath 'com.android.tools.build:gradle:7.1.2' // NOTE: Do not place your application dependencies here; they belong @@ -16,6 +17,7 @@ buildscript { allprojects { repositories { google() + maven { url "https://jitpack.io" } mavenCentral() } } |