Make android app buildable (#771)

This commit is contained in:
Leendert de Borst
2025-04-09 22:41:41 +02:00
parent a99fa1215e
commit ba02533b5c
30 changed files with 718 additions and 2 deletions

14
.vscode/tasks.json vendored
View File

@@ -125,6 +125,20 @@
"options": {
"cwd": "${workspaceFolder}/mobile-app"
}
},
{
"label": "Run Android App",
"type": "shell",
"command": "npx",
"args": ["expo", "run:android"],
"problemMatcher": [],
"group": {
"kind": "build",
"isDefault": true
},
"options": {
"cwd": "${workspaceFolder}/mobile-app"
}
}
]
}

View File

@@ -20,3 +20,28 @@ Run iOS on Simulator:
```
npx expo run:ios
```
Install OpenJDK for Android dev:
```
brew install openjdk@17
# Add to path
sudo ln -sfn /opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk-17.jdk
# Test if Java works on CLI
java --version
```
Make sure NDK is installed:
1. Open Android Studio.
2. Go to: `Preferences > Appearance & Behavior > System Settings > Android SDK > SDK Tools tab`.
3. Check NDK (Side by side).
4. Click Apply or OK to install it.
If getting `node` errors in Android studio, close and re-open Android Studio from CLI via:
```
open -a "Android Studio"
```

View File

@@ -150,6 +150,9 @@ dependencies {
// The version of react-native is set by the React Native Gradle Plugin
implementation("com.facebook.react:react-android")
// Add biometric dependency for credential management
implementation("androidx.biometric:biometric:1.1.0")
def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true";
def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true";
def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true";

View File

@@ -4,6 +4,7 @@
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
<queries>
<intent>
<action android:name="android.intent.action.VIEW"/>

View File

@@ -0,0 +1,25 @@
package com.aliasvault;
public class Credential {
private String username;
private String password;
private String service;
public Credential(String username, String password, String service) {
this.username = username;
this.password = password;
this.service = service;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
public String getService() {
return service;
}
}

View File

@@ -0,0 +1,75 @@
package com.aliasvault;
import android.util.Log;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import java.util.List;
public class CredentialManagerModule extends ReactContextBaseJavaModule {
private static final String TAG = "CredentialManagerModule";
private final ReactApplicationContext reactContext;
public CredentialManagerModule(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
}
@Override
public String getName() {
return "CredentialManager";
}
@ReactMethod
public void addCredential(String username, String password, String service, Promise promise) {
try {
SharedCredentialStore store = SharedCredentialStore.getInstance(reactContext);
Credential credential = new Credential(username, password, service);
store.addCredential(credential);
promise.resolve(true);
} catch (Exception e) {
Log.e(TAG, "Error adding credential", e);
promise.reject("ERR_ADD_CREDENTIAL", "Failed to add credential: " + e.getMessage(), e);
}
}
@ReactMethod
public void getCredentials(Promise promise) {
try {
SharedCredentialStore store = SharedCredentialStore.getInstance(reactContext);
List<Credential> credentials = store.getAllCredentials();
WritableArray credentialsArray = Arguments.createArray();
for (Credential credential : credentials) {
WritableMap credentialMap = Arguments.createMap();
credentialMap.putString("username", credential.getUsername());
credentialMap.putString("password", credential.getPassword());
credentialMap.putString("service", credential.getService());
credentialsArray.pushMap(credentialMap);
}
promise.resolve(credentialsArray);
} catch (Exception e) {
Log.e(TAG, "Error getting credentials", e);
promise.reject("ERR_GET_CREDENTIALS", "Failed to get credentials: " + e.getMessage(), e);
}
}
@ReactMethod
public void clearCredentials(Promise promise) {
try {
SharedCredentialStore store = SharedCredentialStore.getInstance(reactContext);
store.clearAllCredentials();
promise.resolve(true);
} catch (Exception e) {
Log.e(TAG, "Error clearing credentials", e);
promise.reject("ERR_CLEAR_CREDENTIALS", "Failed to clear credentials: " + e.getMessage(), e);
}
}
}

View File

@@ -0,0 +1,24 @@
package com.aliasvault;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class CredentialManagerPackage implements ReactPackage {
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new CredentialManagerModule(reactContext));
return modules;
}
}

View File

@@ -0,0 +1,220 @@
package com.aliasvault;
import android.content.Context;
import android.content.SharedPreferences;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.util.Base64;
import android.util.Log;
import androidx.biometric.BiometricPrompt;
import androidx.fragment.app.FragmentActivity;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.UnrecoverableEntryException;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
public class SharedCredentialStore {
private static final String TAG = "SharedCredentialStore";
private static final String ANDROID_KEYSTORE = "AndroidKeyStore";
private static final String ENCRYPTION_KEY_ALIAS = "aliasvault_encryption_key";
private static final String SHARED_PREFS_NAME = "net.aliasvault.autofill";
private static final String CREDENTIALS_KEY = "storedCredentials";
private static final String IV_SUFFIX = "_iv";
private static SharedCredentialStore instance;
private final Context appContext;
private SecretKey cachedEncryptionKey;
private SharedCredentialStore(Context context) {
this.appContext = context.getApplicationContext();
}
public static synchronized SharedCredentialStore getInstance(Context context) {
if (instance == null) {
instance = new SharedCredentialStore(context);
}
return instance;
}
private SecretKey getOrCreateEncryptionKey() throws Exception {
if (cachedEncryptionKey != null) {
return cachedEncryptionKey;
}
try {
KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
keyStore.load(null);
// Check if the key exists
if (keyStore.containsAlias(ENCRYPTION_KEY_ALIAS)) {
// Key exists, retrieve it
KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry) keyStore.getEntry(
ENCRYPTION_KEY_ALIAS, null);
cachedEncryptionKey = secretKeyEntry.getSecretKey();
return cachedEncryptionKey;
} else {
// Key doesn't exist, create it
KeyGenerator keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE);
KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(
ENCRYPTION_KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256)
.setUserAuthenticationRequired(true)
.build();
keyGenerator.init(keyGenParameterSpec);
cachedEncryptionKey = keyGenerator.generateKey();
return cachedEncryptionKey;
}
} catch (Exception e) {
Log.e(TAG, "Error getting or creating encryption key", e);
throw e;
}
}
private Cipher getCipher() throws NoSuchPaddingException, NoSuchAlgorithmException {
return Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
+ KeyProperties.BLOCK_MODE_GCM + "/"
+ KeyProperties.ENCRYPTION_PADDING_NONE);
}
private String encrypt(String data) throws Exception {
SecretKey key = getOrCreateEncryptionKey();
Cipher cipher = getCipher();
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] iv = cipher.getIV();
byte[] encryptedBytes = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
// Store IV in SharedPreferences
SharedPreferences prefs = appContext.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
prefs.edit().putString(CREDENTIALS_KEY + IV_SUFFIX, Base64.encodeToString(iv, Base64.DEFAULT)).apply();
return Base64.encodeToString(encryptedBytes, Base64.DEFAULT);
}
private String decrypt(String encryptedData) throws Exception {
SecretKey key = getOrCreateEncryptionKey();
Cipher cipher = getCipher();
// Get IV from SharedPreferences
SharedPreferences prefs = appContext.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
String ivString = prefs.getString(CREDENTIALS_KEY + IV_SUFFIX, null);
if (ivString == null) {
throw new Exception("IV not found for decryption");
}
byte[] iv = Base64.decode(ivString, Base64.DEFAULT);
byte[] encryptedBytes = Base64.decode(encryptedData, Base64.DEFAULT);
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, iv);
cipher.init(Cipher.DECRYPT_MODE, key, gcmParameterSpec);
byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
return new String(decryptedBytes, StandardCharsets.UTF_8);
}
public List<Credential> getAllCredentials() throws Exception {
SharedPreferences prefs = appContext.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
String encryptedData = prefs.getString(CREDENTIALS_KEY, null);
if (encryptedData == null) {
return new ArrayList<>();
}
String decryptedData = decrypt(encryptedData);
return parseCredentialsFromJson(decryptedData);
}
public void addCredential(Credential credential) throws Exception {
List<Credential> credentials = getAllCredentials();
credentials.add(credential);
String jsonData = credentialsToJson(credentials);
String encryptedData = encrypt(jsonData);
SharedPreferences prefs = appContext.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
prefs.edit().putString(CREDENTIALS_KEY, encryptedData).apply();
}
public void clearAllCredentials() {
SharedPreferences prefs = appContext.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
prefs.edit()
.remove(CREDENTIALS_KEY)
.remove(CREDENTIALS_KEY + IV_SUFFIX)
.apply();
try {
KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
keyStore.load(null);
if (keyStore.containsAlias(ENCRYPTION_KEY_ALIAS)) {
keyStore.deleteEntry(ENCRYPTION_KEY_ALIAS);
}
cachedEncryptionKey = null;
} catch (Exception e) {
Log.e(TAG, "Error clearing encryption key", e);
}
}
public void clearCache() {
cachedEncryptionKey = null;
}
private List<Credential> parseCredentialsFromJson(String json) throws JSONException {
List<Credential> credentials = new ArrayList<>();
JSONArray jsonArray = new JSONArray(json);
for (int i = 0; i < jsonArray.length(); i++) {
JSONObject jsonObject = jsonArray.getJSONObject(i);
String username = jsonObject.getString("username");
String password = jsonObject.getString("password");
String service = jsonObject.getString("service");
credentials.add(new Credential(username, password, service));
}
return credentials;
}
private String credentialsToJson(List<Credential> credentials) throws JSONException {
JSONArray jsonArray = new JSONArray();
for (Credential credential : credentials) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("username", credential.getUsername());
jsonObject.put("password", credential.getPassword());
jsonObject.put("service", credential.getService());
jsonArray.put(jsonObject);
}
return jsonArray.toString();
}
}

View File

@@ -12,6 +12,7 @@ import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.soloader.OpenSourceMergedSoMapping
import com.facebook.soloader.SoLoader
import net.aliasvault.app.credentialmanager.CredentialManagerPackage
import expo.modules.ApplicationLifecycleDispatcher
import expo.modules.ReactNativeHostWrapper
@@ -25,6 +26,7 @@ class MainApplication : Application(), ReactApplication {
val packages = PackageList(this).packages
// Packages that cannot be autolinked yet can be added manually here, for example:
// packages.add(new MyReactNativePackage());
packages.add(CredentialManagerPackage())
return packages
}

View File

@@ -0,0 +1,25 @@
package net.aliasvault.app.credentialmanager;
public class Credential {
private String username;
private String password;
private String service;
public Credential(String username, String password, String service) {
this.username = username;
this.password = password;
this.service = service;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
public String getService() {
return service;
}
}

View File

@@ -0,0 +1,75 @@
package net.aliasvault.app.credentialmanager;
import android.util.Log;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import java.util.List;
public class CredentialManagerModule extends ReactContextBaseJavaModule {
private static final String TAG = "CredentialManagerModule";
private final ReactApplicationContext reactContext;
public CredentialManagerModule(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
}
@Override
public String getName() {
return "CredentialManager";
}
@ReactMethod
public void addCredential(String username, String password, String service, Promise promise) {
try {
SharedCredentialStore store = SharedCredentialStore.getInstance(reactContext);
Credential credential = new Credential(username, password, service);
store.addCredential(credential);
promise.resolve(true);
} catch (Exception e) {
Log.e(TAG, "Error adding credential", e);
promise.reject("ERR_ADD_CREDENTIAL", "Failed to add credential: " + e.getMessage(), e);
}
}
@ReactMethod
public void getCredentials(Promise promise) {
try {
SharedCredentialStore store = SharedCredentialStore.getInstance(reactContext);
List<Credential> credentials = store.getAllCredentials();
WritableArray credentialsArray = Arguments.createArray();
for (Credential credential : credentials) {
WritableMap credentialMap = Arguments.createMap();
credentialMap.putString("username", credential.getUsername());
credentialMap.putString("password", credential.getPassword());
credentialMap.putString("service", credential.getService());
credentialsArray.pushMap(credentialMap);
}
promise.resolve(credentialsArray);
} catch (Exception e) {
Log.e(TAG, "Error getting credentials", e);
promise.reject("ERR_GET_CREDENTIALS", "Failed to get credentials: " + e.getMessage(), e);
}
}
@ReactMethod
public void clearCredentials(Promise promise) {
try {
SharedCredentialStore store = SharedCredentialStore.getInstance(reactContext);
store.clearAllCredentials();
promise.resolve(true);
} catch (Exception e) {
Log.e(TAG, "Error clearing credentials", e);
promise.reject("ERR_CLEAR_CREDENTIALS", "Failed to clear credentials: " + e.getMessage(), e);
}
}
}

View File

@@ -0,0 +1,24 @@
package net.aliasvault.app.credentialmanager;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class CredentialManagerPackage implements ReactPackage {
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new CredentialManagerModule(reactContext));
return modules;
}
}

View File

@@ -0,0 +1,205 @@
package net.aliasvault.app.credentialmanager;
import android.content.Context;
import android.content.SharedPreferences;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.util.Base64;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.util.ArrayList;
import java.util.List;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
public class SharedCredentialStore {
private static final String TAG = "SharedCredentialStore";
private static final String ANDROID_KEYSTORE = "AndroidKeyStore";
private static final String ENCRYPTION_KEY_ALIAS = "aliasvault_encryption_key";
private static final String SHARED_PREFS_NAME = "net.aliasvault.autofill";
private static final String CREDENTIALS_KEY = "storedCredentials";
private static final String IV_SUFFIX = "_iv";
private static SharedCredentialStore instance;
private final Context appContext;
private SecretKey cachedEncryptionKey;
private SharedCredentialStore(Context context) {
this.appContext = context.getApplicationContext();
}
public static synchronized SharedCredentialStore getInstance(Context context) {
if (instance == null) {
instance = new SharedCredentialStore(context);
}
return instance;
}
private SecretKey getOrCreateEncryptionKey() throws Exception {
if (cachedEncryptionKey != null) {
return cachedEncryptionKey;
}
try {
KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
keyStore.load(null);
// Check if the key exists
if (keyStore.containsAlias(ENCRYPTION_KEY_ALIAS)) {
// Key exists, retrieve it
KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry) keyStore.getEntry(
ENCRYPTION_KEY_ALIAS, null);
cachedEncryptionKey = secretKeyEntry.getSecretKey();
return cachedEncryptionKey;
} else {
// Key doesn't exist, create it
KeyGenerator keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE);
KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(
ENCRYPTION_KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256)
.setUserAuthenticationRequired(true)
.build();
keyGenerator.init(keyGenParameterSpec);
cachedEncryptionKey = keyGenerator.generateKey();
return cachedEncryptionKey;
}
} catch (Exception e) {
Log.e(TAG, "Error getting or creating encryption key", e);
throw e;
}
}
private Cipher getCipher() throws Exception {
return Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
+ KeyProperties.BLOCK_MODE_GCM + "/"
+ KeyProperties.ENCRYPTION_PADDING_NONE);
}
private String encrypt(String data) throws Exception {
SecretKey key = getOrCreateEncryptionKey();
Cipher cipher = getCipher();
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] iv = cipher.getIV();
byte[] encryptedBytes = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
// Store IV in SharedPreferences
SharedPreferences prefs = appContext.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
prefs.edit().putString(CREDENTIALS_KEY + IV_SUFFIX, Base64.encodeToString(iv, Base64.DEFAULT)).apply();
return Base64.encodeToString(encryptedBytes, Base64.DEFAULT);
}
private String decrypt(String encryptedData) throws Exception {
SecretKey key = getOrCreateEncryptionKey();
Cipher cipher = getCipher();
// Get IV from SharedPreferences
SharedPreferences prefs = appContext.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
String ivString = prefs.getString(CREDENTIALS_KEY + IV_SUFFIX, null);
if (ivString == null) {
throw new Exception("IV not found for decryption");
}
byte[] iv = Base64.decode(ivString, Base64.DEFAULT);
byte[] encryptedBytes = Base64.decode(encryptedData, Base64.DEFAULT);
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, iv);
cipher.init(Cipher.DECRYPT_MODE, key, gcmParameterSpec);
byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
return new String(decryptedBytes, StandardCharsets.UTF_8);
}
public List<Credential> getAllCredentials() throws Exception {
SharedPreferences prefs = appContext.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
String encryptedData = prefs.getString(CREDENTIALS_KEY, null);
if (encryptedData == null) {
return new ArrayList<>();
}
String decryptedData = decrypt(encryptedData);
return parseCredentialsFromJson(decryptedData);
}
public void addCredential(Credential credential) throws Exception {
List<Credential> credentials = getAllCredentials();
credentials.add(credential);
String jsonData = credentialsToJson(credentials);
String encryptedData = encrypt(jsonData);
SharedPreferences prefs = appContext.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
prefs.edit().putString(CREDENTIALS_KEY, encryptedData).apply();
}
public void clearAllCredentials() {
SharedPreferences prefs = appContext.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
prefs.edit()
.remove(CREDENTIALS_KEY)
.remove(CREDENTIALS_KEY + IV_SUFFIX)
.apply();
try {
KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
keyStore.load(null);
if (keyStore.containsAlias(ENCRYPTION_KEY_ALIAS)) {
keyStore.deleteEntry(ENCRYPTION_KEY_ALIAS);
}
cachedEncryptionKey = null;
} catch (Exception e) {
Log.e(TAG, "Error clearing encryption key", e);
}
}
public void clearCache() {
cachedEncryptionKey = null;
}
private List<Credential> parseCredentialsFromJson(String json) throws JSONException {
List<Credential> credentials = new ArrayList<>();
JSONArray jsonArray = new JSONArray(json);
for (int i = 0; i < jsonArray.length(); i++) {
JSONObject jsonObject = jsonArray.getJSONObject(i);
String username = jsonObject.getString("username");
String password = jsonObject.getString("password");
String service = jsonObject.getString("service");
credentials.add(new Credential(username, password, service));
}
return credentials;
}
private String credentialsToJson(List<Credential> credentials) throws JSONException {
JSONArray jsonArray = new JSONArray();
for (Credential credential : credentials) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("username", credential.getUsername());
jsonObject.put("password", credential.getPassword());
jsonObject.put("service", credential.getService());
jsonArray.put(jsonObject);
}
return jsonArray.toString();
}
}

View File

@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/iconBackground"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/iconBackground"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB