App zusammenkopiert aus Teraplex und CodeGuard
17
.idea/deploymentTargetDropDown.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetDropDown">
|
||||
<targetSelectedWithDropDown>
|
||||
<Target>
|
||||
<type value="QUICK_BOOT_TARGET" />
|
||||
<deviceKey>
|
||||
<Key>
|
||||
<type value="VIRTUAL_DEVICE_PATH" />
|
||||
<value value="C:\Users\Cody\.android\avd\for_Website.avd" />
|
||||
</Key>
|
||||
</deviceKey>
|
||||
</Target>
|
||||
</targetSelectedWithDropDown>
|
||||
<timeTargetWasSelectedWithDropDown value="2024-02-23T16:37:40.934381500Z" />
|
||||
</component>
|
||||
</project>
|
@ -4,7 +4,7 @@ plugins {
|
||||
|
||||
android {
|
||||
namespace 'com.example.onetap_ssh'
|
||||
compileSdk 33
|
||||
compileSdk 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.example.onetap_ssh"
|
||||
@ -40,6 +40,11 @@ dependencies {
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2'
|
||||
implementation 'androidx.navigation:navigation-fragment:2.7.5'
|
||||
implementation 'androidx.navigation:navigation-ui:2.7.5'
|
||||
implementation "androidx.biometric:biometric:1.1.0"
|
||||
implementation 'androidx.activity:activity:1.8.0'
|
||||
implementation 'com.google.code.gson:gson:2.10.1'
|
||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.70'
|
||||
implementation 'com.caverock:androidsvg-aar:1.4'
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
|
@ -10,19 +10,36 @@
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.OneTapSSH"
|
||||
android:theme="@style/Theme.OneTap_SSH"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:resizeableActivity="false"
|
||||
android:name=".IntroActivity"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.OneTapSSH.NoActionBar">
|
||||
android:theme="@style/Theme.OneTap_SSH"
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.OneTap_SSH.None">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".unlock.UnlockActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.OneTap_SSH.None"
|
||||
android:configChanges="orientation|screenSize">
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
56
app/src/main/java/com/example/onetap_ssh/BaseActivity.java
Normal file
@ -0,0 +1,56 @@
|
||||
package com.example.onetap_ssh;
|
||||
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.example.onetap_ssh.unlock.UnlockContract;
|
||||
import com.example.onetap_ssh.util.AppLocale;
|
||||
import com.example.onetap_ssh.util.SettingsUtil;
|
||||
import com.example.onetap_ssh.util.ThemeUtil;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class BaseActivity extends AppCompatActivity {
|
||||
|
||||
private ActivityResultLauncher<Void> startUnlockActivity;
|
||||
|
||||
private Runnable unlockSuccess, unlockFailure;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
registerCallbacks();
|
||||
|
||||
if(SettingsUtil.isScreenSecurity(this)) {
|
||||
getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
|
||||
}
|
||||
|
||||
ThemeUtil.loadTheme(this);
|
||||
setLocale(SettingsUtil.getLocale(this));
|
||||
}
|
||||
|
||||
private void registerCallbacks() {
|
||||
startUnlockActivity = registerForActivityResult(new UnlockContract(), success -> {
|
||||
if(success && unlockSuccess != null) unlockSuccess.run();
|
||||
if(!success && unlockFailure != null) unlockFailure.run();
|
||||
});
|
||||
}
|
||||
|
||||
public void promptUnlock(Runnable success, Runnable failure) {
|
||||
unlockSuccess = success;
|
||||
unlockFailure = failure;
|
||||
startUnlockActivity.launch(null);
|
||||
}
|
||||
|
||||
public void setLocale(AppLocale locale) {
|
||||
Configuration config = new Configuration();
|
||||
config.setLocale(locale == AppLocale.SYSTEM_DEFAULT ? Locale.getDefault() : locale.getLocale());
|
||||
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
|
||||
}
|
||||
|
||||
}
|
89
app/src/main/java/com/example/onetap_ssh/IntroActivity.java
Normal file
@ -0,0 +1,89 @@
|
||||
package com.example.onetap_ssh;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.media.MediaPlayer;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.example.onetap_ssh.databinding.ActivityIntroBinding;
|
||||
import com.example.onetap_ssh.unlock.UnlockActivity;
|
||||
import com.example.onetap_ssh.util.SettingsUtil;
|
||||
|
||||
public class IntroActivity extends BaseActivity {
|
||||
|
||||
private ActivityIntroBinding binding;
|
||||
|
||||
private MediaPlayer mMediaPlayer;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (!SettingsUtil.isIntroVideoEnabled(this)) {
|
||||
openMainActivity();
|
||||
return;
|
||||
}
|
||||
|
||||
binding = ActivityIntroBinding.inflate(getLayoutInflater());
|
||||
|
||||
Uri uri = Uri.parse(String.format("android.resource://%s/%s", getPackageName(), R.raw.intro_vp9));
|
||||
binding.videoView.setVideoURI(uri);
|
||||
binding.videoView.start();
|
||||
|
||||
binding.videoView.setOnPreparedListener(mediaPlayer -> {
|
||||
mMediaPlayer = mediaPlayer;
|
||||
setDimension();
|
||||
});
|
||||
|
||||
binding.videoView.setOnCompletionListener(mp -> openMainActivity());
|
||||
|
||||
binding.videoView.setOnErrorListener((MediaPlayer mp, int what, int extra) -> {
|
||||
Toast.makeText(this, R.string.intro_video_failed, Toast.LENGTH_LONG).show();
|
||||
openMainActivity();
|
||||
return true;
|
||||
});
|
||||
|
||||
setContentView(binding.getRoot());
|
||||
}
|
||||
|
||||
public void openMainActivity() {
|
||||
Intent m = new Intent(getApplicationContext(), UnlockActivity.class);
|
||||
m.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NO_ANIMATION);
|
||||
startActivity(m);
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if(mMediaPlayer != null) mMediaPlayer.release();
|
||||
mMediaPlayer = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
binding.videoView.start();
|
||||
}
|
||||
|
||||
private void setDimension() {
|
||||
float videoProportion = (float) mMediaPlayer.getVideoHeight() / mMediaPlayer.getVideoWidth();
|
||||
int screenWidth = getResources().getDisplayMetrics().widthPixels;
|
||||
int screenHeight = getResources().getDisplayMetrics().heightPixels;
|
||||
float screenProportion = (float) screenHeight / (float) screenWidth;
|
||||
ViewGroup.LayoutParams lp = binding.videoView.getLayoutParams();
|
||||
|
||||
if (videoProportion < screenProportion) {
|
||||
lp.height= screenHeight;
|
||||
lp.width = (int) ((float) screenHeight / videoProportion);
|
||||
} else {
|
||||
lp.width = screenWidth;
|
||||
lp.height = (int) ((float) screenWidth * videoProportion);
|
||||
}
|
||||
binding.videoView.setLayoutParams(lp);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -36,7 +36,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
.setAction("Action", null).show();
|
||||
}
|
||||
});
|
||||
DrawerLayout drawer = binding.drawerLayout;
|
||||
DrawerLayout drawer = binding.appBackground;
|
||||
NavigationView navigationView = binding.navView;
|
||||
// Passing each menu ID as a set of Ids because each
|
||||
// menu should be considered as top level destinations.
|
||||
|
@ -0,0 +1,53 @@
|
||||
package com.example.onetap_ssh.backup;
|
||||
|
||||
import android.util.Base64;
|
||||
|
||||
import com.example.onetap_ssh.crypto.CryptoException;
|
||||
import com.example.onetap_ssh.crypto.CryptoParameters;
|
||||
import com.example.onetap_ssh.util.BackupException;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
public class BackupData {
|
||||
|
||||
private CryptoParameters parameters;
|
||||
|
||||
private String database;
|
||||
|
||||
private BackupGroup[] groups;
|
||||
|
||||
private BackupData() {}
|
||||
|
||||
public BackupData(CryptoParameters parameters, String database, BackupGroup[] groups) {
|
||||
this.parameters = parameters;
|
||||
this.database = database;
|
||||
this.groups = groups;
|
||||
}
|
||||
|
||||
public CryptoParameters getParameters() {
|
||||
return parameters;
|
||||
}
|
||||
|
||||
public String getDatabase() {
|
||||
return database;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public BackupGroup[] getGroups() {
|
||||
return groups;
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
if(parameters == null || database == null || groups == null) return false;
|
||||
|
||||
if(!parameters.isValid()) return false;
|
||||
|
||||
for(BackupGroup group : groups) {
|
||||
if(!group.isValid()) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package com.example.onetap_ssh.backup;
|
||||
|
||||
public class BackupGroup {
|
||||
|
||||
public String id;
|
||||
public String name;
|
||||
|
||||
private BackupGroup() {}
|
||||
|
||||
public BackupGroup(String id, String name) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return id != null && name != null;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
package com.example.onetap_ssh.backup;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.util.Base64;
|
||||
|
||||
import com.example.onetap_ssh.crypto.CryptoException;
|
||||
import com.example.onetap_ssh.crypto.CryptoParameters;
|
||||
import com.example.onetap_ssh.util.BackupException;
|
||||
import com.example.onetap_ssh.util.IOUtil;
|
||||
|
||||
import com.example.onetap_ssh.util.SettingsUtil;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
public class BackupUtil {
|
||||
|
||||
private static final SimpleDateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.ENGLISH);
|
||||
|
||||
public static String getBackupName() {
|
||||
return "code_guard_backup_" + FORMAT.format(new Date());
|
||||
}
|
||||
|
||||
public static void saveBackup(Context context, Uri backupFile, SecretKey key, CryptoParameters parameters) throws BackupException, CryptoException {
|
||||
|
||||
}
|
||||
|
||||
public static CryptoParameters loadParametersFromBackup(Context context, Uri backupFile) throws BackupException {
|
||||
try(InputStream in = context.getContentResolver().openInputStream(backupFile)) {
|
||||
if(in == null) throw new BackupException("Failed to read backup file");
|
||||
byte[] backupBytes = IOUtil.readBytes(in);
|
||||
BackupData data = SettingsUtil.GSON.fromJson(new String(backupBytes, StandardCharsets.UTF_8), BackupData.class);
|
||||
if(!data.getParameters().isValid()) throw new BackupException("Invalid crypto parameters");
|
||||
return data.getParameters();
|
||||
} catch (JsonSyntaxException e) {
|
||||
throw new BackupException("Invalid JSON", e);
|
||||
} catch (IOException e) {
|
||||
throw new BackupException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static BackupData loadBackup(Context context, Uri backupFile) throws BackupException {
|
||||
try (InputStream in = context.getContentResolver().openInputStream(backupFile)) {
|
||||
if (in == null) throw new BackupException("Failed to read backup file");
|
||||
byte[] backupBytes = IOUtil.readBytes(in);
|
||||
BackupData data = SettingsUtil.GSON.fromJson(new String(backupBytes, StandardCharsets.UTF_8), BackupData.class);
|
||||
if(!data.isValid()) throw new BackupException("Invalid backup data"); // TODO: more info on backup errors
|
||||
return data;
|
||||
} catch (JsonSyntaxException e) {
|
||||
throw new BackupException("Invalid JSON", e);
|
||||
} catch (IOException e) {
|
||||
throw new BackupException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package com.example.onetap_ssh.crypto;
|
||||
|
||||
public class BiometricKey {
|
||||
|
||||
private final String id;
|
||||
private final byte[] encryptedKey;
|
||||
private final byte[] iv;
|
||||
|
||||
public BiometricKey(String id, byte[] encryptedKey, byte[] iv) {
|
||||
this.id = id;
|
||||
this.encryptedKey = encryptedKey;
|
||||
this.iv = iv;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public byte[] getEncryptedKey() {
|
||||
return encryptedKey;
|
||||
}
|
||||
|
||||
public byte[] getIV() {
|
||||
return iv;
|
||||
}
|
||||
}
|
134
app/src/main/java/com/example/onetap_ssh/crypto/Crypto.java
Normal file
@ -0,0 +1,134 @@
|
||||
package com.example.onetap_ssh.crypto;
|
||||
|
||||
import android.os.Build;
|
||||
import android.security.keystore.KeyGenParameterSpec;
|
||||
import android.security.keystore.KeyProperties;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import org.bouncycastle.crypto.generators.Argon2BytesGenerator;
|
||||
import org.bouncycastle.crypto.params.Argon2Parameters;
|
||||
|
||||
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.SecureRandom;
|
||||
import java.security.UnrecoverableKeyException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.UUID;
|
||||
|
||||
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;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class Crypto {
|
||||
|
||||
private static final String KEY_STORE = "AndroidKeyStore";
|
||||
|
||||
public static byte[] generateHash(CryptoParameters parameters, String password) {
|
||||
Argon2Parameters params = new Argon2Parameters.Builder()
|
||||
.withVersion(parameters.getArgon2Version())
|
||||
.withIterations(parameters.getArgon2Iterations())
|
||||
.withMemoryAsKB(parameters.getArgon2Memory())
|
||||
.withParallelism(parameters.getArgon2Parallelism())
|
||||
.withSalt(parameters.getSalt())
|
||||
.build();
|
||||
|
||||
Argon2BytesGenerator generator = new Argon2BytesGenerator();
|
||||
generator.init(params);
|
||||
byte[] result = new byte[parameters.getEncryptionAESKeyLength()];
|
||||
generator.generateBytes(password.getBytes(StandardCharsets.UTF_8), result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static SecretKey generateKey(CryptoParameters parameters, String password) throws CryptoException {
|
||||
byte[] hash = generateHash(parameters, password);
|
||||
return new SecretKeySpec(hash, "AES");
|
||||
}
|
||||
|
||||
private static byte[] generateBytes(int length) {
|
||||
SecureRandom r = new SecureRandom();
|
||||
byte[] bytes = new byte[length];
|
||||
r.nextBytes(bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public static byte[] generateSalt(CryptoParameters parameters) {
|
||||
return generateBytes(parameters.getEncryptionSaltLength());
|
||||
}
|
||||
|
||||
public static byte[] generateIV(CryptoParameters parameters) {
|
||||
return generateBytes(parameters.getEncryptionIVLength());
|
||||
}
|
||||
|
||||
public static CryptoResult encryptWithResult(CryptoParameters parameters, byte[] bytes, SecretKey key, boolean useIV) throws CryptoException {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(parameters.getEncryptionAlgorithm());
|
||||
GCMParameterSpec spec = new GCMParameterSpec(parameters.getEncryptionGCMTagLength() * 8, parameters.getIV());
|
||||
if(useIV) {
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
|
||||
}else {
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key);
|
||||
}
|
||||
return new CryptoResult(cipher.doFinal(bytes), cipher.getIV());
|
||||
}catch(NoSuchAlgorithmException | NoSuchPaddingException |
|
||||
InvalidAlgorithmParameterException | InvalidKeyException | BadPaddingException |
|
||||
IllegalBlockSizeException e) {
|
||||
throw new CryptoException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] encrypt(CryptoParameters parameters, byte[] bytes, SecretKey key) throws CryptoException {
|
||||
return encryptWithResult(parameters, bytes, key, true).getEncrypted();
|
||||
}
|
||||
|
||||
public static byte[] decrypt(CryptoParameters parameters, byte[] bytes, SecretKey key, byte[] overrideIV) throws CryptoException {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(parameters.getEncryptionAlgorithm());
|
||||
GCMParameterSpec spec = new GCMParameterSpec(parameters.getEncryptionGCMTagLength() * 8, overrideIV != null ? overrideIV : parameters.getIV());
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, spec);
|
||||
return cipher.doFinal(bytes);
|
||||
}catch(NoSuchAlgorithmException | NoSuchPaddingException |
|
||||
InvalidAlgorithmParameterException | InvalidKeyException | BadPaddingException |
|
||||
IllegalBlockSizeException e) {
|
||||
throw new CryptoException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] decrypt(CryptoParameters parameters, byte[] bytes, SecretKey key) throws CryptoException {
|
||||
return decrypt(parameters, bytes, key, null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static SecretKey getBiometricKey(BiometricKey key) throws CryptoException {
|
||||
try {
|
||||
KeyStore store = KeyStore.getInstance(KEY_STORE);
|
||||
store.load(null);
|
||||
return (SecretKey) store.getKey(key.getId(), null);
|
||||
} catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException | CertificateException | IOException e) {
|
||||
throw new CryptoException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void deleteBiometricKey(BiometricKey key) throws CryptoException {
|
||||
try {
|
||||
KeyStore ks = KeyStore.getInstance(KEY_STORE);
|
||||
ks.load(null);
|
||||
ks.deleteEntry(key.getId());
|
||||
} catch (KeyStoreException | CertificateException | IOException | NoSuchAlgorithmException e) {
|
||||
throw new CryptoException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package com.example.onetap_ssh.crypto;
|
||||
|
||||
public class CryptoException extends Exception {
|
||||
public CryptoException() {
|
||||
}
|
||||
|
||||
public CryptoException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public CryptoException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public CryptoException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
package com.example.onetap_ssh.crypto;
|
||||
|
||||
import org.bouncycastle.crypto.params.Argon2Parameters;
|
||||
|
||||
public class CryptoParameters {
|
||||
|
||||
private final String hashType = "argon2";
|
||||
|
||||
private final int argon2Version = Argon2Parameters.ARGON2_VERSION_13;
|
||||
private final int argon2Iterations = 2;
|
||||
private final int argon2Memory = 16384;
|
||||
private final int argon2Parallelism = 1;
|
||||
|
||||
private final String encryptionAlgorithm = "AES/GCM/NoPadding";
|
||||
private final int encryptionGCMTagLength = 16;
|
||||
private final int encryptionIVLength = 12;
|
||||
private final int encryptionAESKeyLength = 32;
|
||||
private final int encryptionSaltLength = 16;
|
||||
|
||||
private byte[] salt;
|
||||
private byte[] iv;
|
||||
|
||||
private CryptoParameters() {}
|
||||
|
||||
private void init(byte[] salt, byte[] iv) {
|
||||
this.salt = salt;
|
||||
this.iv = iv;
|
||||
}
|
||||
|
||||
public byte[] getSalt() {
|
||||
return salt;
|
||||
}
|
||||
|
||||
public byte[] getIV() {
|
||||
return iv;
|
||||
}
|
||||
|
||||
public String getHashType() {
|
||||
return hashType;
|
||||
}
|
||||
|
||||
public int getArgon2Version() {
|
||||
return argon2Version;
|
||||
}
|
||||
|
||||
public int getArgon2Iterations() {
|
||||
return argon2Iterations;
|
||||
}
|
||||
|
||||
public int getArgon2Memory() {
|
||||
return argon2Memory;
|
||||
}
|
||||
|
||||
public int getArgon2Parallelism() {
|
||||
return argon2Parallelism;
|
||||
}
|
||||
|
||||
public String getEncryptionAlgorithm() {
|
||||
return encryptionAlgorithm;
|
||||
}
|
||||
|
||||
public int getEncryptionGCMTagLength() {
|
||||
return encryptionGCMTagLength;
|
||||
}
|
||||
|
||||
public int getEncryptionIVLength() {
|
||||
return encryptionIVLength;
|
||||
}
|
||||
|
||||
public int getEncryptionAESKeyLength() {
|
||||
return encryptionAESKeyLength;
|
||||
}
|
||||
|
||||
public int getEncryptionSaltLength() {
|
||||
return encryptionSaltLength;
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return hashType != null
|
||||
&& argon2Version > 0
|
||||
&& argon2Iterations > 0
|
||||
&& argon2Memory > 0
|
||||
&& argon2Parallelism > 0
|
||||
&& encryptionAlgorithm != null
|
||||
&& encryptionGCMTagLength > 0
|
||||
&& encryptionIVLength > 0
|
||||
&& encryptionAESKeyLength > 0
|
||||
&& encryptionSaltLength > 0;
|
||||
}
|
||||
|
||||
public static CryptoParameters createNew() {
|
||||
CryptoParameters params = new CryptoParameters();
|
||||
byte[] salt = Crypto.generateSalt(params);
|
||||
byte[] iv = Crypto.generateIV(params);
|
||||
params.init(salt, iv);
|
||||
return params;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package com.example.onetap_ssh.crypto;
|
||||
|
||||
public class CryptoResult {
|
||||
|
||||
private final byte[] encrypted;
|
||||
private final byte[] iv;
|
||||
|
||||
public CryptoResult(byte[] encrypted, byte[] iv) {
|
||||
this.encrypted = encrypted;
|
||||
this.iv = iv;
|
||||
}
|
||||
|
||||
public byte[] getEncrypted() {
|
||||
return encrypted;
|
||||
}
|
||||
|
||||
public byte[] getIV() {
|
||||
return iv;
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package com.example.onetap_ssh.fragment;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
public abstract class NamedFragment extends Fragment {
|
||||
|
||||
public abstract String getName();
|
||||
|
||||
}
|
@ -0,0 +1,254 @@
|
||||
package com.example.onetap_ssh.fragment;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import com.example.onetap_ssh.MainActivity;
|
||||
import com.example.onetap_ssh.R;
|
||||
import com.example.onetap_ssh.backup.BackupData;
|
||||
import com.example.onetap_ssh.backup.BackupUtil;
|
||||
import com.example.onetap_ssh.crypto.BiometricKey;
|
||||
import com.example.onetap_ssh.crypto.Crypto;
|
||||
import com.example.onetap_ssh.crypto.CryptoException;
|
||||
import com.example.onetap_ssh.crypto.CryptoParameters;
|
||||
import com.example.onetap_ssh.databinding.DialogManageIconPacksBinding;
|
||||
import com.example.onetap_ssh.databinding.FragmentSettingsBinding;
|
||||
import com.example.onetap_ssh.icon.IconPack;
|
||||
import com.example.onetap_ssh.icon.IconPackListAdapter;
|
||||
import com.example.onetap_ssh.icon.IconUtil;
|
||||
import com.example.onetap_ssh.util.AppLocale;
|
||||
import com.example.onetap_ssh.util.Appearance;
|
||||
import com.example.onetap_ssh.util.BackupException;
|
||||
import com.example.onetap_ssh.util.BiometricUtil;
|
||||
import com.example.onetap_ssh.util.DialogUtil;
|
||||
|
||||
import com.example.onetap_ssh.util.SettingsUtil;
|
||||
import com.example.onetap_ssh.util.StyledDialogBuilder;
|
||||
import com.example.onetap_ssh.util.Theme;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
public class SettingsFragment extends NamedFragment {
|
||||
|
||||
private FragmentSettingsBinding binding;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return requireActivity().getString(R.string.fragment_settings);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
binding = FragmentSettingsBinding.inflate(inflater);
|
||||
|
||||
|
||||
String[] localeNames = new String[AppLocale.values().length];
|
||||
for(int i = 0; i < localeNames.length; i++) {
|
||||
localeNames[i] = AppLocale.values()[i].getName(requireContext());
|
||||
}
|
||||
|
||||
binding.settingsLanguage.setAdapter(new ArrayAdapter<>(requireContext(), android.R.layout.simple_list_item_1, localeNames));
|
||||
binding.settingsLanguage.setSelection(Arrays.asList(AppLocale.values()).indexOf(SettingsUtil.getLocale(requireContext())));
|
||||
binding.settingsLanguage.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
AppLocale locale = AppLocale.values()[position];
|
||||
if(locale.equals(SettingsUtil.getLocale(requireContext()))) return;
|
||||
|
||||
SettingsUtil.setLocale(requireContext(), locale);
|
||||
requireActivity().recreate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
binding.settingsEnableEncryption.setChecked(SettingsUtil.isDatabaseEncrypted(requireContext()));
|
||||
binding.settingsEnableEncryption.setOnCheckedChangeListener((view, checked) -> {
|
||||
if(checked) {
|
||||
if(SettingsUtil.isDatabaseEncrypted(requireContext())) return;
|
||||
|
||||
DialogUtil.showSetPasswordDialog(requireContext(), password -> {
|
||||
CryptoParameters params = CryptoParameters.createNew();
|
||||
Log.d("Crypto", "Created new crypto params");
|
||||
|
||||
|
||||
}, () -> view.setChecked(false));
|
||||
}else {
|
||||
if(!SettingsUtil.isDatabaseEncrypted(requireContext())) return;
|
||||
|
||||
DialogUtil.showYesNo(requireContext(), R.string.disable_encryption_title, R.string.disable_encryption_message, () -> {
|
||||
|
||||
}, () -> view.setChecked(true));
|
||||
}
|
||||
});
|
||||
|
||||
boolean biometricSupported = BiometricUtil.isSupported(requireContext());
|
||||
binding.settingsBiometricLock.setEnabled(SettingsUtil.isDatabaseEncrypted(requireContext()) && biometricSupported);
|
||||
binding.settingsBiometricLock.setChecked(SettingsUtil.isBiometricEncryption(requireContext()));
|
||||
|
||||
if(!biometricSupported) {
|
||||
binding.settingsBiometricLockInfo.setVisibility(View.VISIBLE);
|
||||
binding.settingsBiometricLockInfo.setText(R.string.biometric_encryption_unavailable);
|
||||
}
|
||||
|
||||
if(biometricSupported) {
|
||||
binding.settingsBiometricLock.setOnCheckedChangeListener((view, checked) -> {
|
||||
if(checked) {
|
||||
|
||||
}else {
|
||||
|
||||
SettingsUtil.disableBiometricEncryption(requireContext());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
binding.settingsScreenSecurity.setChecked(SettingsUtil.isScreenSecurity(requireContext()));
|
||||
binding.settingsScreenSecurity.setOnCheckedChangeListener((view, checked) -> {
|
||||
SettingsUtil.setScreenSecurity(requireContext(), checked);
|
||||
requireActivity().recreate();
|
||||
});
|
||||
|
||||
binding.settingsHideCodes.setChecked(SettingsUtil.isHideCodes(requireContext()));
|
||||
binding.settingsHideCodes.setOnCheckedChangeListener((view, checked) -> SettingsUtil.setHideCodes(requireContext(), checked));
|
||||
|
||||
binding.settingsShowImages.setChecked(SettingsUtil.isShowImages(requireContext()));
|
||||
binding.settingsShowImages.setOnCheckedChangeListener((view, checked) -> SettingsUtil.setShowImages(requireContext(), checked));
|
||||
|
||||
String[] themeNames = new String[Theme.values().length];
|
||||
for(int i = 0; i < Theme.values().length; i++) {
|
||||
themeNames[i] = getResources().getString(Theme.values()[i].getName());
|
||||
}
|
||||
|
||||
binding.settingsEnableIntroVideo.setChecked(SettingsUtil.isIntroVideoEnabled(requireContext()));
|
||||
binding.settingsEnableIntroVideo.setOnCheckedChangeListener((view, checked) -> SettingsUtil.setEnableIntroVideo(requireContext(), checked));
|
||||
|
||||
binding.settingsEnableThemedBackground.setChecked(SettingsUtil.isThemedBackgroundEnabled(requireContext()));
|
||||
binding.settingsEnableThemedBackground.setOnCheckedChangeListener((view, checked) -> {
|
||||
SettingsUtil.setEnableThemedBackground(requireContext(), checked);
|
||||
requireActivity().recreate();
|
||||
});
|
||||
|
||||
binding.settingsTheme.setAdapter(new ArrayAdapter<>(requireContext(), android.R.layout.simple_list_item_1, themeNames));
|
||||
binding.settingsTheme.setSelection(SettingsUtil.getTheme(requireContext()).ordinal());
|
||||
binding.settingsTheme.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
Theme theme = Theme.values()[position];
|
||||
if(theme == SettingsUtil.getTheme(requireContext())) return;
|
||||
|
||||
SettingsUtil.setTheme(requireContext(), theme);
|
||||
requireActivity().recreate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {}
|
||||
});
|
||||
|
||||
binding.settingsEnableMinimalistTheme.setChecked(SettingsUtil.isMinimalistThemeEnabled(requireContext()));
|
||||
binding.settingsEnableMinimalistTheme.setOnCheckedChangeListener((view, checked) -> {
|
||||
SettingsUtil.setEnableMinimalistTheme(requireContext(), checked);
|
||||
requireActivity().recreate();
|
||||
});
|
||||
|
||||
String[] appearanceNames = new String[Appearance.values().length];
|
||||
for(int i = 0; i < Appearance.values().length; i++) {
|
||||
appearanceNames[i] = getResources().getString(Appearance.values()[i].getName());
|
||||
}
|
||||
|
||||
binding.settingsAppearance.setAdapter(new ArrayAdapter<>(requireContext(), android.R.layout.simple_list_item_1, appearanceNames));
|
||||
binding.settingsAppearance.setSelection(SettingsUtil.getAppearance(requireContext()).ordinal());
|
||||
binding.settingsAppearance.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
Appearance appearance = Appearance.values()[position];
|
||||
if(appearance == SettingsUtil.getAppearance(requireContext())) return;
|
||||
|
||||
SettingsUtil.setAppearance(requireContext(), appearance);
|
||||
requireActivity().recreate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {}
|
||||
});
|
||||
|
||||
binding.settingsCreateBackup.setOnClickListener(view -> {
|
||||
new StyledDialogBuilder(requireContext())
|
||||
.setTitle(R.string.create_backup)
|
||||
.setItems(R.array.backup_create, (d, which) -> {
|
||||
switch(which) {
|
||||
case 0:
|
||||
break;
|
||||
case 1:
|
||||
break;
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, (d, which) -> {})
|
||||
.show();
|
||||
});
|
||||
|
||||
binding.settingsManageIconPacks.setOnClickListener(v -> {
|
||||
List<String> brokenPacks = new ArrayList<>();
|
||||
List<IconPack> packs = IconUtil.loadAllIconPacks(requireContext(), brokenPacks::add);
|
||||
|
||||
if(!brokenPacks.isEmpty()) {
|
||||
DialogUtil.showYesNo(requireContext(), R.string.broken_icon_packs_title, R.string.broken_icon_packs_message, () -> {
|
||||
for(String pack : brokenPacks) {
|
||||
IconUtil.removeIconPack(requireContext(), pack);
|
||||
}
|
||||
}, null);
|
||||
}
|
||||
|
||||
if(packs.isEmpty()) {
|
||||
Toast.makeText(requireContext(), R.string.no_icon_packs_installed, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
DialogManageIconPacksBinding binding = DialogManageIconPacksBinding.inflate(getLayoutInflater());
|
||||
|
||||
binding.manageIconPacksList.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||
binding.manageIconPacksList.setAdapter(new IconPackListAdapter(requireContext(), IconUtil.loadAllIconPacks(requireContext())));
|
||||
|
||||
new StyledDialogBuilder(requireContext())
|
||||
.setTitle(R.string.manage_icon_packs_title)
|
||||
.setView(binding.getRoot())
|
||||
.setPositiveButton(R.string.ok, (d, which) -> {})
|
||||
.show();
|
||||
});
|
||||
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
private void createBackup(SecretKey key, CryptoParameters parameters) {}
|
||||
|
||||
private void loadBackup(Uri uri) {
|
||||
|
||||
}
|
||||
|
||||
private void loadBackup(Uri uri, SecretKey key, CryptoParameters parameters) throws BackupException, CryptoException {}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
this.binding = null;
|
||||
}
|
||||
|
||||
}
|
21
app/src/main/java/com/example/onetap_ssh/icon/Icon.java
Normal file
@ -0,0 +1,21 @@
|
||||
package com.example.onetap_ssh.icon;
|
||||
|
||||
public class Icon {
|
||||
|
||||
private final IconMetadata metadata;
|
||||
private final byte[] bytes;
|
||||
|
||||
public Icon(IconMetadata metadata, byte[] bytes) {
|
||||
this.metadata = metadata;
|
||||
this.bytes = bytes;
|
||||
}
|
||||
|
||||
public IconMetadata getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
public byte[] getBytes() {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
package com.example.onetap_ssh.icon;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseExpandableListAdapter;
|
||||
import android.widget.ExpandableListView;
|
||||
|
||||
import androidx.core.util.Consumer;
|
||||
|
||||
import com.example.onetap_ssh.R;
|
||||
import com.example.onetap_ssh.databinding.IconListCategoryBinding;
|
||||
import com.example.onetap_ssh.databinding.IconListIconBinding;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
public class IconListAdapter extends BaseExpandableListAdapter implements ExpandableListView.OnChildClickListener {
|
||||
|
||||
private final Context context;
|
||||
|
||||
private final Map<String, List<Icon>> icons;
|
||||
private final List<String> categories;
|
||||
|
||||
private Map<String, List<Icon>> filteredIcons;
|
||||
|
||||
private final Consumer<Icon> selected;
|
||||
|
||||
public IconListAdapter(Context context, Map<String, List<Icon>> icons, Consumer<Icon> selected) {
|
||||
this.context = context;
|
||||
this.icons = icons;
|
||||
this.categories = new ArrayList<>(icons.keySet());
|
||||
this.filteredIcons = new TreeMap<>(icons);
|
||||
this.selected = selected;
|
||||
}
|
||||
|
||||
public void filter(String query) {
|
||||
Map<String, List<Icon>> filtered = new TreeMap<>();
|
||||
for(String cat : categories) {
|
||||
List<Icon> f = new ArrayList<>();
|
||||
for(Icon i : icons.get(cat)) {
|
||||
if(i.getMetadata().getName().toLowerCase().contains(query.toLowerCase())) {
|
||||
f.add(i);
|
||||
}
|
||||
}
|
||||
filtered.put(cat, f);
|
||||
}
|
||||
|
||||
filteredIcons = filtered;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getGroupCount() {
|
||||
return categories.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChildrenCount(int groupPosition) {
|
||||
return filteredIcons.get(categories.get(groupPosition)).size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGroup(int groupPosition) {
|
||||
return categories.get(groupPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Icon getChild(int groupPosition, int childPosition) {
|
||||
return filteredIcons.get(categories.get(groupPosition)).get(childPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getGroupId(int groupPosition) {
|
||||
return groupPosition;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getChildId(int groupPosition, int childPosition) {
|
||||
return childPosition;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasStableIds() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
|
||||
IconListCategoryBinding binding = IconListCategoryBinding.inflate(LayoutInflater.from(context));
|
||||
binding.getRoot().setText(getGroup(groupPosition));
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
|
||||
IconListIconBinding binding = IconListIconBinding.inflate(LayoutInflater.from(context));
|
||||
|
||||
Icon icon = getChild(groupPosition, childPosition);
|
||||
binding.iconListIconImage.setImageResource(R.drawable.codeguard_white);
|
||||
IconUtil.loadImage(binding.iconListIconImage, icon.getBytes(), v -> v.setImageDrawable(new ColorDrawable(Color.TRANSPARENT)));
|
||||
binding.iconListIconText.setText(icon.getMetadata().getName());
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChildSelectable(int groupPosition, int childPosition) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) {
|
||||
selected.accept(getChild(groupPosition, childPosition));
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package com.example.onetap_ssh.icon;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class IconMetadata {
|
||||
|
||||
private String name;
|
||||
private String filename;
|
||||
private String category;
|
||||
private String[] issuer;
|
||||
|
||||
private IconMetadata() {}
|
||||
|
||||
public String getName() {
|
||||
if(name != null) return name;
|
||||
|
||||
String fileName = new File(filename).getName();
|
||||
int i = fileName.lastIndexOf('.');
|
||||
return i == -1 ? fileName : fileName.substring(0, i);
|
||||
}
|
||||
|
||||
public String getFilename() {
|
||||
return filename;
|
||||
}
|
||||
|
||||
public String getCategory() {
|
||||
return category;
|
||||
}
|
||||
|
||||
public String[] getIssuer() {
|
||||
return issuer;
|
||||
}
|
||||
|
||||
public boolean validate() {
|
||||
return filename != null && category != null && issuer != null;
|
||||
}
|
||||
|
||||
}
|
33
app/src/main/java/com/example/onetap_ssh/icon/IconPack.java
Normal file
@ -0,0 +1,33 @@
|
||||
package com.example.onetap_ssh.icon;
|
||||
|
||||
public class IconPack {
|
||||
|
||||
private final IconPackMetadata metadata;
|
||||
private final Icon[] icons;
|
||||
|
||||
public IconPack(IconPackMetadata metadata, Icon[] icons) {
|
||||
this.metadata = metadata;
|
||||
this.icons = icons;
|
||||
}
|
||||
|
||||
public IconPackMetadata getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
public Icon[] getIcons() {
|
||||
return icons;
|
||||
}
|
||||
|
||||
public Icon findIconForIssuer(String issuer) {
|
||||
for(Icon icon : icons) {
|
||||
for(String i : icon.getMetadata().getIssuer()) {
|
||||
if(issuer.equalsIgnoreCase(i)) {
|
||||
return icon;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package com.example.onetap_ssh.icon;
|
||||
|
||||
public class IconPackException extends Exception {
|
||||
public IconPackException() {
|
||||
}
|
||||
|
||||
public IconPackException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public IconPackException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public IconPackException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package com.example.onetap_ssh.icon;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.example.onetap_ssh.databinding.DialogManageIconPacksItemBinding;
|
||||
|
||||
public class IconPackItem extends RecyclerView.ViewHolder {
|
||||
|
||||
private final DialogManageIconPacksItemBinding binding;
|
||||
|
||||
private IconPack pack;
|
||||
|
||||
public IconPackItem(@NonNull DialogManageIconPacksItemBinding binding) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
}
|
||||
|
||||
public DialogManageIconPacksItemBinding getBinding() {
|
||||
return binding;
|
||||
}
|
||||
|
||||
public void setPack(IconPack pack) {
|
||||
this.pack = pack;
|
||||
}
|
||||
|
||||
public IconPack getPack() {
|
||||
return pack;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
package com.example.onetap_ssh.icon;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.example.onetap_ssh.R;
|
||||
import com.example.onetap_ssh.databinding.DialogManageIconPacksItemBinding;
|
||||
import com.example.onetap_ssh.util.DialogUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class IconPackListAdapter extends RecyclerView.Adapter<IconPackItem> {
|
||||
|
||||
private final Context context;
|
||||
|
||||
private final LayoutInflater inflater;
|
||||
|
||||
private final List<IconPack> packs;
|
||||
|
||||
public IconPackListAdapter(Context context, List<IconPack> packs) {
|
||||
this.context = context;
|
||||
this.inflater = LayoutInflater.from(context);
|
||||
this.packs = packs;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public IconPackItem onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return new IconPackItem(DialogManageIconPacksItemBinding.inflate(inflater, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull IconPackItem holder, int position) {
|
||||
IconPack pack = packs.get(position);
|
||||
holder.setPack(pack);
|
||||
|
||||
holder.getBinding().iconPackName.setText(pack.getMetadata().getName());
|
||||
|
||||
holder.getBinding().iconPackDelete.setOnClickListener(view -> {
|
||||
DialogUtil.showYesNo(context, R.string.delete_pack_title, R.string.delete_pack_message, () -> {
|
||||
IconUtil.removeIconPack(context, pack.getMetadata().getUuid());
|
||||
|
||||
int idx = packs.indexOf(pack);
|
||||
packs.remove(idx);
|
||||
notifyItemRemoved(idx);
|
||||
}, null);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return packs.size();
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package com.example.onetap_ssh.icon;
|
||||
|
||||
public class IconPackMetadata {
|
||||
|
||||
private String uuid;
|
||||
private String name;
|
||||
private int version;
|
||||
private IconMetadata[] icons;
|
||||
|
||||
private IconPackMetadata() {}
|
||||
|
||||
public void setUuid(String uuid) {
|
||||
this.uuid = uuid;
|
||||
}
|
||||
|
||||
public String getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public int getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public IconMetadata[] getIcons() {
|
||||
return icons;
|
||||
}
|
||||
|
||||
public boolean validate() {
|
||||
if(uuid == null || name == null || icons == null) return false;
|
||||
|
||||
for(IconMetadata i : icons) {
|
||||
if(!i.validate()) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
382
app/src/main/java/com/example/onetap_ssh/icon/IconUtil.java
Normal file
@ -0,0 +1,382 @@
|
||||
package com.example.onetap_ssh.icon;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.Rect;
|
||||
import android.net.Uri;
|
||||
import android.util.Base64;
|
||||
|
||||
import androidx.core.util.Consumer;
|
||||
|
||||
import com.caverock.androidsvg.SVG;
|
||||
import com.caverock.androidsvg.SVGImageView;
|
||||
import com.caverock.androidsvg.SVGParseException;
|
||||
import com.example.onetap_ssh.util.IOUtil;
|
||||
import com.example.onetap_ssh.util.SettingsUtil;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
public class IconUtil {
|
||||
|
||||
private static final int ICON_SIZE = 128;
|
||||
|
||||
// Source: https://sashamaps.net/docs/resources/20-colors/
|
||||
private static final List<Integer> DISTINCT_COLORS = Collections.unmodifiableList(Arrays.asList(
|
||||
Color.parseColor("#e6194B"), // Red
|
||||
Color.parseColor("#f58231"), // Orange
|
||||
// Color.parseColor("#ffe119"), // Yellow
|
||||
// Color.parseColor("#bfef45"), // Lime
|
||||
Color.parseColor("#3cb44b"), // Green
|
||||
// Color.parseColor("#42d4f4"), // Cyan
|
||||
Color.parseColor("#4363d8"), // Blue
|
||||
Color.parseColor("#911eb4"), // Purple
|
||||
Color.parseColor("#f032e6"), // Magenta
|
||||
// Color.parseColor("#a9a9a9"), // Grey
|
||||
Color.parseColor("#800000"), // Maroon
|
||||
Color.parseColor("#9A6324"), // Brown
|
||||
Color.parseColor("#808000"), // Olive
|
||||
Color.parseColor("#469990"), // Teal
|
||||
Color.parseColor("#000075") // Navy
|
||||
// Color.parseColor("#000000"), // Black
|
||||
// Color.parseColor("#fabed4"), // Pink
|
||||
// Color.parseColor("#ffd8b1"), // Apricot
|
||||
// Color.parseColor("#fffac8"), // Beige
|
||||
// Color.parseColor("#aaffc3"), // Mint
|
||||
// Color.parseColor("#dcbeff"), // Lavender
|
||||
// Color.parseColor("#ffffff") // White
|
||||
));
|
||||
|
||||
private static final Map<String, IconPack> loadedPacks = new HashMap<>();
|
||||
|
||||
private static File getIconPacksDir(Context context) {
|
||||
File iconPacksDir = new File(context.getFilesDir(), "iconpacks");
|
||||
if(!iconPacksDir.exists()) {
|
||||
iconPacksDir.mkdirs();
|
||||
}
|
||||
return iconPacksDir;
|
||||
}
|
||||
|
||||
public static void importIconPack(Context context, Uri uri) throws IconPackException {
|
||||
IconPackMetadata meta = loadPackMetadata(context, uri);
|
||||
|
||||
File iconPackFile = new File(getIconPacksDir(context), meta.getUuid());
|
||||
|
||||
try {
|
||||
if (!iconPackFile.exists()) {
|
||||
iconPackFile.createNewFile();
|
||||
}
|
||||
|
||||
try (OutputStream out = new BufferedOutputStream(new FileOutputStream(iconPackFile));
|
||||
InputStream in = context.getContentResolver().openInputStream(uri)) {
|
||||
if(in == null) throw new IconPackException("Failed to read icon pack");
|
||||
byte[] bytes = IOUtil.readBytes(in);
|
||||
out.write(bytes);
|
||||
}
|
||||
}catch(IOException e) {
|
||||
throw new IconPackException("Failed to import icon pack", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void importIconPack(Context context, Uri uri, String newName, String newUUID) throws IconPackException {
|
||||
IconPackMetadata meta = loadPackMetadata(context, uri);
|
||||
meta.setName(newName);
|
||||
meta.setUuid(newUUID);
|
||||
|
||||
File iconPackFile = new File(getIconPacksDir(context), meta.getUuid());
|
||||
|
||||
try {
|
||||
if (!iconPackFile.exists()) {
|
||||
iconPackFile.createNewFile();
|
||||
}
|
||||
|
||||
try (InputStream in = context.getContentResolver().openInputStream(uri)) {
|
||||
if(in == null) throw new IconPackException("Failed to read icon pack");
|
||||
writeRenamedPack(in, iconPackFile, meta);
|
||||
}
|
||||
}catch(IOException e) {
|
||||
throw new IconPackException("Failed to import icon pack", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void renameIconPack(Context context, IconPack pack, String newName, String newUUID) throws IconPackException {
|
||||
File packFile = new File(getIconPacksDir(context), pack.getMetadata().getUuid());
|
||||
if(!packFile.exists()) return;
|
||||
|
||||
File newPackFile = new File(getIconPacksDir(context), newUUID);
|
||||
|
||||
String oldName = pack.getMetadata().getName();
|
||||
String oldUUID = pack.getMetadata().getUuid();
|
||||
|
||||
|
||||
loadedPacks.remove(oldUUID);
|
||||
|
||||
pack.getMetadata().setName(newName);
|
||||
pack.getMetadata().setUuid(newUUID);
|
||||
|
||||
try {
|
||||
writeRenamedPack(new BufferedInputStream(new FileInputStream(packFile)), newPackFile, pack.getMetadata());
|
||||
packFile.delete();
|
||||
}catch(IconPackException e) {
|
||||
pack.getMetadata().setName(oldName);
|
||||
pack.getMetadata().setUuid(oldUUID);
|
||||
throw e;
|
||||
} catch (FileNotFoundException e) {
|
||||
throw new IconPackException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void removeIconPack(Context context, String uuid) {
|
||||
File packFile = new File(getIconPacksDir(context), uuid);
|
||||
packFile.delete();
|
||||
loadedPacks.remove(uuid);
|
||||
}
|
||||
|
||||
public static IconPackMetadata loadPackMetadata(Context context, Uri uri) throws IconPackException {
|
||||
try(InputStream in = context.getContentResolver().openInputStream(uri)) {
|
||||
if(in == null) throw new IconPackException("Failed to read icon pack");
|
||||
try(ZipInputStream zIn = new ZipInputStream(in)) {
|
||||
ZipEntry en;
|
||||
while((en = zIn.getNextEntry()) != null) {
|
||||
if(en.isDirectory()) continue;
|
||||
|
||||
if(en.getName().equals("pack.json")) {
|
||||
byte[] entryBytes = readEntry(zIn, en);
|
||||
return SettingsUtil.GSON.fromJson(new String(entryBytes, StandardCharsets.UTF_8), IconPackMetadata.class); // TODO: validate metadata
|
||||
}
|
||||
}
|
||||
}
|
||||
}catch(IOException e) {
|
||||
throw new IconPackException("Failed to read icon pack", e);
|
||||
}
|
||||
|
||||
throw new IconPackException("No pack.json");
|
||||
}
|
||||
|
||||
private static void writeRenamedPack(InputStream oldFile, File newFile, IconPackMetadata meta) throws IconPackException {
|
||||
try(ZipInputStream in = new ZipInputStream(oldFile);
|
||||
ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(newFile)))) {
|
||||
ZipEntry en;
|
||||
while((en = in.getNextEntry()) != null) {
|
||||
if(en.isDirectory()) continue;
|
||||
|
||||
byte[] entryBytes = readEntry(in, en);
|
||||
if(en.getName().equals("pack.json")) {
|
||||
out.putNextEntry(new ZipEntry("pack.json"));
|
||||
out.write(SettingsUtil.GSON.toJson(meta).getBytes(StandardCharsets.UTF_8));
|
||||
continue;
|
||||
}
|
||||
|
||||
out.putNextEntry(new ZipEntry(en.getName()));
|
||||
out.write(entryBytes);
|
||||
}
|
||||
}catch(IOException e) {
|
||||
throw new IconPackException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static Map<String, List<Icon>> loadAllIcons(Context context) {
|
||||
List<IconPack> packs = loadAllIconPacks(context);
|
||||
|
||||
Map<String, List<Icon>> icons = new TreeMap<>();
|
||||
for(IconPack pack : packs) {
|
||||
for(Icon i : pack.getIcons()) {
|
||||
String category = i.getMetadata().getCategory();
|
||||
List<Icon> is = icons.get(category);
|
||||
if(is != null) {
|
||||
is.add(i);
|
||||
}else {
|
||||
is = new ArrayList<>();
|
||||
is.add(i);
|
||||
icons.put(category, is);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return icons;
|
||||
}
|
||||
|
||||
public static List<IconPack> loadAllIconPacks(Context context, Consumer<String> brokenPack) {
|
||||
File iconPacksDir = getIconPacksDir(context);
|
||||
|
||||
String[] packIDs = iconPacksDir.list();
|
||||
if(packIDs == null) return Collections.emptyList();
|
||||
|
||||
List<IconPack> packs = new ArrayList<>();
|
||||
for(String pack : packIDs) {
|
||||
try {
|
||||
IconPack p = loadIconPack(context, pack);
|
||||
if(p == null) continue;
|
||||
if(!p.getMetadata().getUuid().equals(pack)) throw new IconPackException("Invalid metadata");
|
||||
packs.add(p);
|
||||
}catch(IconPackException e) {
|
||||
e.printStackTrace();
|
||||
if(brokenPack != null) brokenPack.accept(pack);
|
||||
//DialogUtil.showErrorDialog(context, "An icon pack failed to load", e);
|
||||
}
|
||||
}
|
||||
|
||||
return packs;
|
||||
}
|
||||
|
||||
public static List<IconPack> loadAllIconPacks(Context context) {
|
||||
return loadAllIconPacks(context, null);
|
||||
}
|
||||
|
||||
public static IconPack loadIconPack(Context context, String uuid) throws IconPackException {
|
||||
if(loadedPacks.containsKey(uuid)) return loadedPacks.get(uuid);
|
||||
|
||||
IconPack p = loadIconPack(new File(getIconPacksDir(context), uuid));
|
||||
if(p == null) return null;
|
||||
|
||||
loadedPacks.put(uuid, p);
|
||||
return p;
|
||||
}
|
||||
|
||||
private static IconPack loadIconPack(File file) throws IconPackException {
|
||||
if(!file.exists()) return null;
|
||||
|
||||
try(ZipInputStream in = new ZipInputStream(new BufferedInputStream(new FileInputStream(file)))) {
|
||||
IconPackMetadata metadata = null;
|
||||
Map<String, byte[]> files = new HashMap<>();
|
||||
|
||||
ZipEntry en;
|
||||
while((en = in.getNextEntry()) != null) {
|
||||
if(en.isDirectory()) continue;
|
||||
|
||||
byte[] entryBytes = readEntry(in, en);
|
||||
|
||||
if(en.getName().equals("pack.json")) {
|
||||
metadata = SettingsUtil.GSON.fromJson(new String(entryBytes, StandardCharsets.UTF_8), IconPackMetadata.class); // TODO: validate metadata
|
||||
}else {
|
||||
files.put(en.getName(), entryBytes);
|
||||
}
|
||||
}
|
||||
|
||||
if(metadata == null) throw new IconPackException("Missing icon pack metadata");
|
||||
|
||||
Icon[] icons = new Icon[metadata.getIcons().length];
|
||||
int iconCount = 0;
|
||||
for(IconMetadata m : metadata.getIcons()) {
|
||||
byte[] bytes = files.get(m.getFilename());
|
||||
if(bytes == null) continue;
|
||||
icons[iconCount++] = new Icon(m, bytes);
|
||||
}
|
||||
|
||||
Icon[] workingIcons = new Icon[iconCount];
|
||||
System.arraycopy(icons, 0, workingIcons, 0, iconCount);
|
||||
|
||||
return new IconPack(metadata, workingIcons);
|
||||
}catch(IOException e) {
|
||||
throw new IconPackException("Failed to read icon pack", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] readEntry(ZipInputStream in, ZipEntry en) throws IOException {
|
||||
if (en.getSize() > Integer.MAX_VALUE) {
|
||||
throw new IOException("Invalid ZIP entry");
|
||||
}
|
||||
|
||||
return IOUtil.readBytes(in);
|
||||
}
|
||||
|
||||
public static Bitmap generateCodeImage(String issuer, String name) {
|
||||
if(issuer == null || issuer.isEmpty()) issuer = name;
|
||||
if(issuer == null || issuer.isEmpty()) issuer = "?";
|
||||
|
||||
Bitmap b = Bitmap.createBitmap(ICON_SIZE, ICON_SIZE, Bitmap.Config.ARGB_8888);
|
||||
Canvas c = new Canvas(b);
|
||||
|
||||
Paint p = new Paint();
|
||||
p.setColor(DISTINCT_COLORS.get(Math.abs(issuer.hashCode()) % DISTINCT_COLORS.size()));
|
||||
p.setStyle(Paint.Style.FILL);
|
||||
c.drawCircle(ICON_SIZE / 2, ICON_SIZE / 2, ICON_SIZE / 2, p);
|
||||
|
||||
p.setColor(Color.WHITE);
|
||||
p.setAntiAlias(true);
|
||||
p.setTextSize(64);
|
||||
|
||||
String text = issuer.substring(0, 1);
|
||||
Rect r = new Rect();
|
||||
p.getTextBounds(text, 0, text.length(), r);
|
||||
c.drawText(text, ICON_SIZE / 2 - r.exactCenterX(), ICON_SIZE / 2 - r.exactCenterY(), p);
|
||||
|
||||
return b;
|
||||
}
|
||||
|
||||
public static void loadEffectiveImage(Context context, String imageData, String issuer, String name, SVGImageView view, Consumer<String> setOTPImage) {
|
||||
|
||||
}
|
||||
|
||||
public static void loadImage(SVGImageView view, byte[] imageBytes, Consumer<SVGImageView> fallback) {
|
||||
if(imageBytes == null) {
|
||||
if(fallback != null) fallback.accept(view);
|
||||
return;
|
||||
}
|
||||
|
||||
Bitmap bm = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
|
||||
if(bm != null) {
|
||||
view.setImageBitmap(bm);
|
||||
}else {
|
||||
try {
|
||||
SVG svg = SVG.getFromInputStream(new ByteArrayInputStream(imageBytes));
|
||||
view.setSVG(svg);
|
||||
}catch(SVGParseException e) {
|
||||
if(fallback != null) fallback.accept(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static Bitmap cutToIcon(Bitmap bitmap) {
|
||||
Bitmap b = Bitmap.createBitmap(ICON_SIZE, ICON_SIZE, Bitmap.Config.ARGB_8888);
|
||||
Canvas c = new Canvas(b);
|
||||
|
||||
double sourceRatio = bitmap.getWidth() / (double) bitmap.getHeight();
|
||||
int newWidth, newHeight, offsetX, offsetY;
|
||||
if(sourceRatio < 1) {
|
||||
newWidth = ICON_SIZE;
|
||||
newHeight = (int) (newWidth / sourceRatio);
|
||||
offsetX = 0;
|
||||
offsetY = (ICON_SIZE - newHeight) / 2;
|
||||
}else {
|
||||
newHeight = ICON_SIZE;
|
||||
newWidth = (int) (newHeight * sourceRatio);
|
||||
offsetX = (ICON_SIZE - newWidth) / 2;
|
||||
offsetY = 0;
|
||||
}
|
||||
|
||||
Paint p = new Paint();
|
||||
Path path = new Path();
|
||||
path.addCircle(ICON_SIZE / 2, ICON_SIZE / 2, ICON_SIZE / 2, Path.Direction.CW);
|
||||
c.clipPath(path);
|
||||
c.drawBitmap(bitmap, null, new Rect(offsetX, offsetY, offsetX + newWidth, offsetY + newHeight), p);
|
||||
|
||||
return b;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
package com.example.onetap_ssh.unlock;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.example.onetap_ssh.BaseActivity;
|
||||
import com.example.onetap_ssh.MainActivity;
|
||||
import com.example.onetap_ssh.R;
|
||||
import com.example.onetap_ssh.crypto.BiometricKey;
|
||||
import com.example.onetap_ssh.crypto.Crypto;
|
||||
import com.example.onetap_ssh.crypto.CryptoException;
|
||||
import com.example.onetap_ssh.databinding.ActivityUnlockBinding;
|
||||
import com.example.onetap_ssh.util.BiometricUtil;
|
||||
import com.example.onetap_ssh.util.DialogUtil;
|
||||
import com.example.onetap_ssh.util.SettingsUtil;
|
||||
import com.example.onetap_ssh.util.ThemeUtil;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class UnlockActivity extends BaseActivity {
|
||||
|
||||
private ActivityUnlockBinding binding;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if(!SettingsUtil.isDatabaseEncrypted(this)) {
|
||||
success();
|
||||
return;
|
||||
}
|
||||
|
||||
binding = ActivityUnlockBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
ThemeUtil.loadBackground(this);
|
||||
|
||||
if(SettingsUtil.isBiometricEncryption(this) && BiometricUtil.isSupported(this)) {
|
||||
Runnable onSuccess = () -> {
|
||||
|
||||
};
|
||||
|
||||
binding.unlockBiometrics.setOnClickListener(view -> BiometricUtil.promptBiometricAuth(this, onSuccess, () -> {}));
|
||||
BiometricUtil.promptBiometricAuth(this, onSuccess, () -> {});
|
||||
}else {
|
||||
binding.unlockBiometrics.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
binding.unlockButton.setOnClickListener(view -> {
|
||||
if(binding.unlockPassword.getText().length() == 0) {
|
||||
DialogUtil.showErrorDialog(this, getString(R.string.error_unlock_no_password));
|
||||
return;
|
||||
}
|
||||
|
||||
String password = binding.unlockPassword.getText().toString();
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
private void success() {
|
||||
if(getIntent() != null && getIntent().hasExtra("contract")) {
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
Intent m = new Intent(getApplicationContext(), MainActivity.class);
|
||||
m.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NO_ANIMATION);
|
||||
startActivity(m);
|
||||
finish();
|
||||
}
|
||||
|
||||
private void failure() {
|
||||
setResult(RESULT_CANCELED);
|
||||
finish();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package com.example.onetap_ssh.unlock;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.activity.result.contract.ActivityResultContract;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class UnlockContract extends ActivityResultContract<Void, Boolean> {
|
||||
@NonNull
|
||||
@Override
|
||||
public Intent createIntent(@NonNull Context context, Void unused) {
|
||||
return new Intent(context, UnlockActivity.class).putExtra("contract", true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean parseResult(int i, @Nullable Intent intent) {
|
||||
return i == Activity.RESULT_OK;
|
||||
}
|
||||
}
|
45
app/src/main/java/com/example/onetap_ssh/util/AppLocale.java
Normal file
@ -0,0 +1,45 @@
|
||||
package com.example.onetap_ssh.util;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import com.example.onetap_ssh.R;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public enum AppLocale {
|
||||
|
||||
SYSTEM_DEFAULT(R.string.locale_system_default),
|
||||
ENGLISH(Locale.ENGLISH),
|
||||
GERMAN(Locale.GERMAN),
|
||||
FRENCH(Locale.FRENCH),
|
||||
POLISH(new Locale("pl")),
|
||||
|
||||
UKRAINIAN(new Locale("uk")),
|
||||
;
|
||||
|
||||
@StringRes
|
||||
private final int name;
|
||||
|
||||
private final Locale locale;
|
||||
|
||||
AppLocale(@StringRes int name) {
|
||||
this.name = name;
|
||||
this.locale = null;
|
||||
}
|
||||
|
||||
AppLocale(Locale locale) {
|
||||
this.name = 0;
|
||||
this.locale = locale;
|
||||
}
|
||||
|
||||
public String getName(Context context) {
|
||||
return locale == null ? context.getString(name) : locale.getDisplayName(locale);
|
||||
}
|
||||
|
||||
public Locale getLocale() {
|
||||
return locale;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package com.example.onetap_ssh.util;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
|
||||
import com.example.onetap_ssh.R;
|
||||
|
||||
public enum Appearance {
|
||||
|
||||
FOLLOW_SYSTEM(R.string.appearance_follow_system, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM),
|
||||
LIGHT(R.string.appearance_light, AppCompatDelegate.MODE_NIGHT_NO),
|
||||
DARK(R.string.appearance_dark, AppCompatDelegate.MODE_NIGHT_YES),
|
||||
;
|
||||
|
||||
@StringRes
|
||||
private final int name;
|
||||
|
||||
@AppCompatDelegate.NightMode
|
||||
private final int value;
|
||||
|
||||
Appearance(@StringRes int name, @AppCompatDelegate.NightMode int value) {
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@StringRes
|
||||
public int getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@AppCompatDelegate.NightMode
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package com.example.onetap_ssh.util;
|
||||
|
||||
public class BackupException extends Exception {
|
||||
public BackupException() {
|
||||
}
|
||||
|
||||
public BackupException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public BackupException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public BackupException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
package com.example.onetap_ssh.util;
|
||||
|
||||
import static androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG;
|
||||
import static androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.biometric.BiometricManager;
|
||||
import androidx.biometric.BiometricPrompt;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
|
||||
import com.example.onetap_ssh.R;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
public class BiometricUtil {
|
||||
|
||||
public static boolean isSupported(Context context) {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && BiometricManager.from(context).canAuthenticate(BIOMETRIC_STRONG | DEVICE_CREDENTIAL) == BiometricManager.BIOMETRIC_SUCCESS;
|
||||
}
|
||||
|
||||
public static void promptBiometricAuth(FragmentActivity context, Runnable success, Runnable failure) {
|
||||
if(!isSupported(context)) {
|
||||
failure.run();
|
||||
return;
|
||||
}
|
||||
|
||||
Executor executor = ContextCompat.getMainExecutor(context);
|
||||
BiometricPrompt prompt = new BiometricPrompt(context, executor, new BiometricPrompt.AuthenticationCallback() {
|
||||
@Override
|
||||
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
|
||||
failure.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
|
||||
success.run();
|
||||
}
|
||||
});
|
||||
|
||||
BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(context.getString(R.string.app_name))
|
||||
.setSubtitle(context.getString(R.string.biometric_lock_subtitle))
|
||||
.setAllowedAuthenticators(BIOMETRIC_STRONG | DEVICE_CREDENTIAL)
|
||||
.build();
|
||||
|
||||
prompt.authenticate(info);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package com.example.onetap_ssh.util;
|
||||
|
||||
public interface DialogCallback {
|
||||
|
||||
boolean callback();
|
||||
|
||||
}
|
275
app/src/main/java/com/example/onetap_ssh/util/DialogUtil.java
Normal file
@ -0,0 +1,275 @@
|
||||
package com.example.onetap_ssh.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.util.Consumer;
|
||||
|
||||
import com.example.onetap_ssh.R;
|
||||
import com.example.onetap_ssh.backup.BackupData;
|
||||
import com.example.onetap_ssh.databinding.DialogCreateGroupBinding;
|
||||
import com.example.onetap_ssh.databinding.DialogErrorBinding;
|
||||
import com.example.onetap_ssh.databinding.DialogInputPasswordBinding;
|
||||
import com.example.onetap_ssh.databinding.DialogSetPasswordBinding;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class DialogUtil {
|
||||
|
||||
private static final Integer[] DIGITS = new Integer[]{6, 7, 8, 9, 10, 11, 12};
|
||||
|
||||
private static void showCodeDialog(Context context, View view, DialogCallback ok) {
|
||||
AlertDialog dialog = new StyledDialogBuilder(context)
|
||||
.setTitle(R.string.code_input_title)
|
||||
.setView(view)
|
||||
.setPositiveButton(R.string.ok, (btnView, which) -> {})
|
||||
.setNegativeButton(R.string.cancel, (btnView, which) -> {})
|
||||
.create();
|
||||
|
||||
dialog.setOnShowListener(d -> {
|
||||
Button okButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
okButton.setOnClickListener(v -> {
|
||||
if(ok.callback()) dialog.dismiss();
|
||||
});
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
public static void showErrorDialog(Context context, String errorMessage, String details, Runnable closed) {
|
||||
DialogErrorBinding binding = DialogErrorBinding.inflate(LayoutInflater.from(context));
|
||||
|
||||
binding.errorMessage.setText(errorMessage);
|
||||
|
||||
AlertDialog.Builder b = new StyledDialogBuilder(context)
|
||||
.setTitle(R.string.failed_title)
|
||||
.setView(binding.getRoot())
|
||||
.setPositiveButton(R.string.ok, (d, which) -> {})
|
||||
.setOnDismissListener(d -> { if(closed != null) closed.run(); });
|
||||
|
||||
if(details != null) {
|
||||
binding.errorDetailsText.setText(details);
|
||||
b.setNeutralButton("Details", (d, which) -> {});
|
||||
}
|
||||
|
||||
AlertDialog dialog = b.create();
|
||||
|
||||
if(details != null) {
|
||||
dialog.setOnShowListener(d -> {
|
||||
Button detailsButton = dialog.getButton(AlertDialog.BUTTON_NEUTRAL);
|
||||
detailsButton.setOnClickListener(v -> binding.errorDetails.setVisibility(binding.errorDetails.getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE));
|
||||
});
|
||||
}
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
public static void showErrorDialog(Context context, String errorMessage, Runnable closed) {
|
||||
showErrorDialog(context, errorMessage, (String) null, closed);
|
||||
}
|
||||
|
||||
public static void showErrorDialog(Context context, String errorMessage) {
|
||||
showErrorDialog(context, errorMessage, (Runnable) null);
|
||||
}
|
||||
|
||||
public static void showErrorDialog(Context context, String errorMessage, Exception exception, Runnable closed) {
|
||||
showErrorDialog(context, errorMessage, stackTraceToString(exception), closed);
|
||||
}
|
||||
|
||||
public static void showErrorDialog(Context context, String errorMessage, Exception exception) {
|
||||
showErrorDialog(context, errorMessage, stackTraceToString(exception), null);
|
||||
}
|
||||
|
||||
private static String stackTraceToString(Throwable t) {
|
||||
StringBuilder b = new StringBuilder();
|
||||
|
||||
b.append(t.toString()).append('\n');
|
||||
for(StackTraceElement e : t.getStackTrace()) {
|
||||
b.append(" ").append(e.toString()).append('\n');
|
||||
}
|
||||
|
||||
if(t.getCause() != null) {
|
||||
b.append("Caused by: ").append(stackTraceToString(t.getCause()));
|
||||
}
|
||||
|
||||
return b.toString().trim();
|
||||
}
|
||||
|
||||
public static void showCreateGroupDialog(LayoutInflater inflater, String initialName, Consumer<String> callback, Runnable onDismiss) {
|
||||
Context context = inflater.getContext();
|
||||
|
||||
DialogCreateGroupBinding binding = DialogCreateGroupBinding.inflate(inflater);
|
||||
binding.createGroupName.setText(initialName);
|
||||
|
||||
AlertDialog dialog = new StyledDialogBuilder(context)
|
||||
.setTitle(R.string.action_new_group)
|
||||
.setView(binding.getRoot())
|
||||
.setPositiveButton(R.string.add, (view, which) -> {})
|
||||
.setNegativeButton(R.string.cancel, (view, which) -> { if(onDismiss != null) onDismiss.run(); })
|
||||
.create();
|
||||
|
||||
dialog.setOnShowListener(d -> {
|
||||
Button okButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
okButton.setOnClickListener(v -> {
|
||||
if(binding.createGroupName.getText().length() == 0) {
|
||||
DialogUtil.showErrorDialog(context, context.getString(R.string.new_group_missing_title));
|
||||
return;
|
||||
}
|
||||
|
||||
dialog.dismiss();
|
||||
callback.accept(binding.createGroupName.getText().toString());
|
||||
if(onDismiss != null) onDismiss.run();
|
||||
});
|
||||
});
|
||||
|
||||
dialog.setOnCancelListener(d -> { if(onDismiss != null) onDismiss.run(); });
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
public static void showChooseGroupDialog(Context context, Consumer<String> callback, Runnable onDismiss) {
|
||||
List<String> groups = SettingsUtil.getGroups(context);
|
||||
String[] groupNames = new String[groups.size() + 1];
|
||||
|
||||
groupNames[0] = context.getString(R.string.uri_handler_create_group);
|
||||
for(int i = 0; i < groups.size(); i++) {
|
||||
groupNames[i + 1] = SettingsUtil.getGroupName(context, groups.get(i));
|
||||
}
|
||||
|
||||
AlertDialog dialog = new StyledDialogBuilder(context)
|
||||
.setTitle(R.string.uri_handler_add_code_title)
|
||||
.setItems(groupNames, (d, which) -> {
|
||||
if(which == 0) { // Create New Group
|
||||
DialogUtil.showCreateGroupDialog(LayoutInflater.from(context), null, group -> {
|
||||
String id = UUID.randomUUID().toString();
|
||||
SettingsUtil.addGroup(context, id, group);
|
||||
callback.accept(id);
|
||||
}, onDismiss);
|
||||
return;
|
||||
}
|
||||
|
||||
callback.accept(groups.get(which - 1));
|
||||
if(onDismiss != null) onDismiss.run();
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, (d, which) -> { if(onDismiss != null) onDismiss.run(); })
|
||||
.setOnCancelListener(d -> { if(onDismiss != null) onDismiss.run(); })
|
||||
.create();
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
public static void showSetPasswordDialog(Context context, Consumer<String> callback, Runnable onCancel) {
|
||||
DialogSetPasswordBinding binding = DialogSetPasswordBinding.inflate(LayoutInflater.from(context));
|
||||
|
||||
AlertDialog dialog = new StyledDialogBuilder(context)
|
||||
.setTitle(R.string.set_password)
|
||||
.setView(binding.getRoot())
|
||||
.setPositiveButton(R.string.ok, (d, which) -> {})
|
||||
.setNegativeButton(R.string.cancel, (d, which) -> { if(onCancel != null) onCancel.run(); })
|
||||
.setOnCancelListener(d -> { if(onCancel != null) onCancel.run(); })
|
||||
.create();
|
||||
|
||||
dialog.setOnShowListener(d -> {
|
||||
Button okButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
okButton.setOnClickListener(v -> {
|
||||
if(binding.setPassword.getText().length() == 0) {
|
||||
DialogUtil.showErrorDialog(context, "You need to enter a password");
|
||||
return;
|
||||
}
|
||||
|
||||
String pass = binding.setPassword.getText().toString();
|
||||
String confirm = binding.confirmPassword.getText().toString();
|
||||
if(!pass.equals(confirm)) {
|
||||
DialogUtil.showErrorDialog(context, "The passwords do not match");
|
||||
return;
|
||||
}
|
||||
|
||||
dialog.dismiss();
|
||||
callback.accept(pass);
|
||||
});
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
public static void showInputPasswordDialog(Context context, Consumer<String> callback, Runnable onCancel) {
|
||||
DialogInputPasswordBinding binding = DialogInputPasswordBinding.inflate(LayoutInflater.from(context));
|
||||
|
||||
AlertDialog dialog = new StyledDialogBuilder(context)
|
||||
.setTitle("Input Password")
|
||||
.setView(binding.getRoot())
|
||||
.setPositiveButton(R.string.ok, (d, which) -> {})
|
||||
.setNegativeButton(R.string.cancel, (d, which) -> { if(onCancel != null) onCancel.run(); })
|
||||
.setOnCancelListener(d -> { if(onCancel != null) onCancel.run(); })
|
||||
.create();
|
||||
|
||||
dialog.setOnShowListener(d -> {
|
||||
Button okButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
okButton.setOnClickListener(v -> {
|
||||
if(binding.inputPassword.getText().length() == 0) {
|
||||
DialogUtil.showErrorDialog(context, "You need to enter a password");
|
||||
return;
|
||||
}
|
||||
|
||||
dialog.dismiss();
|
||||
callback.accept(binding.inputPassword.getText().toString());
|
||||
});
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
public static void showYesNo(Context context, @StringRes int title, @StringRes int message, @StringRes int yesText, @StringRes int noText, Runnable yes, Runnable no) {
|
||||
new StyledDialogBuilder(context)
|
||||
.setTitle(title)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(yesText, (d, w) -> {
|
||||
if(yes != null) yes.run();
|
||||
})
|
||||
.setNegativeButton(noText, (d, w) -> {
|
||||
if(no != null) no.run();
|
||||
})
|
||||
.show()
|
||||
.setCanceledOnTouchOutside(false);
|
||||
}
|
||||
|
||||
public static void showYesNo(Context context, @StringRes int title, @StringRes int message, Runnable yes, Runnable no) {
|
||||
showYesNo(context, title, message, R.string.yes, R.string.no, yes, no);
|
||||
}
|
||||
|
||||
public static void showYesNoCancel(Context context, @StringRes int title, @StringRes int message, @StringRes int yesText, @StringRes int noText, @StringRes int cancelText, Runnable yes, Runnable no, Runnable cancel) {
|
||||
new StyledDialogBuilder(context)
|
||||
.setTitle(title)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(yesText, (d, w) -> {
|
||||
if(yes != null) yes.run();
|
||||
})
|
||||
.setNegativeButton(noText, (d, w) -> {
|
||||
if(no != null) no.run();
|
||||
})
|
||||
.setNeutralButton(cancelText, (d, w) -> d.cancel())
|
||||
.setOnCancelListener(d -> {
|
||||
if(cancel != null) cancel.run();
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
public static void showYesNoCancel(Context context, @StringRes int title, @StringRes int message, Runnable yes, Runnable no, Runnable cancel) {
|
||||
showYesNoCancel(context, title, message, R.string.yes, R.string.no, R.string.cancel, yes, no, cancel);
|
||||
}
|
||||
|
||||
public static void showBackupLoadedDialog(Context context, BackupData data) {
|
||||
AlertDialog dialog = new StyledDialogBuilder(context)
|
||||
.setTitle("Backup loaded")
|
||||
.setMessage(String.format("Successfully loaded %s group(s) from the backup", data.getGroups().length))
|
||||
.setPositiveButton(R.string.ok, (d, which) -> {})
|
||||
.create();
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
}
|
37
app/src/main/java/com/example/onetap_ssh/util/IOUtil.java
Normal file
@ -0,0 +1,37 @@
|
||||
package com.example.onetap_ssh.util;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class IOUtil {
|
||||
|
||||
public static byte[] readBytes(File file) throws IOException {
|
||||
try(InputStream fIn = new BufferedInputStream(new FileInputStream(file))) {
|
||||
ByteBuffer fileBuffer = ByteBuffer.allocate((int) file.length());
|
||||
byte[] buffer = new byte[1024];
|
||||
int len;
|
||||
while ((len = fIn.read(buffer)) > 0) {
|
||||
fileBuffer.put(buffer, 0, len);
|
||||
}
|
||||
|
||||
return fileBuffer.array();
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] readBytes(InputStream in) throws IOException {
|
||||
ByteArrayOutputStream bOut = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[1024];
|
||||
int len;
|
||||
while ((len = in.read(buffer)) > 0) {
|
||||
bOut.write(buffer, 0, len);
|
||||
}
|
||||
|
||||
return bOut.toByteArray();
|
||||
}
|
||||
|
||||
}
|
246
app/src/main/java/com/example/onetap_ssh/util/SettingsUtil.java
Normal file
@ -0,0 +1,246 @@
|
||||
package com.example.onetap_ssh.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import com.example.onetap_ssh.backup.BackupGroup;
|
||||
import com.example.onetap_ssh.crypto.BiometricKey;
|
||||
import com.example.onetap_ssh.crypto.CryptoParameters;
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class SettingsUtil {
|
||||
|
||||
public static final String
|
||||
GROUPS_PREFS_NAME = "groups",
|
||||
GENERAL_PREFS_NAME = "general";
|
||||
|
||||
public static final Gson GSON = new Gson();
|
||||
|
||||
public static List<String> getGroups(Context ctx) {
|
||||
SharedPreferences prefs = ctx.getSharedPreferences(GROUPS_PREFS_NAME, Context.MODE_PRIVATE);
|
||||
return Arrays.asList(GSON.fromJson(prefs.getString("groups", "[]"), String[].class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Only for reordering groups. Don't add/delete groups with this!
|
||||
* @param groups Groups
|
||||
*/
|
||||
public static void setGroups(Context ctx, List<String> groups) {
|
||||
SharedPreferences prefs = ctx.getSharedPreferences(GROUPS_PREFS_NAME, Context.MODE_PRIVATE);
|
||||
prefs.edit().putString("groups", GSON.toJson(groups)).apply();
|
||||
}
|
||||
|
||||
public static void restoreGroups(Context ctx, BackupGroup[] groups) {
|
||||
List<String> oldGroups = getGroups(ctx);
|
||||
for(String group : oldGroups) removeGroup(ctx, group);
|
||||
|
||||
List<String> newGroups = new ArrayList<>();
|
||||
for(BackupGroup group : groups) {
|
||||
newGroups.add(group.getId());
|
||||
setGroupName(ctx, group.getId(), group.getName());
|
||||
}
|
||||
|
||||
setGroups(ctx, newGroups);
|
||||
}
|
||||
|
||||
public static void addGroup(Context ctx, String group, String groupName) {
|
||||
List<String> groups = new ArrayList<>(getGroups(ctx));
|
||||
groups.add(group);
|
||||
|
||||
SharedPreferences prefs = ctx.getSharedPreferences(GROUPS_PREFS_NAME, Context.MODE_PRIVATE);
|
||||
prefs.edit().putString("groups", GSON.toJson(groups)).apply();
|
||||
|
||||
setGroupName(ctx, group, groupName);
|
||||
}
|
||||
|
||||
public static void removeGroup(Context ctx, String group) {
|
||||
List<String> groups = new ArrayList<>(getGroups(ctx));
|
||||
groups.remove(group);
|
||||
|
||||
SharedPreferences prefs = ctx.getSharedPreferences(GROUPS_PREFS_NAME, Context.MODE_PRIVATE);
|
||||
prefs.edit().putString("groups", GSON.toJson(groups)).apply();
|
||||
|
||||
deleteGroupData(ctx, group);
|
||||
}
|
||||
|
||||
public static String getGroupName(Context ctx, String group) {
|
||||
return ctx.getSharedPreferences(GROUPS_PREFS_NAME, Context.MODE_PRIVATE).getString("group." + group + ".name", group);
|
||||
}
|
||||
|
||||
public static void setGroupName(Context ctx, String group, String name) {
|
||||
ctx.getSharedPreferences(GROUPS_PREFS_NAME, Context.MODE_PRIVATE).edit()
|
||||
.putString("group." + group + ".name", name)
|
||||
.apply();
|
||||
}
|
||||
|
||||
private static void deleteGroupData(Context ctx, String group) {
|
||||
ctx.getSharedPreferences(GROUPS_PREFS_NAME, Context.MODE_PRIVATE).edit()
|
||||
.remove("group." + group + ".otps")
|
||||
.remove("group." + group + ".name")
|
||||
.apply();
|
||||
}
|
||||
|
||||
public static boolean isDatabaseEncrypted(Context ctx) {
|
||||
return ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE).getBoolean("encryption", false);
|
||||
}
|
||||
|
||||
public static void enableEncryption(Context ctx, CryptoParameters parameters) {
|
||||
ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE).edit()
|
||||
.putBoolean("encryption", true)
|
||||
.putString("encryption.parameters", GSON.toJson(parameters))
|
||||
.apply();
|
||||
}
|
||||
|
||||
public static void disableEncryption(Context ctx) {
|
||||
ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE).edit()
|
||||
.putBoolean("encryption", false)
|
||||
.remove("encryption.parameters")
|
||||
.apply();
|
||||
}
|
||||
|
||||
public static CryptoParameters getCryptoParameters(Context ctx) {
|
||||
return GSON.fromJson(ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE).getString("encryption.parameters", "{}"), CryptoParameters.class);
|
||||
}
|
||||
|
||||
public static void enableBiometricEncryption(Context ctx, BiometricKey biometricKey) {
|
||||
ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE).edit()
|
||||
.putBoolean("encryption.biometric", true)
|
||||
.putString("encryption.biometric.key", GSON.toJson(biometricKey))
|
||||
.apply();
|
||||
}
|
||||
|
||||
public static void disableBiometricEncryption(Context ctx) {
|
||||
ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE).edit()
|
||||
.putBoolean("encryption.biometric", false)
|
||||
.remove("encryption.biometric.key")
|
||||
.apply();
|
||||
}
|
||||
|
||||
public static boolean isBiometricEncryption(Context ctx) {
|
||||
return ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE).getBoolean("encryption.biometric", false);
|
||||
}
|
||||
|
||||
public static BiometricKey getBiometricKey(Context ctx) {
|
||||
String encoded = ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE).getString("encryption.biometric.key", null);
|
||||
if(encoded == null) return null;
|
||||
return GSON.fromJson(encoded, BiometricKey.class);
|
||||
}
|
||||
|
||||
public static void setEnableIntroVideo(Context ctx, boolean enableIntroVideo) {
|
||||
SharedPreferences prefs = ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE);
|
||||
prefs.edit().putBoolean("enableIntroVideo", enableIntroVideo).apply();
|
||||
}
|
||||
|
||||
public static boolean isIntroVideoEnabled(Context ctx) {
|
||||
return ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE).getBoolean("enableIntroVideo", true);
|
||||
}
|
||||
|
||||
public static void setEnableThemedBackground(Context ctx, boolean enableThemedBackground) {
|
||||
SharedPreferences prefs = ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE);
|
||||
prefs.edit().putBoolean("enableThemedBackground", enableThemedBackground).apply();
|
||||
}
|
||||
|
||||
public static boolean isThemedBackgroundEnabled(Context ctx) {
|
||||
return ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE).getBoolean("enableThemedBackground", true);
|
||||
}
|
||||
|
||||
public static void setEnableMinimalistTheme(Context ctx, boolean enableMinimalistTheme) {
|
||||
SharedPreferences prefs = ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE);
|
||||
prefs.edit().putBoolean("enableMinimalistTheme", enableMinimalistTheme).apply();
|
||||
}
|
||||
|
||||
public static boolean isMinimalistThemeEnabled(Context ctx) {
|
||||
return ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE).getBoolean("enableMinimalistTheme", false);
|
||||
}
|
||||
|
||||
public static void setScreenSecurity(Context ctx, boolean screenSecurity) {
|
||||
SharedPreferences prefs = ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE);
|
||||
prefs.edit().putBoolean("screenSecurity", screenSecurity).apply();
|
||||
}
|
||||
|
||||
public static boolean isScreenSecurity(Context ctx) {
|
||||
return ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE).getBoolean("screenSecurity", true);
|
||||
}
|
||||
|
||||
public static void setHideCodes(Context ctx, boolean hideCodes) {
|
||||
SharedPreferences prefs = ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE);
|
||||
prefs.edit().putBoolean("hideCodes", hideCodes).apply();
|
||||
}
|
||||
|
||||
public static boolean isHideCodes(Context ctx) {
|
||||
return ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE).getBoolean("hideCodes", false);
|
||||
}
|
||||
|
||||
public static void setFirstLaunch(Context ctx, boolean firstLaunch) {
|
||||
SharedPreferences prefs = ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE);
|
||||
prefs.edit().putBoolean("firstLaunch", firstLaunch).apply();
|
||||
}
|
||||
|
||||
public static boolean isFirstLaunch(Context ctx) {
|
||||
return ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE).getBoolean("firstLaunch", true);
|
||||
}
|
||||
|
||||
public static void setShowImages(Context ctx, boolean showImages) {
|
||||
SharedPreferences prefs = ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE);
|
||||
prefs.edit().putBoolean("showImages", showImages).apply();
|
||||
}
|
||||
|
||||
public static boolean isShowImages(Context ctx) {
|
||||
return ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE).getBoolean("showImages", true);
|
||||
}
|
||||
|
||||
public static void setTheme(Context ctx, Theme theme) {
|
||||
SharedPreferences prefs = ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE);
|
||||
prefs.edit().putString("theme", theme.name()).apply();
|
||||
}
|
||||
|
||||
public static Theme getTheme(Context ctx) {
|
||||
String themeId = ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE).getString("theme", Theme.BLUE_GREEN.name());
|
||||
try {
|
||||
return Theme.valueOf(themeId);
|
||||
}catch(IllegalArgumentException e) {
|
||||
return Theme.BLUE_GREEN;
|
||||
}
|
||||
}
|
||||
|
||||
public static void setAppearance(Context ctx, Appearance appearance) {
|
||||
SharedPreferences prefs = ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE);
|
||||
prefs.edit().putString("appearance", appearance.name()).apply();
|
||||
}
|
||||
|
||||
public static Appearance getAppearance(Context ctx) {
|
||||
String themeId = ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE).getString("appearance", Appearance.FOLLOW_SYSTEM.name());
|
||||
try {
|
||||
return Appearance.valueOf(themeId);
|
||||
}catch(IllegalArgumentException e) {
|
||||
return Appearance.FOLLOW_SYSTEM;
|
||||
}
|
||||
}
|
||||
|
||||
public static void setLocale(Context ctx, AppLocale locale) {
|
||||
SharedPreferences prefs = ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE);
|
||||
prefs.edit().putString("locale", locale.name()).apply();
|
||||
}
|
||||
|
||||
public static AppLocale getLocale(Context ctx) {
|
||||
String lang = ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE).getString("locale", AppLocale.ENGLISH.name());
|
||||
try {
|
||||
return AppLocale.valueOf(lang);
|
||||
}catch(IllegalArgumentException e) {
|
||||
return AppLocale.SYSTEM_DEFAULT;
|
||||
}
|
||||
}
|
||||
|
||||
public static void enableSuperSecretHamburgers(Context ctx) {
|
||||
ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE).edit().putBoolean("iLikeHamburgers", true).apply();
|
||||
}
|
||||
|
||||
public static boolean isSuperSecretHamburgersEnabled(Context ctx) {
|
||||
return ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE).getBoolean("iLikeHamburgers", false);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package com.example.onetap_ssh.util;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.example.onetap_ssh.R;
|
||||
|
||||
public class StyledDialogBuilder extends AlertDialog.Builder {
|
||||
|
||||
public StyledDialogBuilder(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public StyledDialogBuilder(Context context, int themeResId) {
|
||||
super(context, themeResId);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public AlertDialog create() {
|
||||
AlertDialog dialog = super.create();
|
||||
|
||||
TypedArray arr = dialog.getContext().obtainStyledAttributes(new int[] {R.attr.dialogBackground});
|
||||
try {
|
||||
dialog.getWindow().setBackgroundDrawable(arr.getDrawable(0));
|
||||
|
||||
if(SettingsUtil.isScreenSecurity(getContext())) {
|
||||
dialog.getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
|
||||
}
|
||||
}finally {
|
||||
arr.close();
|
||||
}
|
||||
|
||||
return dialog;
|
||||
}
|
||||
|
||||
}
|
57
app/src/main/java/com/example/onetap_ssh/util/Theme.java
Normal file
@ -0,0 +1,57 @@
|
||||
package com.example.onetap_ssh.util;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.annotation.StyleRes;
|
||||
|
||||
import com.example.onetap_ssh.R;
|
||||
|
||||
public enum Theme {
|
||||
|
||||
BLUE_GREEN(R.string.theme_blue_green, R.style.Theme_OneTap_SSH_Blue_Green, R.drawable.background_blue_green_light, R.drawable.background_blue_green),
|
||||
RED_BLUE(R.string.theme_red_blue, R.style.Theme_OneTap_SSH_Red_Blue, R.drawable.background_red_blue_light, R.drawable.background_red_blue),
|
||||
PINK_GREEN(R.string.theme_pink_green, R.style.Theme_OneTap_SSH_Pink_Green, R.drawable.background_pink_green_light, R.drawable.background_pink_green),
|
||||
BLUE_YELLOW(R.string.theme_blue_yellow, R.style.Theme_OneTap_SSH_Blue_Yellow, R.drawable.background_blue_yellow_light, R.drawable.background_blue_yellow),
|
||||
GREEN_YELLOW(R.string.theme_green_yellow, R.style.Theme_OneTap_SSH_Green_Yellow, R.drawable.background_green_yellow_light, R.drawable.background_green_yellow),
|
||||
ORANGE_TURQUOISE(R.string.theme_orange_turquoise, R.style.Theme_OneTap_SSH_Orange_Turquoise, R.drawable.background_orange_turquoise_light, R.drawable.background_orange_turquoise),
|
||||
;
|
||||
|
||||
@StringRes
|
||||
private final int name;
|
||||
|
||||
@StyleRes
|
||||
private final int style;
|
||||
|
||||
@DrawableRes
|
||||
private final int lightBackground;
|
||||
|
||||
@DrawableRes
|
||||
private final int darkBackground;
|
||||
|
||||
Theme(@StringRes int name, @StyleRes int style, @DrawableRes int lightBackground, @DrawableRes int darkBackground) {
|
||||
this.name = name;
|
||||
this.style = style;
|
||||
this.lightBackground = lightBackground;
|
||||
this.darkBackground = darkBackground;
|
||||
}
|
||||
|
||||
@StringRes
|
||||
public int getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@StyleRes
|
||||
public int getStyle() {
|
||||
return style;
|
||||
}
|
||||
|
||||
@DrawableRes
|
||||
public int getLightBackground() {
|
||||
return lightBackground;
|
||||
}
|
||||
|
||||
@DrawableRes
|
||||
public int getDarkBackground() {
|
||||
return darkBackground;
|
||||
}
|
||||
}
|
57
app/src/main/java/com/example/onetap_ssh/util/ThemeUtil.java
Normal file
@ -0,0 +1,57 @@
|
||||
package com.example.onetap_ssh.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
|
||||
import com.example.onetap_ssh.R;
|
||||
|
||||
public class ThemeUtil {
|
||||
|
||||
public static void loadTheme(AppCompatActivity activity) {
|
||||
Theme theme = SettingsUtil.getTheme(activity);
|
||||
activity.setTheme(theme.getStyle());
|
||||
|
||||
if(SettingsUtil.isMinimalistThemeEnabled(activity)) {
|
||||
activity.getTheme().applyStyle(R.style.Theme_CringeAuthenticator_Minimalist, true);
|
||||
}
|
||||
|
||||
AppCompatDelegate.setDefaultNightMode(SettingsUtil.getAppearance(activity).getValue());
|
||||
}
|
||||
|
||||
@DrawableRes
|
||||
public static int getBackground(Context context) {
|
||||
if(!SettingsUtil.isThemedBackgroundEnabled(context)) return 0;
|
||||
|
||||
Theme theme = SettingsUtil.getTheme(context);
|
||||
|
||||
int nightMode = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||
boolean isNightMode;
|
||||
switch(nightMode) {
|
||||
case Configuration.UI_MODE_NIGHT_NO:
|
||||
default:
|
||||
isNightMode = false;
|
||||
break;
|
||||
case Configuration.UI_MODE_NIGHT_YES:
|
||||
isNightMode = true;
|
||||
break;
|
||||
}
|
||||
|
||||
return !isNightMode ? theme.getLightBackground() : theme.getDarkBackground();
|
||||
}
|
||||
|
||||
public static void loadBackground(AppCompatActivity activity) {
|
||||
if(!SettingsUtil.isThemedBackgroundEnabled(activity)) return;
|
||||
|
||||
int background = getBackground(activity);
|
||||
View v = activity.findViewById(R.id.app_background);
|
||||
if(v != null) {
|
||||
v.setBackgroundResource(background);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
BIN
app/src/main/res/drawable/background_blue_green.jpg
Normal file
After Width: | Height: | Size: 62 KiB |
BIN
app/src/main/res/drawable/background_blue_green_light.jpg
Normal file
After Width: | Height: | Size: 53 KiB |
BIN
app/src/main/res/drawable/background_blue_yellow.jpg
Normal file
After Width: | Height: | Size: 67 KiB |
BIN
app/src/main/res/drawable/background_blue_yellow_light.jpg
Normal file
After Width: | Height: | Size: 65 KiB |
BIN
app/src/main/res/drawable/background_green_yellow.jpg
Normal file
After Width: | Height: | Size: 62 KiB |
BIN
app/src/main/res/drawable/background_green_yellow_light.jpg
Normal file
After Width: | Height: | Size: 54 KiB |
BIN
app/src/main/res/drawable/background_orange_turquoise.jpg
Normal file
After Width: | Height: | Size: 62 KiB |
BIN
app/src/main/res/drawable/background_orange_turquoise_light.jpg
Normal file
After Width: | Height: | Size: 67 KiB |
BIN
app/src/main/res/drawable/background_pink_green.jpg
Normal file
After Width: | Height: | Size: 68 KiB |
BIN
app/src/main/res/drawable/background_pink_green_light.jpg
Normal file
After Width: | Height: | Size: 73 KiB |
BIN
app/src/main/res/drawable/background_red_blue.jpg
Normal file
After Width: | Height: | Size: 57 KiB |
BIN
app/src/main/res/drawable/background_red_blue_light.jpg
Normal file
After Width: | Height: | Size: 57 KiB |
5
app/src/main/res/drawable/baseline_delete_24.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="?android:attr/textColor"
|
||||
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="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
|
||||
</vector>
|
5
app/src/main/res/drawable/baseline_edit_24.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="?android:attr/textColor"
|
||||
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="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
|
||||
</vector>
|
5
app/src/main/res/drawable/baseline_lock_open_24.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="?android:attr/textColor"
|
||||
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="M12,17c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6h1.9c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM18,20L6,20L6,10h12v10z"/>
|
||||
</vector>
|
13
app/src/main/res/drawable/button_simple.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape>
|
||||
<solid android:color="?attr/colorOnBackground" />
|
||||
<corners
|
||||
android:bottomLeftRadius="20dp"
|
||||
android:bottomRightRadius="20dp"
|
||||
android:topLeftRadius="20dp"
|
||||
android:topRightRadius="20dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
36
app/src/main/res/drawable/button_themed.xml
Normal file
@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<stroke
|
||||
android:width="0dp"
|
||||
android:color="#FFFFFF" />
|
||||
<gradient
|
||||
android:angle="180"
|
||||
android:startColor="?attr/colorTheme1"
|
||||
android:endColor="?attr/colorTheme2" />
|
||||
<corners
|
||||
android:bottomLeftRadius="20dp"
|
||||
android:bottomRightRadius="20dp"
|
||||
android:topLeftRadius="25dp"
|
||||
android:topRightRadius="25dp" />
|
||||
</shape>
|
||||
</item>
|
||||
<item
|
||||
android:bottom="2dp"
|
||||
android:left="0dp"
|
||||
android:right="0dp"
|
||||
android:top="0dp">
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<stroke
|
||||
android:width="0dp"
|
||||
android:color="#FFFFFF" />
|
||||
<solid android:color="?attr/colorOnBackground" />
|
||||
<corners
|
||||
android:bottomLeftRadius="20dp"
|
||||
android:bottomRightRadius="20dp"
|
||||
android:topLeftRadius="20dp"
|
||||
android:topRightRadius="20dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
28
app/src/main/res/drawable/codeguard_white.xml
Normal file
@ -0,0 +1,28 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="698dp"
|
||||
android:height="764dp"
|
||||
android:viewportWidth="698"
|
||||
android:viewportHeight="764">
|
||||
<path
|
||||
android:pathData="m298.48,1.72c-76.2,4.17 -159.05,18.76 -228.58,40.67 -22.21,7 -55.01,18.32 -56.01,19.33 -1.88,1.92 -8.63,47.21 -11.07,74.28 -0.59,6.55 -0.91,19.1 -1.1,32.87v40.72c0.19,13.6 0.51,25.97 1.09,32.41 5.75,63.48 18.36,118.85 40.43,177.5 18.45,49.06 46.7,102 77.08,144.5 22.98,32.14 41.88,54.35 71.11,83.55 43.51,43.48 85.16,75.08 140.42,106.56 8.59,4.89 16.24,8.89 17,8.89 0.76,0 7.52,-3.45 15.02,-7.67 30.21,-17.01 51.63,-31.06 79.24,-51.97 39.1,-29.62 83.35,-73.06 113.72,-111.63 76.62,-97.32 123.26,-211.26 136.61,-333.78 3.83,-35.13 4.54,-89.83 1.59,-122.45 -2.22,-24.6 -9.23,-72.07 -10.88,-73.74 -0.99,-1 -34.6,-12.61 -56.15,-19.41 -43.68,-13.77 -103.9,-27.01 -153.59,-33.79 -19.24,-2.62 -49.64,-5.33 -75.15,-6.84h-100.79zM698,1.72v380.28,382h-349,-347.28v1.72h698v-764h-1.72zM349.03,50.65c23.63,0 47.25,0.38 59.47,1.13 43.36,2.67 101.12,11.6 153.5,23.72 12.29,2.85 77.75,21.91 78.79,22.95 0.79,0.79 5.09,41.05 5.66,53.05 0.34,7.15 0.88,15.59 1.19,18.75l0.56,5.75 -5.62,-2.15c-3.09,-1.18 -5.8,-1.97 -6.03,-1.74 -0.23,0.23 -0.73,11.92 -1.13,25.99l-0.71,25.58 5.64,2.34c5.56,2.3 5.65,2.4 5.63,6.41 -0.03,9.28 -5.17,48.54 -8.97,68.57 -2.25,11.82 -4.29,22.07 -4.53,22.77 -0.35,0.99 -1.27,0.92 -4.21,-0.36 -5.84,-2.52 -7.21,-2.75 -7.69,-1.28 -0.24,0.75 -3,10.81 -6.14,22.36 -3.14,11.55 -5.9,21.72 -6.15,22.59 -0.32,1.14 1.1,2.27 5.07,4l5.52,2.41 -8.44,22.85c-8.2,22.2 -23.05,55.73 -32.03,72.31 -4.7,8.68 -23.14,40.4 -23.95,41.21 -0.27,0.27 -2.99,-0.3 -6.06,-1.27s-5.73,-1.6 -5.92,-1.41c-2.89,2.89 -28.45,40.43 -28.12,41.3 0.25,0.66 2.53,1.78 5.05,2.49 2.53,0.71 4.59,1.61 4.59,2.01s-7.05,9.23 -15.67,19.62c-15.93,19.21 -49.61,54.39 -52.06,54.39 -0.74,0 -3.11,-0.9 -5.27,-2s-4.59,-2 -5.41,-2c-1.15,0 -33.3,25.86 -40.47,32.54 -1.15,1.08 -0.42,1.78 4.41,4.22l5.79,2.93 -7.41,5.45c-19.73,14.53 -52.77,36.33 -58.23,38.43 -0.67,0.26 -11.93,-6.41 -25.02,-14.8 -64.72,-41.51 -124.55,-99.35 -167.43,-161.85 -18.27,-26.63 -21.41,-31.77 -36.53,-59.91 -17.06,-31.75 -32.15,-67.82 -42.67,-102 -9.47,-30.75 -9.89,-32.35 -14.91,-57 -5.88,-28.85 -6.83,-35.05 -9.6,-62.87 -2.15,-21.66 -2.52,-30.38 -2.52,-59.62 0,-29.33 0.36,-37.76 2.49,-58.63 3.57,-34.93 2.41,-31.14 10.31,-33.66 61.32,-19.58 114.36,-31.6 171.19,-38.78 19.52,-2.47 44.05,-5.01 54.5,-5.66 12.26,-0.75 35.9,-1.13 59.53,-1.13z"
|
||||
android:strokeWidth="3.4434"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="m349.3,72.66c-21.7,-0 -43.51,0.39 -55.3,1.19 -55.57,3.75 -110.9,12.54 -162,25.73 -14.59,3.77 -54.04,15.15 -54.69,15.78 -0.1,0.1 -1,8.05 -2,17.66 -1.43,13.86 -1.8,25.86 -1.77,57.98 0.03,35.85 0.29,42.62 2.3,59 3.17,25.84 5.62,40.87 9.78,60 20.31,93.28 60.38,175.14 122.35,250 21.42,25.88 65.46,67.6 94.03,89.08 8.07,6.07 45.95,31.92 46.77,31.92 0.21,0 6.7,-4.06 14.42,-9.02 13.24,-8.51 40.81,-28.4 40.81,-29.45 0,-0.26 -5.85,-3.91 -13,-8.11 -7.15,-4.2 -13,-8 -13,-8.44 0,-1.08 16.55,-13.98 17.94,-13.98 0.59,0 6.13,2.86 12.32,6.36 6.18,3.5 12.21,6.86 13.39,7.48 1.93,1.01 3.33,0.04 14.07,-9.73 12.21,-11.11 32.76,-31.79 44.72,-45 10.2,-11.27 29.77,-35.99 29.39,-37.13 -0.28,-0.84 -16.67,-8.85 -24.58,-12.02 -2.19,-0.88 -2.06,-1.16 4.35,-9.93 3.63,-4.96 6.89,-9.02 7.25,-9.02 0.36,0 6.28,2.47 13.16,5.48 6.88,3.01 13.07,5.49 13.77,5.5 4.05,0.06 37.55,-59.02 52.72,-92.98 5.97,-13.37 17.35,-43.51 21.5,-56.95 1.76,-5.7 3.41,-10.92 3.67,-11.6 0.31,-0.81 -4.1,-3.03 -12.84,-6.46 -7.32,-2.87 -13.61,-5.4 -13.98,-5.61 -0.93,-0.54 4.6,-20.69 5.78,-21.08 0.53,-0.18 6.76,1.95 13.84,4.73 7.08,2.78 13.18,4.7 13.57,4.27 1.33,-1.5 8.15,-35.85 11.07,-55.79 2.65,-18.09 5.91,-52.85 5.9,-63v-4l-14.75,-5.12 -14.75,-5.12 -0.28,-11.63c-0.15,-6.4 0.09,-11.63 0.54,-11.63 0.45,0 6.86,2.03 14.24,4.5s13.84,4.5 14.36,4.5c2.05,0 -2.14,-63.07 -4.33,-65.34 -1.95,-2.02 -53.9,-16.56 -79,-22.12 -44.92,-9.95 -89.36,-16.33 -137.03,-19.68 -11.39,-0.8 -33,-1.2 -54.7,-1.21zM363.24,151.17c0.38,0 0.87,0.08 1.51,0.2 1.51,0.29 7.7,1.26 13.75,2.15 25.03,3.69 55.23,16.47 76.82,32.51l10.82,8.04 -3.02,3.44c-3.59,4.08 -4.83,9.36 -3.15,13.4 3.61,8.72 14.83,11.18 21.01,4.6 1.3,-1.39 2.7,-2.52 3.09,-2.52s4.32,4.84 8.73,10.75c16.12,21.64 27.97,49.56 32.3,76.1 2.75,16.87 2.72,17.15 -1.85,17.16 -11.88,0.01 -17.48,13.82 -8.83,21.75 2.91,2.66 4.36,3.25 8.06,3.25 4.26,0 4.52,0.16 4.52,2.75 -0,1.51 -0.91,8.3 -2.01,15.08 -4.14,25.4 -15.58,52.75 -30.89,73.87 -4.46,6.16 -8.57,11.48 -9.13,11.82 -0.56,0.34 -2.39,-0.53 -4.07,-1.95 -7.57,-6.37 -18.01,-3.85 -20.95,5.08 -1.46,4.43 -0.41,8.76 3.12,12.77l2.88,3.28 -2.22,2.01c-13.24,11.95 -38.85,26.04 -59.48,32.72 -11.67,3.78 -34.2,8.5 -40.87,8.56 -0.62,0.01 -1.41,-2.14 -1.76,-4.78 -1.06,-8.03 -5.67,-12.21 -13.44,-12.21 -6.69,0 -12.18,6.3 -12.18,13.97 0,3.62 0.32,3.6 -16.16,0.98 -25.54,-4.05 -54.43,-16.18 -75.77,-31.81 -5.74,-4.2 -10.88,-7.94 -11.43,-8.31 -0.57,-0.39 0.34,-2.19 2.17,-4.27 4.06,-4.63 4.85,-7.93 3.09,-12.97 -3.3,-9.47 -15.29,-11.46 -22.31,-3.7l-1.9,2.11 -8.37,-11.25c-15.75,-21.18 -28.27,-50.42 -32.43,-75.75 -1.04,-6.32 -1.89,-12.96 -1.89,-14.75 -0,-3.19 0.09,-3.25 4.52,-3.25 3.7,0 5.14,-0.58 8.03,-3.25 8.7,-8.04 2.94,-21.74 -9.15,-21.75l-4.1,-0 2.22,-14.75c4.14,-27.5 15.19,-54.53 31.53,-77.08 4.43,-6.11 8.51,-11.4 9.08,-11.75s2.28,0.56 3.8,2.02c7.07,6.77 18.07,4.12 21.11,-5.09 1.47,-4.47 0.4,-8.77 -3.21,-12.89l-2.97,-3.38 10.82,-8.07c21.16,-15.79 51.75,-28.81 76.32,-32.48 5.78,-0.86 11.96,-1.81 13.75,-2.09 3.19,-0.51 3.25,-0.45 3.25,3.27 0.01,7.45 5.73,13.38 13.02,13.48 7.02,0.1 12.98,-6.33 12.98,-14.01 0,-2.32 0.11,-3.01 1.24,-3z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="m355.69,472.05c5.81,-3.54 5.77,-3.25 6.31,-44.85l0.5,-38.31 6.5,-2.19c8.83,-2.98 20.1,-10.7 26.03,-17.83 5.04,-6.05 11.25,-17.84 12.45,-23.62l0.68,-3.25h77.69l3.06,-2.57c4.52,-3.81 5.71,-9.26 3.22,-14.87 -3.38,-7.65 -2.85,-7.56 -45.88,-7.56h-37.85l-2.21,-6.87c-3.37,-10.46 -10.88,-21.03 -20.21,-28.42 -5.12,-4.06 -15.86,-9.41 -20.73,-10.33l-3.25,-0.61v-37.49c0,-32.95 -0.2,-37.91 -1.67,-41.01 -4.2,-8.86 -16.4,-9.74 -21.93,-1.58 -1.76,2.6 -1.94,5.61 -2.4,41.21l-0.5,38.4 -5.24,1.78c-18.55,6.3 -33.6,21.4 -38.87,39l-1.77,5.91h-38.16c-38.1,0 -38.17,0 -41.56,2.29 -7.14,4.8 -7.14,15.65 0,20.43 3.39,2.27 3.49,2.28 41.63,2.28h38.23l1.16,4.61c4.29,17.05 20.48,34.17 37.82,40l6.76,2.27 0.5,38.31c0.38,29.04 0.82,38.86 1.81,40.61 1.88,3.3 7.11,6.19 11.19,6.19 1.92,0 4.93,-0.87 6.69,-1.94z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="m314,443.57v-38.43l-5.75,-3.24c-12.63,-7.12 -23.54,-17.97 -31.32,-31.16l-3.39,-5.75h-38.27c-33.18,0 -38.27,0.2 -38.27,1.49 0,6.19 13.07,36.89 19.99,46.96 2.65,3.86 2.84,3.94 9.12,4.09 20.1,0.46 34.89,15.54 34.89,35.58 0,6.75 -0.08,6.68 15.44,14.96 8.62,4.6 32.69,13.81 36.31,13.9 0.98,0.02 1.25,-8.31 1.25,-38.4z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="m398.27,477.93c11.51,-3.83 24.82,-10.1 34.48,-16.25l4.25,-2.7 0.02,-6.74c0.04,-11.23 5.7,-22.2 14.64,-28.35 6.13,-4.21 11.79,-6.02 19.76,-6.3 6.65,-0.23 7.01,-0.37 9.28,-3.67 4.83,-7.02 12.74,-23.74 16.46,-34.78 2.11,-6.27 3.84,-12.02 3.84,-12.77 0,-1.14 -6.5,-1.37 -38.41,-1.37h-38.41l-3.52,6.25c-6.67,11.85 -17.17,22.41 -29.68,29.84l-6.97,4.14v38.39c0,25.35 0.35,38.39 1.02,38.39 0.56,0 6.52,-1.83 13.25,-4.07z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="m277.03,288.25c6.57,-11.72 17.96,-23.17 30.22,-30.37l6.75,-3.97v-37.9c0,-20.84 -0.37,-38.12 -0.82,-38.4 -0.89,-0.55 -20.72,5.91 -28.58,9.32 -2.69,1.17 -9.06,4.57 -14.14,7.56l-9.25,5.44 -0.36,7.8c-0.88,19.2 -14.84,33.18 -33.81,33.86l-7.55,0.27 -2.86,4.31c-6.76,10.19 -19.64,40.71 -19.64,46.52 0,1.03 7.7,1.29 38.4,1.29h38.4z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="m500.45,290.75c-2.47,-11.34 -11,-31.28 -18.65,-43.56l-3.31,-5.31 -7.71,-0.28c-18.87,-0.69 -32.5,-14.23 -33.58,-33.35l-0.46,-8.25 -5.62,-3.4c-10.88,-6.58 -21.75,-11.58 -33.62,-15.47 -6.6,-2.16 -12.34,-3.72 -12.75,-3.47 -0.41,0.26 -0.75,17.52 -0.75,38.36v37.9l7.25,4.28c9.56,5.64 23.23,19.24 28.71,28.56l4.27,7.25h76.93z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
BIN
app/src/main/res/drawable/cringestudios.png
Normal file
After Width: | Height: | Size: 95 KiB |
31
app/src/main/res/drawable/dialog_themed.xml
Normal file
@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<gradient
|
||||
android:angle="90"
|
||||
android:startColor="?attr/colorTheme1"
|
||||
android:endColor="?attr/colorTheme2" />
|
||||
<corners
|
||||
android:bottomLeftRadius="20dp"
|
||||
android:bottomRightRadius="25dp"
|
||||
android:topLeftRadius="20dp"
|
||||
android:topRightRadius="25dp" />
|
||||
</shape>
|
||||
</item>
|
||||
<item
|
||||
android:bottom="0dp"
|
||||
android:left="2dp"
|
||||
android:right="0dp"
|
||||
android:top="0dp">
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid
|
||||
android:color="?attr/colorOnBackground" />
|
||||
<corners
|
||||
android:bottomLeftRadius="20dp"
|
||||
android:bottomRightRadius="20dp"
|
||||
android:topLeftRadius="20dp"
|
||||
android:topRightRadius="20dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
BIN
app/src/main/res/drawable/jgcody.png
Normal file
After Width: | Height: | Size: 55 KiB |
31
app/src/main/res/drawable/menu_themed.xml
Normal file
@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<gradient
|
||||
android:angle="90"
|
||||
android:startColor="?attr/colorTheme1"
|
||||
android:endColor="?attr/colorTheme2" />
|
||||
<corners
|
||||
android:bottomLeftRadius="20dp"
|
||||
android:bottomRightRadius="20dp"
|
||||
android:topLeftRadius="20dp"
|
||||
android:topRightRadius="0dp" />
|
||||
</shape>
|
||||
</item>
|
||||
<item
|
||||
android:bottom="0dp"
|
||||
android:left="2dp"
|
||||
android:right="0dp"
|
||||
android:top="0dp">
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid
|
||||
android:color="?attr/colorOnBackground" />
|
||||
<corners
|
||||
android:bottomLeftRadius="20dp"
|
||||
android:bottomRightRadius="20dp"
|
||||
android:topLeftRadius="20dp"
|
||||
android:topRightRadius="0dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
21
app/src/main/res/drawable/theme_background.xml
Normal file
@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<stroke
|
||||
android:width="0dp"
|
||||
android:color="#FFFFFF" />
|
||||
<gradient
|
||||
android:angle="135"
|
||||
android:startColor="?attr/colorTheme1"
|
||||
android:startY="500"
|
||||
android:centerColor="?android:attr/colorBackground"
|
||||
android:endColor="?attr/colorTheme2" />
|
||||
</shape>
|
||||
<corners
|
||||
android:bottomLeftRadius="0dp"
|
||||
android:bottomRightRadius="0dp"
|
||||
android:topLeftRadius="0dp"
|
||||
android:topRightRadius="0dp" />
|
||||
</item>
|
||||
</layer-list>
|
19
app/src/main/res/drawable/theme_gradient.xml
Normal file
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<stroke
|
||||
android:width="0dp"
|
||||
android:color="#FFFFFF" />
|
||||
<gradient
|
||||
android:angle="180"
|
||||
android:startColor="?attr/colorTheme1"
|
||||
android:endColor="?attr/colorTheme2" />
|
||||
</shape>
|
||||
<corners
|
||||
android:bottomLeftRadius="0dp"
|
||||
android:bottomRightRadius="0dp"
|
||||
android:topLeftRadius="0dp"
|
||||
android:topRightRadius="0dp" />
|
||||
</item>
|
||||
</layer-list>
|
18
app/src/main/res/layout/activity_intro.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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:background="#000000">
|
||||
|
||||
<VideoView
|
||||
android:id="@+id/videoView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
@ -2,10 +2,10 @@
|
||||
<androidx.drawerlayout.widget.DrawerLayout 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:id="@+id/drawer_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
android:id="@+id/app_background"
|
||||
tools:openDrawer="start">
|
||||
|
||||
<include
|
||||
|
76
app/src/main/res/layout/activity_unlock.xml
Normal file
@ -0,0 +1,76 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/unlock_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/unlock_authenticator"
|
||||
android:textSize="21sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toTopOf="@+id/unlock_password_layout"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/unlock_password_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:passwordToggleEnabled="true">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/unlock_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ems="10"
|
||||
android:inputType="textPassword"
|
||||
android:hint="@string/password" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/unlock_button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:background="?attr/buttonBackground"
|
||||
android:text="@string/unlock"
|
||||
android:textAllCaps="false"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/unlock_password_layout" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/unlock_biometrics"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:background="?attr/buttonBackground"
|
||||
android:text="@string/unlock_using_biometrics"
|
||||
android:textAllCaps="false"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/unlock_button" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:scaleX="0.5"
|
||||
android:scaleY="0.5"
|
||||
app:layout_constraintBottom_toTopOf="@+id/unlock_title"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/baseline_lock_open_24" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -9,14 +9,14 @@
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:theme="@style/Theme.OneTapSSH.AppBarOverlay">
|
||||
android:theme="@style/Theme.OneTap_SSH.AppBarOverlay">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?attr/colorPrimary"
|
||||
app:popupTheme="@style/Theme.OneTapSSH.PopupOverlay" />
|
||||
app:popupTheme="@style/Theme.OneTap_SSH.PopupOverlay" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
|
23
app/src/main/res/layout/dialog_create_group.xml
Normal file
@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/create_group_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ems="10"
|
||||
android:hint="@string/name"
|
||||
android:inputType="text" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
50
app/src/main/res/layout/dialog_error.xml
Normal file
@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout 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:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/error_message"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Failed to do the thing you wanted to do" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/error_details"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:text="@string/details"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<HorizontalScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/error_details_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Lorem ipsum dolor sit amet something went wrong and we don't know why" />
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</HorizontalScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
22
app/src/main/res/layout/dialog_icon_pack_exists.xml
Normal file
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout 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"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/icon_pack_exists_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="@string/error_icon_pack_exists" />
|
||||
|
||||
<ListView
|
||||
android:id="@+id/icon_pack_exists_choices"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="10dp"
|
||||
tools:listitem="@android:layout/simple_list_item_1"/>
|
||||
|
||||
</LinearLayout>
|
31
app/src/main/res/layout/dialog_input_password.xml
Normal file
@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout 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:padding="16dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/input_password_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/enter_password" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:passwordToggleEnabled="true">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/input_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ems="10"
|
||||
android:hint="@string/password"
|
||||
android:inputType="textPassword" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
13
app/src/main/res/layout/dialog_manage_icon_packs.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout 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"
|
||||
android:padding="16dp">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/manage_icon_packs_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</LinearLayout>
|
24
app/src/main/res/layout/dialog_manage_icon_packs_item.xml
Normal file
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="10dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/icon_pack_name"
|
||||
android:layout_weight="1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="start|center"
|
||||
tools:text="My Icon Pack"/>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:id="@+id/icon_pack_delete"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:src="@drawable/baseline_delete_24" />
|
||||
|
||||
</LinearLayout>
|
42
app/src/main/res/layout/dialog_set_password.xml
Normal file
@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout 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:padding="16dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:passwordToggleEnabled="true" >
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/set_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ems="10"
|
||||
android:hint="@string/set_password"
|
||||
android:inputType="textPassword" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
app:passwordToggleEnabled="true">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/confirm_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ems="10"
|
||||
android:hint="@string/confirm_password"
|
||||
android:inputType="textPassword"
|
||||
app:boxStrokeWidth="0dp"/>
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
283
app/src/main/res/layout/fragment_about.xml
Normal file
@ -0,0 +1,283 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.core.widget.NestedScrollView 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"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:background="?android:attr/colorBackground"
|
||||
tools:context=".fragment.AboutFragment">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="16dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/app_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:text="@string/app_name"
|
||||
android:textAlignment="center"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/app_version"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:textAlignment="center"
|
||||
tools:text="APPVERSION" />
|
||||
|
||||
<View
|
||||
android:id="@+id/divider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:background="@drawable/theme_gradient" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/license"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:text="@string/license"
|
||||
android:textAlignment="center"
|
||||
android:textStyle="bold"
|
||||
android:textSize="18sp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/app_license"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:textAlignment="center"
|
||||
android:text="@string/app_license" />
|
||||
|
||||
<View
|
||||
android:id="@+id/divider2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:background="@drawable/theme_gradient" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/contact"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:textAlignment="center"
|
||||
android:textStyle="bold"
|
||||
android:text="@string/contact"
|
||||
android:textSize="18sp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/mailto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:textAlignment="center"
|
||||
android:autoLink="email"
|
||||
android:text="@string/mail_to" />
|
||||
|
||||
<View
|
||||
android:id="@+id/divider3"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:background="@drawable/theme_gradient" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/appcode"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:textAlignment="center"
|
||||
android:textStyle="bold"
|
||||
android:text="@string/appcode"
|
||||
android:textSize="18sp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/appcode_link"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:textAlignment="center"
|
||||
android:autoLink="web"
|
||||
android:text="@string/appcode_link" />
|
||||
|
||||
<View
|
||||
android:id="@+id/divider4"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:background="@drawable/theme_gradient" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/changelog"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:textAlignment="center"
|
||||
android:textStyle="bold"
|
||||
android:text="@string/changelog"
|
||||
android:textSize="18sp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/changelog_link"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:textAlignment="center"
|
||||
android:autoLink="web"
|
||||
android:text="@string/changelog_link" />
|
||||
|
||||
<View
|
||||
android:id="@+id/divider5"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:background="@drawable/theme_gradient" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/documentation"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:textAlignment="center"
|
||||
android:textStyle="bold"
|
||||
android:text="@string/documentation"
|
||||
android:textSize="18sp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/documentation_link"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:textAlignment="center"
|
||||
android:autoLink="web"
|
||||
android:text="@string/documentation_link" />
|
||||
|
||||
<View
|
||||
android:id="@+id/divider6"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:background="@drawable/theme_gradient" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/team"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:textAlignment="center"
|
||||
android:textStyle="bold"
|
||||
android:text="@string/development"
|
||||
android:textSize="18sp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/about_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:text="@string/developed_by"
|
||||
android:textAlignment="center" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linearLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/about_cringe_studios"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="70dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_weight="1"
|
||||
android:src="@drawable/cringestudios"
|
||||
app:tint="?android:attr/textColor"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/about_jg_cody"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_weight="1"
|
||||
android:src="@drawable/jgcody"
|
||||
app:tint="?android:attr/textColor"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/divider7"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:background="@drawable/theme_gradient" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/support"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:textAlignment="center"
|
||||
android:textStyle="bold"
|
||||
android:text="@string/support"
|
||||
android:textSize="18sp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/patreon_link"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:textAlignment="center"
|
||||
android:autoLink="web"
|
||||
android:text="@string/patreon_link" />
|
||||
|
||||
<Space
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="76dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/about_text" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
33
app/src/main/res/layout/fragment_group.xml
Normal file
@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.core.widget.NestedScrollView 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"
|
||||
tools:context=".fragment.GroupFragment">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingTop="5dp"
|
||||
android:paddingBottom="5dp">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/itemList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:orientation="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="76dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/itemList" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
22
app/src/main/res/layout/fragment_no_groups.xml
Normal file
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.core.widget.NestedScrollView 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"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
tools:context=".fragment.NoGroupsFragment">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="16dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/no_groups" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
30
app/src/main/res/layout/fragment_pick_icon.xml
Normal file
@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout 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:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/pick_icon_search"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ems="10"
|
||||
android:hint="@string/search"
|
||||
android:inputType="text" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<ExpandableListView
|
||||
android:id="@+id/pick_icon_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:nestedScrollingEnabled="true" />
|
||||
|
||||
</LinearLayout>
|
214
app/src/main/res/layout/fragment_settings.xml
Normal file
@ -0,0 +1,214 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.core.widget.NestedScrollView 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"
|
||||
tools:context=".fragment.SettingsFragment"
|
||||
android:background="?android:attr/colorBackground">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/localization"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/language" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/settings_language"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginVertical="10dp"
|
||||
android:background="@drawable/theme_gradient" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/security"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/settings_enable_encryption"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/enable_encryption" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/settings_biometric_lock"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/settings_biometric_lock" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/settings_biometric_lock_info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
tools:text="Additional info when biometric auth is unavailable" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/settings_screen_security"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/screen_security" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/settings_hide_codes"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/hide_codes" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/settings_show_images"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/show_images" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginVertical="10dp"
|
||||
android:background="@drawable/theme_gradient" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/appearance"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/settings_enable_intro_video"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/settings_enable_intro_video" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/settings_enable_themed_background"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/settings_enable_themed_background" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/theme" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/settings_theme"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/settings_enable_minimalist_theme"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/settings_enable_minimalist_theme" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/appearance" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/settings_appearance"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginVertical="10dp"
|
||||
android:background="@drawable/theme_gradient" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/backups"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/settings_create_backup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:background="?attr/buttonBackground"
|
||||
android:text="@string/create_backup"
|
||||
android:textAllCaps="false" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/settings_load_backup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:background="?attr/buttonBackground"
|
||||
android:text="@string/load_backup"
|
||||
android:textAllCaps="false" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginVertical="10dp"
|
||||
android:background="@drawable/theme_gradient" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/settings_icon_packs"
|
||||
android:gravity="center"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/settings_load_icon_pack"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:background="?attr/buttonBackground"
|
||||
android:text="@string/settings_icon_packs_import"
|
||||
android:textAllCaps="false" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/settings_manage_icon_packs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:background="?attr/buttonBackground"
|
||||
android:text="@string/settings_icon_packs_manage"
|
||||
android:textAllCaps="false" />
|
||||
|
||||
<Space
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="76dp" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
9
app/src/main/res/layout/icon_list_category.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView 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"
|
||||
android:paddingVertical="16dp"
|
||||
android:paddingLeft="?android:attr/expandableListPreferredItemPaddingLeft"
|
||||
tools:text="This is a category">
|
||||
</TextView>
|
22
app/src/main/res/layout/icon_list_icon.xml
Normal file
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingVertical="5dp"
|
||||
android:paddingLeft="?android:attr/expandableListPreferredChildPaddingLeft">
|
||||
<com.caverock.androidsvg.SVGImageView
|
||||
android:id="@+id/icon_list_icon_image"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:src="@drawable/baseline_edit_24"
|
||||
android:layout_marginEnd="10dp"/>
|
||||
<TextView
|
||||
android:id="@+id/icon_list_icon_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="start|center"
|
||||
tools:text="This is an icon">
|
||||
</TextView>
|
||||
</LinearLayout>
|
BIN
app/src/main/res/raw/intro_vp9.webm
Normal file
@ -1,16 +1,14 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.OneTapSSH" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_200</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/black</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_200</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
<style name="Theme.OneTap_SSH" parent="Base.Theme.OneTap_SSH">
|
||||
<item name="android:textColor">@color/white</item>
|
||||
<item name="colorOnBackground">@color/background_grey</item>
|
||||
</style>
|
||||
<style name="Theme.OneTap_SSH.PopupOverlay">
|
||||
<item name="colorTheme1">@color/color_blue</item>
|
||||
<item name="colorTheme2">@color/color_light_green</item>
|
||||
</style>
|
||||
<style name="Theme.OneTap_SSH.AppBarOverlay">
|
||||
<item name="colorTheme1">@color/color_blue</item>
|
||||
<item name="colorTheme2">@color/color_light_green</item>
|
||||
</style>
|
||||
</resources>
|
8
app/src/main/res/values/attrs.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<attr name="colorTheme1" format="color" />
|
||||
<attr name="colorTheme2" format="color" />
|
||||
<attr name="dialogBackground" format="reference|color" />
|
||||
<attr name="menuBackground" format="reference|color" />
|
||||
<attr name="buttonBackground" format="reference|color" />
|
||||
</resources>
|
@ -1,10 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="background_grey">#222222</color>
|
||||
<color name="background_light_grey">#DCDCDC</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
<color name="colorPrimary">#008BFF</color>
|
||||
<color name="colorSecondary">#90D14C</color>
|
||||
|
||||
<!-- Theme colors -->
|
||||
<color name="color_blue">#008BFF</color>
|
||||
<color name="color_light_green">#90D14C</color>
|
||||
<color name="color_pink">#D900FF</color>
|
||||
<color name="color_orange">#FF7700</color>
|
||||
<color name="color_red">#DA0303</color>
|
||||
<color name="color_yellow">#FFE500</color>
|
||||
<color name="color_turquoise">#00FFF7</color>
|
||||
<color name="color_green">#00FF0A</color>
|
||||
</resources>
|
@ -1,13 +1,196 @@
|
||||
<resources>
|
||||
<string name="app_name">OneTap-SSH</string>
|
||||
<string name="navigation_drawer_open">Open navigation drawer</string>
|
||||
<string name="navigation_drawer_close">Close navigation drawer</string>
|
||||
<string name="nav_header_title">Android Studio</string>
|
||||
<string name="nav_header_subtitle">android.studio@android.com</string>
|
||||
<string name="nav_header_desc">Navigation header</string>
|
||||
<string name="app_name" translatable="false">Code Guard</string>
|
||||
<string name="action_settings">Settings</string>
|
||||
<string name="action_about">About</string>
|
||||
<string name="edit">Edit</string>
|
||||
<string name="choose_language">Choose Language</string>
|
||||
<string name="cancel">Cancel</string>
|
||||
<string name="add">Add</string>
|
||||
<string name="ok" translatable="false">OK</string>
|
||||
<string name="no">No</string>
|
||||
<string name="yes">Yes</string>
|
||||
<string name="invalid_input">Invalid Input</string>
|
||||
<string name="haptic_feedback">Haptic Feedback</string>
|
||||
<string name="reset_app">Reset App</string>
|
||||
<string name="import_export">Import / Export</string>
|
||||
<string name="biometric_lock_subtitle">Unlock the authenticator</string>
|
||||
<string name="create_totp_title">Select Code Type</string>
|
||||
<string name="action_new_group">New Group</string>
|
||||
<string name="new_group_missing_title">You need to input a name</string>
|
||||
<string name="qr_scanner_failed">Scan failed: %s</string>
|
||||
<string name="intro_video_failed">Failed to play video</string>
|
||||
<string name="edit_otp_title">Edit OTP</string>
|
||||
<string name="group_delete_title">Delete Groups</string>
|
||||
<string name="group_delete_message">Do you want to delete the groups? Note: This will delete all of the contained OTPs!</string>
|
||||
<string name="hotp_generated_new_code">Generated new code</string>
|
||||
<string name="uri_handler_failed_title">Failed to add code</string>
|
||||
<string name="code_input_title">Input Code</string>
|
||||
<string name="failed_title">Action failed</string>
|
||||
<string name="input_code_invalid_number">Invalid number entered</string>
|
||||
<string name="back">Back</string>
|
||||
<string name="otp_delete_title">Delete OTP(s)</string>
|
||||
<string name="otp_delete_message">Do you want to delete the selected OTP(s)?</string>
|
||||
<string name="edit_group_title">Edit Group</string>
|
||||
<string name="settings_enable_intro_video">Enable intro video</string>
|
||||
<string name="settings_biometric_lock">Use biometric unlock</string>
|
||||
<string name="uri_handler_code_added">Code added</string>
|
||||
<string name="uri_handler_add_code_title">Add Code</string>
|
||||
<string name="uri_handler_create_group">Create New Group</string>
|
||||
<string name="theme">Theme</string>
|
||||
<string name="otp_add_counter">Counter</string>
|
||||
<string name="otp_add_checksum">Add Checksum</string>
|
||||
<string name="otp_add_secret">Secret</string>
|
||||
<string name="otp_add_name">Name</string>
|
||||
<string name="otp_add_period">Period</string>
|
||||
<string name="otp_add_error">Failed to update OTP: %s</string>
|
||||
<string name="otp_add_issuer">Issuer (optional)</string>
|
||||
<string name="otp_add_missing_name">Missing name</string>
|
||||
<string name="qr_scanner_migration_title">OTP Migration</string>
|
||||
<string name="qr_scanner_migration_message">It seems like you\'re trying to import OTP codes from another app. Do you want to import all codes into this group?</string>
|
||||
<string name="qr_scanner_migration_part">Code %d of %d scanned</string>
|
||||
<string name="screen_security">Screen security</string>
|
||||
<string name="hide_codes">Hide codes</string>
|
||||
<string name="enable_encryption">Enable encryption</string>
|
||||
<string name="password">Password</string>
|
||||
<string name="set_password">Set Password</string>
|
||||
<string name="confirm_password">Confirm Password</string>
|
||||
<string name="unlock_authenticator">Unlock Authenticator</string>
|
||||
<string name="unlock">Unlock</string>
|
||||
<string name="unlock_using_biometrics">Unlock using biometrics</string>
|
||||
<string name="name">Name</string>
|
||||
<string name="add_code">Add Code</string>
|
||||
<string name="settings">Settings</string>
|
||||
<string name="appearance">Appearance</string>
|
||||
<string name="developed_by">Developed by Cringe Studios and JG-Cody</string>
|
||||
<string name="fragment_settings">Settings</string>
|
||||
<string name="fragment_about">About</string>
|
||||
<string name="fragment_edit_otp">Edit OTP</string>
|
||||
<string name="fragment_no_groups">No Groups</string>
|
||||
<string-array name="view_edit_move_delete">
|
||||
<item>View</item>
|
||||
<item>Edit</item>
|
||||
<item>Move to other group</item>
|
||||
<item>Delete</item>
|
||||
</string-array>
|
||||
<string-array name="rename_delete">
|
||||
<item>Rename</item>
|
||||
<item>Delete</item>
|
||||
</string-array>
|
||||
<string name="theme_blue_green">Blue/Green</string>
|
||||
<string name="theme_red_blue">Red/Blue</string>
|
||||
<string name="theme_pink_green">Pink/Green</string>
|
||||
<string name="theme_blue_yellow">Blue/Yellow</string>
|
||||
<string name="theme_green_yellow">Green/Yellow</string>
|
||||
<string name="theme_orange_turquoise">Orange/Turquoise</string>
|
||||
<string name="appearance_dark">Dark</string>
|
||||
<string name="appearance_light">Light</string>
|
||||
<string name="appearance_follow_system">System default</string>
|
||||
<string name="locale_system_default">System default</string>
|
||||
<string name="backup_create_title">Create backup</string>
|
||||
<string-array name="backup_create">
|
||||
<item>Create with current password</item>
|
||||
<item>Create with new password</item>
|
||||
</string-array>
|
||||
<string name="backup_load_title">Load backup</string>
|
||||
<string name="backups">Backups</string>
|
||||
<string name="create_backup">Create backup</string>
|
||||
<string name="load_backup">Load backup</string>
|
||||
<string name="enter_password">Enter Password</string>
|
||||
<string name="disable_encryption_title">Disable encryption</string>
|
||||
<string name="disable_encryption_message">Do you really want to disable encryption?</string>
|
||||
<string name="load_backup_title">Load backup</string>
|
||||
<string name="backup_load_message">Do you want to load this backup? This will delete ALL of the current data in the app and replace it with the data from the backup!</string>
|
||||
<string name="otp_add_type">Type</string>
|
||||
<string name="otp_add_algorithm">Algorithm</string>
|
||||
<string name="otp_add_digits">Digits</string>
|
||||
<string name="show_images">Show images</string>
|
||||
<string name="delete_pack_title">Delete pack</string>
|
||||
<string name="delete_pack_message">Do you want to delete the icon pack?</string>
|
||||
<string name="no_icon_packs_installed">No icon packs are currently installed</string>
|
||||
<string name="license">License</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="website">Website</string>
|
||||
<string name="development">Development</string>
|
||||
<string name="appcode">Appcode</string>
|
||||
<string name="support">Support</string>
|
||||
<string name="changelog">Changelog</string>
|
||||
<string name="documentation">Documentation</string>
|
||||
<string name="app_license" translatable="false">GNU General Public License, version 3.0</string>
|
||||
<string name="mail_to">Mail: info@code-guard.com</string>
|
||||
<string name="appcode_link" translatable="false">https://git.cringe-studios.com/CringeStudios/Code-Guard</string>
|
||||
<string name="changelog_link" translatable="false">https://code-guard.com/#4</string>
|
||||
<string name="documentation_link" translatable="false">https://git.cringe-studios.com/CringeStudios/Code-Guard</string>
|
||||
<string name="patreon_link" translatable="false">https://git.cringe-studios.com/CringeStudios/Code-Guard</string>
|
||||
<string name="error_icon_pack_empty">The icon pack doesn\'t contain any icons</string>
|
||||
<string name="error_icon_pack_invalid">Pack contains invalid metadata. Make sure you selected the correct file</string>
|
||||
<string name="error_icon_pack_exists">The icon pack you\'re trying to import already exists. Imported: %s (version %d) Existing: %s (version %d) What do you want to do?</string>
|
||||
<string name="broken_icon_packs_title">Broken icon packs</string>
|
||||
<string name="broken_icon_packs_message">Some icon packs failed to load. Do you want to delete the broken icon packs?</string>
|
||||
<string name="icon_pack_imported">Icon pack with %d icon(s) imported</string>
|
||||
<string name="enable_encryption_message">It is recommended to enable encryption to improve the security of the application. Do you want to go to the settings now to enable encryption?</string>
|
||||
<string name="enable_encryption_title">Enable encryption</string>
|
||||
<string name="back_pressed">Press back again to exit</string>
|
||||
<string name="error_backup_database_not_encrypted">Database must be encrypted for this option</string>
|
||||
<string name="manage_icon_packs_title">Manage icon packs</string>
|
||||
<string name="error_backup_load_crypto">Failed to load backup. Make sure the password is valid</string>
|
||||
<string name="error_backup_load_other">Failed to load backup</string>
|
||||
<string name="error_biometric_encryption_enable">Failed to enable biometric encryption</string>
|
||||
<string name="error_biometric_encryption_disable">Failed to disable biometric encryption</string>
|
||||
<string name="edit_otp_choose_image">Choose Image</string>
|
||||
<string name="error_edit_otp_image">Failed to open image</string>
|
||||
<string name="error_database_save">Failed to save database</string>
|
||||
<string name="error_otp_refresh">An error occurred while refreshing the code</string>
|
||||
<string name="error_enable_encryption">Failed to enable encryption</string>
|
||||
<string name="error_disable_encryption">Failed to disable encryption</string>
|
||||
<string name="error_unlock_no_password">You need to enter a password</string>
|
||||
<string name="error_unlock_crypto">Failed to load database: Invalid password or database corrupted</string>
|
||||
<string name="error_unlock_other">Failed to load database</string>
|
||||
<string name="error_qr_scan_not_detected">No codes were detected in the provided image</string>
|
||||
<string name="error_qr_scan_failed">Failed to detect code</string>
|
||||
<string name="error_qr_scan_image_failed">Failed to read image</string>
|
||||
<string name="icon_pack_exists_title">Icon pack already exists</string>
|
||||
<string name="error_import_icon_pack">Failed to import icon pack</string>
|
||||
<string name="error_no_camera_permission">No camera permission</string>
|
||||
<string name="details">Details</string>
|
||||
<string name="search">Search</string>
|
||||
<string name="settings_enable_themed_background">Use themed background</string>
|
||||
<string name="settings_enable_minimalist_theme">Use minimalist theme</string>
|
||||
<string name="settings_icon_packs">Icon packs</string>
|
||||
<string name="settings_icon_packs_import">Import icon pack</string>
|
||||
<string name="settings_icon_packs_manage">Manage icon packs</string>
|
||||
<string name="save">Save</string>
|
||||
<string name="lock">Lock</string>
|
||||
<string name="otp_input">Input manually</string>
|
||||
<string name="otp_scan">Scan QR code</string>
|
||||
<string name="otp_scan_image">Scan image</string>
|
||||
<string name="otp_view">View OTP</string>
|
||||
<string name="otp_edit">Edit OTP</string>
|
||||
<string name="otp_move">Move OTP</string>
|
||||
<string name="otp_delete">Delete OTP</string>
|
||||
<string name="close">Close</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="localization">Localization</string>
|
||||
<string name="error_duplicate_otp_message">An OTP with the name of the OTP you\'re trying to add already exists. Do you want to automatically rename the new OTP to distinguish them from each other?</string>
|
||||
<string name="error_duplicate_otp_title">Duplicate OTP</string>
|
||||
<string name="biometric_encryption_unavailable">Biometric authentication is disabled because it is not set up or not available on your device</string>
|
||||
<string name="no_groups">No groups exist yet. Open the menu and press the \'+\' button to create one</string>
|
||||
<string-array name="edit_otp_choose_image_options">
|
||||
<item>Image from icon pack</item>
|
||||
<item>Image from gallery</item>
|
||||
<item>No image</item>
|
||||
<item>Reset to default image</item>
|
||||
</string-array>
|
||||
<string-array name="error_icon_pack_exists_choices">
|
||||
<item>Override</item>
|
||||
<item>Rename existing</item>
|
||||
<item>Rename imported</item>
|
||||
</string-array>
|
||||
<string name="language">Language</string>
|
||||
<string name="nav_header_subtitle">1</string>
|
||||
<string name="nav_header_title">1</string>
|
||||
<string name="nav_header_desc">1</string>
|
||||
<string name="menu_home">1</string>
|
||||
<string name="menu_gallery">1</string>
|
||||
<string name="menu_slideshow">1</string>
|
||||
|
||||
<string name="menu_home">Home</string>
|
||||
<string name="menu_gallery">Gallery</string>
|
||||
<string name="menu_slideshow">Slideshow</string>
|
||||
</resources>
|
@ -1,25 +1,85 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.OneTapSSH" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_500</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_700</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
<style name="Base.Theme.OneTap_SSH" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<item name="colorPrimary">?attr/colorTheme1</item>
|
||||
<item name="colorPrimaryDark">@android:color/transparent</item>
|
||||
<item name="colorSecondary">?attr/colorTheme2</item>
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
|
||||
<item name="colorTheme1">#FF00FF</item>
|
||||
<item name="colorTheme2">#000000</item>
|
||||
<item name="android:textColor">@color/black</item>
|
||||
<item name="colorOnBackground">@color/background_light_grey</item>
|
||||
<item name="actionOverflowMenuStyle">@style/ActionPopupMenuStyle</item>
|
||||
<item name="dialogBackground">@drawable/dialog_themed</item>
|
||||
<item name="menuBackground">@drawable/menu_themed</item>
|
||||
<item name="buttonBackground">@drawable/button_themed</item>
|
||||
<item name="android:navigationBarColor">?android:attr/colorBackground</item>
|
||||
<item name="android:spinnerStyle">@style/SpinnerStyle</item>
|
||||
|
||||
<item name="actionMenuTextAppearance">@style/ActionMenuTextAppearance</item>
|
||||
|
||||
<item name="textInputStyle">@style/TextInputStyle</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.OneTapSSH.NoActionBar">
|
||||
<item name="windowActionBar">false</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
<style name="Theme.OneTap_SSH" parent="Base.Theme.OneTap_SSH" />
|
||||
|
||||
<style name="Theme.OneTap_SSH.None" />
|
||||
|
||||
<style name="Theme.OneTap_SSH.Blue_Green">
|
||||
<item name="colorTheme1">@color/color_blue</item>
|
||||
<item name="colorTheme2">@color/color_light_green</item>
|
||||
</style>
|
||||
<style name="Theme.OneTap_SSH.Red_Blue">
|
||||
<item name="colorTheme1">@color/color_red</item>
|
||||
<item name="colorTheme2">@color/color_blue</item>
|
||||
</style>
|
||||
<style name="Theme.OneTap_SSH.Pink_Green">
|
||||
<item name="colorTheme1">@color/color_pink</item>
|
||||
<item name="colorTheme2">@color/color_green</item>
|
||||
</style>
|
||||
<style name="Theme.OneTap_SSH.Blue_Yellow">
|
||||
<item name="colorTheme1">@color/color_blue</item>
|
||||
<item name="colorTheme2">@color/color_yellow</item>
|
||||
</style>
|
||||
<style name="Theme.OneTap_SSH.Green_Yellow">
|
||||
<item name="colorTheme1">@color/color_green</item>
|
||||
<item name="colorTheme2">@color/color_yellow</item>
|
||||
</style>
|
||||
<style name="Theme.OneTap_SSH.Orange_Turquoise">
|
||||
<item name="colorTheme1">@color/color_orange</item>
|
||||
<item name="colorTheme2">@color/color_turquoise</item>
|
||||
</style>
|
||||
<style name="Theme.CringeAuthenticator.Minimalist" parent="">
|
||||
<item name="dialogBackground">?android:attr/colorBackground</item>
|
||||
<item name="menuBackground">?android:attr/colorBackground</item>
|
||||
<item name="buttonBackground">@drawable/button_simple</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.OneTapSSH.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
|
||||
<style name="ActionPopupMenuStyle" parent="Widget.AppCompat.PopupMenu">
|
||||
<item name="android:popupBackground">?attr/menuBackground</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.OneTapSSH.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
|
||||
<style name="SpinnerStyle" parent="Widget.AppCompat.Spinner.DropDown">
|
||||
<item name="android:popupBackground">?attr/dialogBackground</item>
|
||||
</style>
|
||||
|
||||
<style name="ActionMenuTextAppearance" parent="TextAppearance.AppCompat.Widget.ActionBar.Menu">
|
||||
<item name="android:textAllCaps">false</item>
|
||||
</style>
|
||||
|
||||
<style name="TextInputStyle" parent="Widget.Material3.TextInputLayout.OutlinedBox">
|
||||
<item name="boxCornerRadiusTopStart">25dp</item>
|
||||
<item name="boxCornerRadiusTopEnd">25dp</item>
|
||||
<item name="boxCornerRadiusBottomStart">25dp</item>
|
||||
<item name="boxCornerRadiusBottomEnd">25dp</item>
|
||||
</style>
|
||||
<style name="Theme.OneTap_SSH.PopupOverlay">
|
||||
<item name="colorTheme1">@color/color_blue</item>
|
||||
<item name="colorTheme2">@color/color_light_green</item>
|
||||
</style>
|
||||
<style name="Theme.OneTap_SSH.AppBarOverlay">
|
||||
<item name="colorTheme1">@color/color_blue</item>
|
||||
<item name="colorTheme2">@color/color_light_green</item>
|
||||
</style>
|
||||
</resources>
|
@ -1,4 +1,6 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
id 'com.android.application' version '8.1.1' apply false
|
||||
id 'com.android.application' version '8.1.2' apply false
|
||||
id 'com.android.library' version '8.1.2' apply false
|
||||
id 'com.google.protobuf' version '0.9.3' apply false
|
||||
}
|