diff --git a/app/build.gradle b/app/build.gradle index ce58ab2..d29e970 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -93,6 +93,8 @@ dependencies { implementation "androidx.camera:camera-view:${camerax_version}" implementation "androidx.camera:camera-extensions:${camerax_version}" + implementation "androidx.exifinterface:exifinterface:1.3.6" + implementation 'com.google.protobuf:protobuf-javalite:3.24.3' implementation 'org.bouncycastle:bcprov-jdk15on:1.70' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f7ee53f..a99d1ef 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -34,7 +34,8 @@ android:exported="true" android:label="@string/app_name" android:theme="@style/Theme.CringeAuthenticator.None" - android:configChanges="orientation|screenSize"> + android:configChanges="orientation|screenSize" + android:windowSoftInputMode="adjustPan" > pickIconPackFileLoad; + private ActivityResultLauncher pickIconImage; + + private Consumer pickIconImageCallback; + private QRScanner qrScanner; private boolean fullyLaunched; @@ -137,6 +142,8 @@ public class MainActivity extends BaseActivity { }); pickIconPackFileLoad = registerForActivityResult(new ActivityResultContracts.OpenDocument(), doc -> { + lockOnStop = true; + try { if(doc == null) return; IconPackMetadata meta = IconUtil.importIconPack(this, doc); @@ -146,6 +153,13 @@ public class MainActivity extends BaseActivity { } }); + pickIconImage = registerForActivityResult(new ActivityResultContracts.PickVisualMedia(), img -> { + if(pickIconImageCallback != null) { + pickIconImageCallback.accept(img); + pickIconImageCallback = null; + } + }); + if(SettingsUtil.isFirstLaunch(this) && SettingsUtil.getGroups(this).isEmpty()) { SettingsUtil.addGroup(this, UUID.randomUUID().toString(), "My Codes"); SettingsUtil.setFirstLaunch(this, false); @@ -194,6 +208,11 @@ public class MainActivity extends BaseActivity { return true; } + if(fragment instanceof EditOTPFragment) { + getMenuInflater().inflate(R.menu.menu_edit_otp, menu); + return true; + } + getMenuInflater().inflate(R.menu.menu_main, menu); return true; } @@ -224,6 +243,11 @@ public class MainActivity extends BaseActivity { } } + if(fragment instanceof EditOTPFragment) { + ((EditOTPFragment) fragment).cancel(); + return; + } + if(!(fragment instanceof HomeFragment)) { NavigationUtil.navigate(this, HomeFragment.class, null); } @@ -331,6 +355,20 @@ public class MainActivity extends BaseActivity { } } + public void saveOTP(MenuItem item) { + Fragment frag = NavigationUtil.getCurrentFragment(this); + if(frag instanceof EditOTPFragment) { + ((EditOTPFragment) frag).save(); + } + } + + public void cancelEditingOTP(MenuItem item) { + Fragment frag = NavigationUtil.getCurrentFragment(this); + if(frag instanceof EditOTPFragment) { + ((EditOTPFragment) frag).cancel(); + } + } + public void lockApp(MenuItem item) { OTPDatabase.unloadDatabase(); OTPDatabase.promptLoadDatabase(this, () -> {}, () -> {}); @@ -359,6 +397,17 @@ public class MainActivity extends BaseActivity { pickIconPackFileLoad.launch(new String[]{"application/zip", "*/*"}); } + public void promptPickIconImage(Consumer callback) { + this.lockOnStop = false; + this.pickIconImageCallback = uri -> { + lockOnStop = true; + callback.accept(uri); + }; + pickIconImage.launch(new PickVisualMediaRequest.Builder() + .setMediaType(ActivityResultContracts.PickVisualMedia.ImageOnly.INSTANCE) + .build()); + } + @Override public void recreate() { lockOnStop = false; diff --git a/app/src/main/java/com/cringe_studios/cringe_authenticator/fragment/EditOTPFragment.java b/app/src/main/java/com/cringe_studios/cringe_authenticator/fragment/EditOTPFragment.java new file mode 100644 index 0000000..6b22c19 --- /dev/null +++ b/app/src/main/java/com/cringe_studios/cringe_authenticator/fragment/EditOTPFragment.java @@ -0,0 +1,293 @@ +package com.cringe_studios.cringe_authenticator.fragment; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Matrix; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Base64; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.util.Consumer; +import androidx.exifinterface.media.ExifInterface; + +import com.cringe_studios.cringe_authenticator.MainActivity; +import com.cringe_studios.cringe_authenticator.R; +import com.cringe_studios.cringe_authenticator.databinding.FragmentEditOtpBinding; +import com.cringe_studios.cringe_authenticator.icon.IconUtil; +import com.cringe_studios.cringe_authenticator.model.OTPData; +import com.cringe_studios.cringe_authenticator.util.DialogUtil; +import com.cringe_studios.cringe_authenticator.util.IOUtil; +import com.cringe_studios.cringe_authenticator.util.NavigationUtil; +import com.cringe_studios.cringe_authenticator.util.StyledDialogBuilder; +import com.cringe_studios.cringe_authenticator.util.ThemeUtil; +import com.cringe_studios.cringe_authenticator_library.OTPAlgorithm; +import com.cringe_studios.cringe_authenticator_library.OTPType; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; + +public class EditOTPFragment extends NamedFragment { + + private static final Integer[] DIGITS = new Integer[]{6, 7, 8, 9, 10, 11, 12}; + + private static final String[] TYPES = new String[]{ + OTPType.HOTP.getFriendlyName() + " (HOTP)", + OTPType.TOTP.getFriendlyName() + " (TOTP)" + }; + + private FragmentEditOtpBinding binding; + + private OTPData data; + + private String imageData; + + private boolean view; + + private Consumer callback; + + public EditOTPFragment(OTPData data, boolean view, Consumer callback) { + this.data = data; + this.view = view; + this.callback = callback; + } + + @Override + public String getName() { + return requireActivity().getString(R.string.fragment_edit_otp); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + binding = FragmentEditOtpBinding.inflate(inflater); + + @DrawableRes int bg = ThemeUtil.getBackground(requireContext()); + if(bg != 0) { + binding.getRoot().setBackgroundResource(bg); + } + + IconUtil.loadEffectiveImage(requireContext(), data, binding.inputImage, null); + binding.inputImage.setOnClickListener(v -> { + new StyledDialogBuilder(requireContext()) + .setTitle("Choose Image") + .setItems(new String[]{"Image from icon pack", "Image from gallery", "No image", "Reset to default image"}, (d, which) -> { + switch(which) { + case 0: + // TODO: pick from icon pack + break; + case 1: + pickGalleryImage(); + break; + case 2: + setNoImage(); + break; + case 3: + setDefaultImage(); + break; + } + }) + .show(); + }); + + binding.inputType.setAdapter(new ArrayAdapter<>(requireContext(), android.R.layout.simple_list_item_1, TYPES)); + binding.inputType.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + OTPType newType = OTPType.values()[position]; + switch(newType) { + case HOTP: + binding.textPeriod.setVisibility(View.GONE); + binding.inputPeriod.setVisibility(View.GONE); + binding.textCounter.setVisibility(View.VISIBLE); + binding.inputCounter.setVisibility(View.VISIBLE); + break; + case TOTP: + binding.textCounter.setVisibility(View.GONE); + binding.inputCounter.setVisibility(View.GONE); + binding.textPeriod.setVisibility(View.VISIBLE); + binding.inputPeriod.setVisibility(View.VISIBLE); + break; + } + } + + @Override + public void onNothingSelected(AdapterView parent) {} + }); + binding.inputType.setEnabled(!view); + binding.inputType.setSelection(data != null ? data.getType().ordinal() : 0); + + binding.inputAlgorithm.setAdapter(new ArrayAdapter<>(requireContext(), android.R.layout.simple_list_item_1, OTPAlgorithm.values())); + binding.inputAlgorithm.setEnabled(!view); + + binding.inputDigits.setAdapter(new ArrayAdapter<>(requireContext(), android.R.layout.simple_list_item_1, DIGITS)); + binding.inputDigits.setEnabled(!view); + + TextWatcher watcher = new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + + @Override + public void afterTextChanged(Editable s) { + updateImage(); + } + }; + + binding.inputName.setEnabled(!view); + binding.inputName.addTextChangedListener(watcher); + + binding.inputIssuer.setEnabled(!view); + binding.inputIssuer.addTextChangedListener(watcher); + + binding.inputSecret.setEnabled(!view); + binding.inputPeriod.setEnabled(!view); + binding.inputChecksum.setEnabled(!view); + + if(data != null) { + imageData = data.getImageData(); + + binding.inputName.setText(data.getName()); + binding.inputIssuer.setText(data.getIssuer()); + binding.inputSecret.setText(data.getSecret()); + binding.inputAlgorithm.setSelection(data.getAlgorithm().ordinal()); + + int index = Arrays.asList(DIGITS).indexOf(data.getDigits()); + if(index != -1) binding.inputDigits.setSelection(index); + binding.inputChecksum.setChecked(data.hasChecksum()); + + switch(data.getType()) { + case HOTP: + binding.inputCounter.setText(String.valueOf(data.getCounter())); + break; + case TOTP: + binding.inputPeriod.setText(String.valueOf(data.getPeriod())); + break; + } + } + + return binding.getRoot(); + } + + private void updateImage() { + if(imageData != null && !imageData.equals(OTPData.IMAGE_DATA_NONE)) return; + IconUtil.loadEffectiveImage(requireContext(), imageData, binding.inputIssuer.getText().toString(), binding.inputName.getText().toString(), binding.inputImage, null); + } + + private void pickGalleryImage() { + ((MainActivity) requireActivity()).promptPickIconImage(uri -> { + if(uri == null) return; + + try(InputStream in = requireActivity().getContentResolver().openInputStream(uri)) { + if(in == null) return; + + byte[] bytes = IOUtil.readBytes(in); + Bitmap bm = BitmapFactory.decodeByteArray(bytes, 0, bytes.length); + + ExifInterface i = new ExifInterface(new ByteArrayInputStream(bytes)); + int orientation = i.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); + + Matrix matrix = new Matrix(); + switch(orientation) { + case ExifInterface.ORIENTATION_ROTATE_270: + matrix.postRotate(90); + case ExifInterface.ORIENTATION_ROTATE_180: + matrix.postRotate(90); + case ExifInterface.ORIENTATION_ROTATE_90: + matrix.postRotate(90); + bm = Bitmap.createBitmap(bm, 0, 0, bm.getWidth(), bm.getHeight(), matrix, true); + } + + bm = IconUtil.cutToIcon(bm); + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + bm.compress(Bitmap.CompressFormat.PNG, 100, bOut); + imageData = Base64.encodeToString(bOut.toByteArray(), Base64.DEFAULT); + + binding.inputImage.setImageBitmap(bm); + }catch(IOException e) { + DialogUtil.showErrorDialog(requireContext(), "Failed to open image", e); + } + }); + } + + private void setNoImage() { + imageData = OTPData.IMAGE_DATA_NONE; + updateImage(); + } + + private void setDefaultImage() { + imageData = null; + updateImage(); + } + + public boolean isView() { + return view; + } + + public void cancel() { + NavigationUtil.closeOverlay(this); + } + + public void save() { + try { + String name = binding.inputName.getText().toString(); + if(name.trim().isEmpty()) { + DialogUtil.showErrorDialog(requireContext(), requireContext().getString(R.string.otp_add_missing_name)); + return; + } + + String issuer = binding.inputIssuer.getText().toString(); + if(issuer.trim().isEmpty()) { + issuer = null; + } + + String secret = binding.inputSecret.getText().toString(); + OTPAlgorithm algorithm = (OTPAlgorithm) binding.inputAlgorithm.getSelectedItem(); + int digits = (int) binding.inputDigits.getSelectedItem(); + boolean checksum = binding.inputChecksum.isChecked(); + + int period = 0; + int counter = 0; + OTPType type = OTPType.values()[binding.inputType.getSelectedItemPosition()]; + switch(type) { + case HOTP: + counter = Integer.parseInt(binding.inputCounter.getText().toString()); + if(counter < 0) throw new NumberFormatException(); + break; + case TOTP: + period = Integer.parseInt(binding.inputPeriod.getText().toString()); + if(period <= 0) throw new NumberFormatException(); + break; + } + + OTPData data = new OTPData(name, issuer, type, secret, algorithm, digits, period, counter, checksum); + data.setImageData(imageData); + + String errorMessage = data.validate(); + if(errorMessage != null) { + DialogUtil.showErrorDialog(requireContext(), errorMessage); + return; + } + + callback.accept(data); + NavigationUtil.closeOverlay(this); + }catch(NumberFormatException e) { + DialogUtil.showErrorDialog(requireContext(), requireContext().getString(R.string.input_code_invalid_number)); + } + } + +} diff --git a/app/src/main/java/com/cringe_studios/cringe_authenticator/fragment/GroupFragment.java b/app/src/main/java/com/cringe_studios/cringe_authenticator/fragment/GroupFragment.java index 2e651ed..21f771a 100644 --- a/app/src/main/java/com/cringe_studios/cringe_authenticator/fragment/GroupFragment.java +++ b/app/src/main/java/com/cringe_studios/cringe_authenticator/fragment/GroupFragment.java @@ -17,6 +17,7 @@ import com.cringe_studios.cringe_authenticator.model.OTPData; import com.cringe_studios.cringe_authenticator.otplist.OTPListAdapter; import com.cringe_studios.cringe_authenticator.otplist.OTPListItem; import com.cringe_studios.cringe_authenticator.util.DialogUtil; +import com.cringe_studios.cringe_authenticator.util.NavigationUtil; import com.cringe_studios.cringe_authenticator.util.OTPDatabase; import com.cringe_studios.cringe_authenticator.util.OTPDatabaseException; import com.cringe_studios.cringe_authenticator.util.SettingsUtil; @@ -133,11 +134,11 @@ public class GroupFragment extends NamedFragment { if(items.size() != 1) return; OTPData data = items.get(0).getOTPData(); - DialogUtil.showEditCodeDialog(getLayoutInflater(), data, newData -> { + NavigationUtil.openOverlay(this, new EditOTPFragment(data, false, newData -> { otpListAdapter.replace(data, newData); saveOTPs(); otpListAdapter.finishEditing(); - }); + })); } public void moveOTP() { diff --git a/app/src/main/java/com/cringe_studios/cringe_authenticator/icon/IconUtil.java b/app/src/main/java/com/cringe_studios/cringe_authenticator/icon/IconUtil.java index 5a3b650..acd7dca 100644 --- a/app/src/main/java/com/cringe_studios/cringe_authenticator/icon/IconUtil.java +++ b/app/src/main/java/com/cringe_studios/cringe_authenticator/icon/IconUtil.java @@ -2,16 +2,26 @@ package com.cringe_studios.cringe_authenticator.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.cringe_studios.cringe_authenticator.model.OTPData; import com.cringe_studios.cringe_authenticator.util.DialogUtil; import com.cringe_studios.cringe_authenticator.util.IOUtil; import com.cringe_studios.cringe_authenticator.util.SettingsUtil; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -30,6 +40,8 @@ import java.util.zip.ZipInputStream; public class IconUtil { + private static final int ICON_SIZE = 128; + // Source: https://sashamaps.net/docs/resources/20-colors/ private static final List DISTINCT_COLORS = Collections.unmodifiableList(Arrays.asList( Color.parseColor("#e6194B"), // Red @@ -189,16 +201,17 @@ public class IconUtil { return entryBytes; } - public static Bitmap generateCodeImage(String issuer) { - int imageSize = 128; + 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(imageSize, imageSize, Bitmap.Config.ARGB_8888); + 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(imageSize / 2, imageSize / 2, imageSize / 2, p); + c.drawCircle(ICON_SIZE / 2, ICON_SIZE / 2, ICON_SIZE / 2, p); p.setColor(Color.WHITE); p.setAntiAlias(true); @@ -207,7 +220,83 @@ public class IconUtil { String text = issuer.substring(0, 1); Rect r = new Rect(); p.getTextBounds(text, 0, text.length(), r); - c.drawText(text, imageSize / 2 - r.exactCenterX(), imageSize / 2 - r.exactCenterY(), p); + 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 setOTPImage) { + List packs = IconUtil.loadAllIconPacks(context); + + byte[] imageBytes = null; + if(!OTPData.IMAGE_DATA_NONE.equals(imageData)) { + if (imageData == null) { + for (IconPack pack : packs) { + Icon ic = pack.findIconForIssuer(issuer); + if (ic != null) { + imageBytes = ic.getBytes(); + if(setOTPImage != null) { + setOTPImage.accept(Base64.encodeToString(imageBytes, Base64.DEFAULT)); + } + break; + } + } + } else { + try { + imageBytes = Base64.decode(imageData, Base64.DEFAULT); + }catch(IllegalArgumentException ignored) { + // Just use default icon + } + } + } + + if(imageBytes == null) { + view.setImageBitmap(IconUtil.generateCodeImage(issuer, name)); + }else { + 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) { + view.setImageBitmap(IconUtil.generateCodeImage(issuer, name)); + } + } + } + } + + public static void loadEffectiveImage(Context context, OTPData data, SVGImageView view, Runnable saveOTP) { + loadEffectiveImage(context, data.getImageData(), data.getIssuer(), data.getName(), view, saveOTP == null ? null : newData -> { + data.setImageData(newData); + saveOTP.run(); + }); + } + + 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; } diff --git a/app/src/main/java/com/cringe_studios/cringe_authenticator/otplist/OTPListAdapter.java b/app/src/main/java/com/cringe_studios/cringe_authenticator/otplist/OTPListAdapter.java index 6490088..294d791 100644 --- a/app/src/main/java/com/cringe_studios/cringe_authenticator/otplist/OTPListAdapter.java +++ b/app/src/main/java/com/cringe_studios/cringe_authenticator/otplist/OTPListAdapter.java @@ -1,11 +1,8 @@ package com.cringe_studios.cringe_authenticator.otplist; import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; import android.os.Handler; import android.os.Looper; -import android.util.Base64; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -15,20 +12,15 @@ import androidx.annotation.NonNull; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.RecyclerView; -import com.caverock.androidsvg.SVG; -import com.caverock.androidsvg.SVGParseException; import com.cringe_studios.cringe_authenticator.BaseActivity; import com.cringe_studios.cringe_authenticator.R; import com.cringe_studios.cringe_authenticator.databinding.OtpCodeBinding; -import com.cringe_studios.cringe_authenticator.icon.Icon; -import com.cringe_studios.cringe_authenticator.icon.IconPack; import com.cringe_studios.cringe_authenticator.icon.IconUtil; import com.cringe_studios.cringe_authenticator.model.OTPData; import com.cringe_studios.cringe_authenticator.util.DialogUtil; import com.cringe_studios.cringe_authenticator_library.OTPException; import com.cringe_studios.cringe_authenticator_library.OTPType; -import java.io.ByteArrayInputStream; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -84,45 +76,7 @@ public class OTPListAdapter extends RecyclerView.Adapter { holder.getBinding().progress.setVisibility(data.getType() == OTPType.TOTP ? View.VISIBLE : View.GONE); holder.getBinding().refresh.setVisibility(data.getType() == OTPType.HOTP ? View.VISIBLE : View.GONE); - List packs = IconUtil.loadAllIconPacks(context); - - byte[] imageBytes = null; - String imageData = holder.getOTPData().getImageData(); - if(!OTPData.IMAGE_DATA_NONE.equals(imageData)) { - if (imageData == null) { - for (IconPack pack : packs) { - Icon ic = pack.findIconForIssuer(holder.getOTPData().getIssuer()); - if (ic != null) { - imageBytes = ic.getBytes(); - // TODO: save new icon - break; - } - } - } else { - try { - imageBytes = Base64.decode(imageData, Base64.DEFAULT); - }catch(IllegalArgumentException ignored) { - // Just use default icon - } - } - } - - if(imageBytes == null) { - String issuer = data.getIssuer() != null ? data.getIssuer() : data.getName(); - holder.getBinding().otpCodeIcon.setImageBitmap(IconUtil.generateCodeImage(issuer)); - }else { - Bitmap bm = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length); - if(bm != null) { - holder.getBinding().otpCodeIcon.setImageBitmap(bm); - }else { - try { - SVG svg = SVG.getFromInputStream(new ByteArrayInputStream(imageBytes)); - holder.getBinding().otpCodeIcon.setSVG(svg); - }catch(SVGParseException e) { - holder.getBinding().otpCodeIcon.setImageBitmap(IconUtil.generateCodeImage(data.getIssuer())); - } - } - } + IconUtil.loadEffectiveImage(context, holder.getOTPData(), holder.getBinding().otpCodeIcon, saveOTPs); holder.getBinding().refresh.setOnClickListener(view -> { if (data.getType() != OTPType.HOTP) return; diff --git a/app/src/main/java/com/cringe_studios/cringe_authenticator/util/NavigationUtil.java b/app/src/main/java/com/cringe_studios/cringe_authenticator/util/NavigationUtil.java index 87f1340..e5e6699 100644 --- a/app/src/main/java/com/cringe_studios/cringe_authenticator/util/NavigationUtil.java +++ b/app/src/main/java/com/cringe_studios/cringe_authenticator/util/NavigationUtil.java @@ -15,15 +15,22 @@ public class NavigationUtil { // TODO: check if this still works after changes + private static void updateActivity(AppCompatActivity activity, NamedFragment newFragment) { + ActionBar bar = activity.getSupportActionBar(); + if(newFragment == null) newFragment = (NamedFragment) getCurrentFragment(activity.getSupportFragmentManager()); + if(bar != null) bar.setTitle(newFragment.getName()); + activity.invalidateMenu(); + } + + public static void navigate(AppCompatActivity activity, NamedFragment fragment) { + FragmentManager manager = activity.getSupportFragmentManager(); + navigate(manager, fragment, () -> updateActivity(activity, fragment)); + } + public static void navigate(AppCompatActivity activity, Class fragmentClass, Bundle args) { FragmentManager manager = activity.getSupportFragmentManager(); NamedFragment fragment = instantiateFragment(manager, fragmentClass, args); - - ActionBar bar = activity.getSupportActionBar(); - navigate(manager, fragment, () -> { - if(bar != null) bar.setTitle(fragment.getName()); - activity.invalidateMenu(); - }); + navigate(activity, fragment); } public static void openMenu(AppCompatActivity activity, Bundle args) { @@ -37,6 +44,26 @@ public class NavigationUtil { navigate((AppCompatActivity) currentFragment.requireActivity(), fragmentClass, args); } + public static void openOverlay(Fragment currentFragment, NamedFragment overlay) { + AppCompatActivity activity = (AppCompatActivity) currentFragment.requireActivity(); + FragmentManager manager = activity.getSupportFragmentManager(); + manager.beginTransaction() + .setReorderingAllowed(true) + .add(R.id.nav_host_fragment_content_main, overlay) + .runOnCommit(() -> updateActivity(activity, overlay)) + .commit(); + } + + public static void closeOverlay(Fragment currentFragment) { + AppCompatActivity activity = (AppCompatActivity) currentFragment.requireActivity(); + FragmentManager manager = activity.getSupportFragmentManager(); + manager.beginTransaction() + .setReorderingAllowed(true) + .remove(currentFragment) + .runOnCommit(() -> updateActivity(activity, null)) + .commit(); + } + @SuppressWarnings("unchecked") private static T instantiateFragment(FragmentManager manager, Class fragmentClass, Bundle args) { T fragment = (T) manager.getFragmentFactory().instantiate(ClassLoader.getSystemClassLoader(), fragmentClass.getName()); diff --git a/app/src/main/java/com/cringe_studios/cringe_authenticator/util/ThemeUtil.java b/app/src/main/java/com/cringe_studios/cringe_authenticator/util/ThemeUtil.java index 84167e2..c5b5521 100644 --- a/app/src/main/java/com/cringe_studios/cringe_authenticator/util/ThemeUtil.java +++ b/app/src/main/java/com/cringe_studios/cringe_authenticator/util/ThemeUtil.java @@ -1,8 +1,10 @@ package com.cringe_studios.cringe_authenticator.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; @@ -21,12 +23,13 @@ public class ThemeUtil { AppCompatDelegate.setDefaultNightMode(SettingsUtil.getAppearance(activity).getValue()); } - public static void loadBackground(AppCompatActivity activity) { - if(!SettingsUtil.isThemedBackgroundEnabled(activity)) return; + @DrawableRes + public static int getBackground(Context context) { + if(!SettingsUtil.isThemedBackgroundEnabled(context)) return 0; - Theme theme = SettingsUtil.getTheme(activity); + Theme theme = SettingsUtil.getTheme(context); - int nightMode = activity.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; + int nightMode = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; boolean isNightMode; switch(nightMode) { case Configuration.UI_MODE_NIGHT_NO: @@ -38,9 +41,16 @@ public class ThemeUtil { 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(!isNightMode ? theme.getLightBackground() : theme.getDarkBackground()); + v.setBackgroundResource(background); } } diff --git a/app/src/main/res/drawable/button_simple.xml b/app/src/main/res/drawable/button_simple.xml index 358c00b..2e7f716 100644 --- a/app/src/main/res/drawable/button_simple.xml +++ b/app/src/main/res/drawable/button_simple.xml @@ -1,10 +1,7 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_edit_otp.xml b/app/src/main/res/menu/menu_edit_otp.xml new file mode 100644 index 0000000..7656171 --- /dev/null +++ b/app/src/main/res/menu/menu_edit_otp.xml @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_view_otp.xml b/app/src/main/res/menu/menu_view_otp.xml new file mode 100644 index 0000000..0c42d66 --- /dev/null +++ b/app/src/main/res/menu/menu_view_otp.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 7a03de7..58a7c75 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -75,6 +75,7 @@ Startseite Einstellungen Über + OTP bearbeiten Blau/Grün Rot/Blau Pink/Grün diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 24c640b..4a876f7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -66,6 +66,7 @@ Home Settings About + Edit OTP View Edit @@ -99,4 +100,7 @@ Do you really want to disable encryption? Load backup Do you want to load this backup?\n\nThis will delete ALL of the current data in the app and replace it with the data from the backup! + Type + Algorithm + Digits \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 5bb362c..2779cba 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -14,6 +14,8 @@ @drawable/dialog_themed @drawable/menu_themed @drawable/button_themed + + @style/ActionMenuTextAppearance + + \ No newline at end of file