Add OTP edit fragment, Icon picker (WIP)
This commit is contained in:
parent
1ee9a6fabe
commit
0352d9c777
@ -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'
|
||||
|
@ -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" >
|
||||
</activity>
|
||||
<activity android:name=".unlock.UnlockActivity"
|
||||
android:exported="false"
|
||||
|
@ -21,6 +21,7 @@ import androidx.fragment.app.Fragment;
|
||||
import com.cringe_studios.cringe_authenticator.databinding.ActivityMainBinding;
|
||||
import com.cringe_studios.cringe_authenticator.databinding.DialogInputCodeChoiceBinding;
|
||||
import com.cringe_studios.cringe_authenticator.fragment.AboutFragment;
|
||||
import com.cringe_studios.cringe_authenticator.fragment.EditOTPFragment;
|
||||
import com.cringe_studios.cringe_authenticator.fragment.GroupFragment;
|
||||
import com.cringe_studios.cringe_authenticator.fragment.HomeFragment;
|
||||
import com.cringe_studios.cringe_authenticator.fragment.NamedFragment;
|
||||
@ -61,6 +62,10 @@ public class MainActivity extends BaseActivity {
|
||||
|
||||
private ActivityResultLauncher<String[]> pickIconPackFileLoad;
|
||||
|
||||
private ActivityResultLauncher<PickVisualMediaRequest> pickIconImage;
|
||||
|
||||
private Consumer<Uri> 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<Uri> 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;
|
||||
|
@ -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<OTPData> callback;
|
||||
|
||||
public EditOTPFragment(OTPData data, boolean view, Consumer<OTPData> 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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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() {
|
||||
|
@ -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<Integer> 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<String> setOTPImage) {
|
||||
List<IconPack> 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;
|
||||
}
|
||||
|
@ -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<OTPListItem> {
|
||||
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<IconPack> 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;
|
||||
|
@ -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<? extends NamedFragment> 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 extends Fragment> T instantiateFragment(FragmentManager manager, Class<? extends T> fragmentClass, Bundle args) {
|
||||
T fragment = (T) manager.getFragmentFactory().instantiate(ClassLoader.getSystemClassLoader(), fragmentClass.getName());
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,7 @@
|
||||
<?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" />
|
||||
<shape>
|
||||
<solid android:color="?attr/colorOnBackground" />
|
||||
<corners
|
||||
android:bottomLeftRadius="20dp"
|
||||
|
153
app/src/main/res/layout/fragment_edit_otp.xml
Normal file
153
app/src/main/res/layout/fragment_edit_otp.xml
Normal file
@ -0,0 +1,153 @@
|
||||
<?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.HomeFragment">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp"
|
||||
android:background="?android:attr/colorBackground">
|
||||
|
||||
<com.caverock.androidsvg.SVGImageView
|
||||
android:id="@+id/input_image"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="100dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
tools:src="@drawable/baseline_edit_24" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/otp_add_type" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/input_type"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:text="@string/otp_add_name" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/input_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ems="10"
|
||||
android:inputType="text"
|
||||
android:hint="@string/otp_add_name"
|
||||
android:autofillHints="" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:text="@string/otp_add_issuer" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/input_issuer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ems="10"
|
||||
android:inputType="text"
|
||||
android:hint="@string/otp_add_issuer"
|
||||
android:autofillHints="" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:text="@string/otp_add_secret" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/input_secret"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ems="10"
|
||||
android:inputType="text"
|
||||
android:hint="@string/otp_add_secret"
|
||||
android:autofillHints="" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:text="@string/otp_add_algorithm" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/input_algorithm"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:text="@string/otp_add_digits" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/input_digits"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/input_checksum"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/otp_add_checksum" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_period"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:text="@string/otp_add_period" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/input_period"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ems="10"
|
||||
android:inputType="number"
|
||||
android:hint="@string/otp_add_period"
|
||||
android:autofillHints="" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_counter"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:text="@string/otp_add_counter" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/input_counter"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ems="10"
|
||||
android:inputType="number"
|
||||
android:hint="@string/otp_add_counter"
|
||||
android:autofillHints="" />
|
||||
|
||||
<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>
|
16
app/src/main/res/menu/menu_edit_otp.xml
Normal file
16
app/src/main/res/menu/menu_edit_otp.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/action_cancel"
|
||||
android:orderInCategory="100"
|
||||
android:title="Cancel"
|
||||
android:onClick="cancelEditingOTP"
|
||||
app:showAsAction="always" />
|
||||
<item
|
||||
android:id="@+id/action_save"
|
||||
android:orderInCategory="100"
|
||||
android:title="Save"
|
||||
android:onClick="saveOTP"
|
||||
app:showAsAction="always" />
|
||||
</menu>
|
10
app/src/main/res/menu/menu_view_otp.xml
Normal file
10
app/src/main/res/menu/menu_view_otp.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/action_cancel"
|
||||
android:orderInCategory="100"
|
||||
android:title="Close"
|
||||
android:onClick="cancelEditingOTP"
|
||||
app:showAsAction="always" />
|
||||
</menu>
|
@ -75,6 +75,7 @@
|
||||
<string name="fragment_home">Startseite</string>
|
||||
<string name="fragment_settings">Einstellungen</string>
|
||||
<string name="fragment_about">Über</string>
|
||||
<string name="fragment_edit_otp">OTP bearbeiten</string>
|
||||
<string name="theme_blue_green">Blau/Grün</string>
|
||||
<string name="theme_red_blue">Rot/Blau</string>
|
||||
<string name="theme_pink_green">Pink/Grün</string>
|
||||
|
@ -66,6 +66,7 @@
|
||||
<string name="fragment_home">Home</string>
|
||||
<string name="fragment_settings">Settings</string>
|
||||
<string name="fragment_about">About</string>
|
||||
<string name="fragment_edit_otp">Edit OTP</string>
|
||||
<string-array name="view_edit_move_delete">
|
||||
<item>View</item>
|
||||
<item>Edit</item>
|
||||
@ -99,4 +100,7 @@
|
||||
<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?\n\nThis 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>
|
||||
</resources>
|
@ -14,6 +14,8 @@
|
||||
<item name="dialogBackground">@drawable/dialog_themed</item>
|
||||
<item name="menuBackground">@drawable/menu_themed</item>
|
||||
<item name="buttonBackground">@drawable/button_themed</item>
|
||||
|
||||
<item name="actionMenuTextAppearance">@style/ActionMenuTextAppearance</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.CringeAuthenticator" parent="Base.Theme.CringeAuthenticator" />
|
||||
@ -53,4 +55,8 @@
|
||||
<style name="ActionPopupMenuStyle" parent="Widget.AppCompat.PopupMenu">
|
||||
<item name="android:popupBackground">?attr/menuBackground</item>
|
||||
</style>
|
||||
|
||||
<style name="ActionMenuTextAppearance" parent="TextAppearance.AppCompat.Widget.ActionBar.Menu">
|
||||
<item name="android:textAllCaps">false</item>
|
||||
</style>
|
||||
</resources>
|
Loading…
Reference in New Issue
Block a user