App zusammenkopiert aus Teraplex und CodeGuard

This commit is contained in:
JG-Cody 2024-02-23 17:44:39 +01:00
parent 3ec8765b0c
commit 5796a4519e
87 changed files with 4033 additions and 54 deletions

View 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>

View File

@ -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'

View File

@ -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>

View 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());
}
}

View 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);
}
}

View File

@ -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.

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View 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);
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -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;
}
}

View 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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View 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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View 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;
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View 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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,7 @@
package com.example.onetap_ssh.util;
public interface DialogCallback {
boolean callback();
}

View 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();
}
}

View 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();
}
}

View 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);
}
}

View File

@ -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;
}
}

View 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;
}
}

View 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);
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

View 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>

View 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>

View 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>

View 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>

View File

@ -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

View 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>

View File

@ -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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

Binary file not shown.

View 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>

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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
}