Choices on duplicate icon pack

This commit is contained in:
MrLetsplay 2023-10-01 23:23:50 +02:00
parent d25799719e
commit 596bd7bc08
Signed by: mr
SSH Key Fingerprint: SHA256:92jBH80vpXyaZHjaIl47pjRq+Yt7XGTArqQg1V7hSqg
9 changed files with 214 additions and 32 deletions

View File

@ -19,6 +19,7 @@ import androidx.core.util.Consumer;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import com.cringe_studios.code_guard.databinding.ActivityMainBinding; import com.cringe_studios.code_guard.databinding.ActivityMainBinding;
import com.cringe_studios.code_guard.databinding.DialogIconPackExistsBinding;
import com.cringe_studios.code_guard.databinding.DialogInputCodeChoiceBinding; import com.cringe_studios.code_guard.databinding.DialogInputCodeChoiceBinding;
import com.cringe_studios.code_guard.fragment.AboutFragment; import com.cringe_studios.code_guard.fragment.AboutFragment;
import com.cringe_studios.code_guard.fragment.EditOTPFragment; import com.cringe_studios.code_guard.fragment.EditOTPFragment;
@ -26,6 +27,7 @@ import com.cringe_studios.code_guard.fragment.GroupFragment;
import com.cringe_studios.code_guard.fragment.HomeFragment; import com.cringe_studios.code_guard.fragment.HomeFragment;
import com.cringe_studios.code_guard.fragment.NamedFragment; import com.cringe_studios.code_guard.fragment.NamedFragment;
import com.cringe_studios.code_guard.fragment.SettingsFragment; import com.cringe_studios.code_guard.fragment.SettingsFragment;
import com.cringe_studios.code_guard.icon.IconPack;
import com.cringe_studios.code_guard.icon.IconPackException; import com.cringe_studios.code_guard.icon.IconPackException;
import com.cringe_studios.code_guard.icon.IconPackMetadata; import com.cringe_studios.code_guard.icon.IconPackMetadata;
import com.cringe_studios.code_guard.icon.IconUtil; import com.cringe_studios.code_guard.icon.IconUtil;
@ -146,7 +148,9 @@ public class MainActivity extends BaseActivity {
try { try {
if(doc == null) return; if(doc == null) return;
IconPackMetadata meta = IconUtil.importIconPack(this, doc); IconPackMetadata meta = IconUtil.loadPackMetadata(this, doc);
IconPack existingIconPack = IconUtil.loadIconPack(this, meta.getUuid());
if(!meta.validate()) { if(!meta.validate()) {
DialogUtil.showErrorDialog(this, getString(R.string.error_icon_pack_invalid)); DialogUtil.showErrorDialog(this, getString(R.string.error_icon_pack_invalid));
@ -158,7 +162,57 @@ public class MainActivity extends BaseActivity {
return; return;
} }
DialogUtil.showErrorDialog(this, "Icon pack contains " + meta.getIcons().length + " icons"); if(existingIconPack != null) {
DialogIconPackExistsBinding binding = DialogIconPackExistsBinding.inflate(getLayoutInflater());
binding.iconPackExistsText.setText(getString(R.string.error_icon_pack_exists, meta.getName(), meta.getVersion(), existingIconPack.getMetadata().getName(), existingIconPack.getMetadata().getVersion()));
binding.iconPackExistsChoices.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, getResources().getStringArray(R.array.error_icon_pack_exists_choices)));
AlertDialog dialog = new StyledDialogBuilder(this)
.setTitle("Icon pack already exists")
.setView(binding.getRoot())
.setNeutralButton(R.string.cancel, (d, which) -> {})
.create();
binding.iconPackExistsChoices.setOnItemClickListener((parent, view, position, id) -> {
switch(position) {
case 0: // Override
try {
IconUtil.importIconPack(this, doc);
Toast.makeText(this, getString(R.string.icon_pack_imported, meta.getIcons().length), Toast.LENGTH_LONG).show();
} catch (IconPackException e) {
DialogUtil.showErrorDialog(this, "Failed to import icon pack", e);
}
break;
case 1: // Rename existing
try {
IconUtil.renameIconPack(this, existingIconPack, existingIconPack.getMetadata().getName() + " (" + existingIconPack.getMetadata().getVersion() + ")", UUID.randomUUID().toString());
IconUtil.importIconPack(this, doc);
Toast.makeText(this, getString(R.string.icon_pack_imported, meta.getIcons().length), Toast.LENGTH_LONG).show();
} catch (IconPackException e) {
DialogUtil.showErrorDialog(this, "Failed to import icon pack", e);
}
break;
case 2: // Rename imported
try {
IconUtil.importIconPack(this, doc, meta.getName() + "(" + meta.getVersion() + ")", UUID.randomUUID().toString());
Toast.makeText(this, getString(R.string.icon_pack_imported, meta.getIcons().length), Toast.LENGTH_LONG).show();
} catch (IconPackException e) {
DialogUtil.showErrorDialog(this, "Failed to import icon pack", e);
}
break;
}
dialog.dismiss();
});
dialog.show();
return;
}
IconUtil.importIconPack(this, doc);
Toast.makeText(this, getString(R.string.icon_pack_imported, meta.getIcons().length), Toast.LENGTH_LONG).show();
} catch (IconPackException e) { } catch (IconPackException e) {
DialogUtil.showErrorDialog(this, "Failed to import icon pack", e); DialogUtil.showErrorDialog(this, "Failed to import icon pack", e);
} }

View File

@ -37,6 +37,7 @@ import com.cringe_studios.code_guard.util.SettingsUtil;
import com.cringe_studios.code_guard.util.StyledDialogBuilder; import com.cringe_studios.code_guard.util.StyledDialogBuilder;
import com.cringe_studios.code_guard.util.Theme; import com.cringe_studios.code_guard.util.Theme;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
@ -264,7 +265,17 @@ public class SettingsFragment extends NamedFragment {
binding.settingsLoadIconPack.setOnClickListener(v -> ((MainActivity) requireActivity()).promptPickIconPackFile()); binding.settingsLoadIconPack.setOnClickListener(v -> ((MainActivity) requireActivity()).promptPickIconPackFile());
binding.settingsManageIconPacks.setOnClickListener(v -> { binding.settingsManageIconPacks.setOnClickListener(v -> {
List<IconPack> packs = IconUtil.loadAllIconPacks(requireContext()); 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()) { if(packs.isEmpty()) {
Toast.makeText(requireContext(), R.string.no_icon_packs_installed, Toast.LENGTH_LONG).show(); Toast.makeText(requireContext(), R.string.no_icon_packs_installed, Toast.LENGTH_LONG).show();
return; return;

View File

@ -41,7 +41,13 @@ public class IconPackListAdapter extends RecyclerView.Adapter<IconPackItem> {
holder.getBinding().iconPackName.setText(pack.getMetadata().getName()); holder.getBinding().iconPackName.setText(pack.getMetadata().getName());
holder.getBinding().iconPackDelete.setOnClickListener(view -> { holder.getBinding().iconPackDelete.setOnClickListener(view -> {
DialogUtil.showYesNo(context, R.string.delete_pack_title, R.string.delete_pack_message, () -> IconUtil.removeIconPack(context, pack.getMetadata().getUuid()), null); 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);
}); });
} }

View File

@ -9,10 +9,18 @@ public class IconPackMetadata {
private IconPackMetadata() {} private IconPackMetadata() {}
public void setUuid(String uuid) {
this.uuid = uuid;
}
public String getUuid() { public String getUuid() {
return uuid; return uuid;
} }
public void setName(String name) {
this.name = name;
}
public String getName() { public String getName() {
return name; return name;
} }

View File

@ -17,13 +17,15 @@ import com.caverock.androidsvg.SVG;
import com.caverock.androidsvg.SVGImageView; import com.caverock.androidsvg.SVGImageView;
import com.caverock.androidsvg.SVGParseException; import com.caverock.androidsvg.SVGParseException;
import com.cringe_studios.code_guard.model.OTPData; import com.cringe_studios.code_guard.model.OTPData;
import com.cringe_studios.code_guard.util.DialogUtil;
import com.cringe_studios.code_guard.util.IOUtil; import com.cringe_studios.code_guard.util.IOUtil;
import com.cringe_studios.code_guard.util.SettingsUtil; import com.cringe_studios.code_guard.util.SettingsUtil;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -38,6 +40,7 @@ import java.util.Map;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream; import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
public class IconUtil { public class IconUtil {
@ -79,10 +82,9 @@ public class IconUtil {
return iconPacksDir; return iconPacksDir;
} }
public static IconPackMetadata importIconPack(Context context, Uri uri) throws IconPackException { public static void importIconPack(Context context, Uri uri) throws IconPackException {
IconPackMetadata meta = loadPackMetadata(context, uri); IconPackMetadata meta = loadPackMetadata(context, uri);
// TODO: check for existing icon pack
File iconPackFile = new File(getIconPacksDir(context), meta.getUuid()); File iconPackFile = new File(getIconPacksDir(context), meta.getUuid());
try { try {
@ -90,7 +92,7 @@ public class IconUtil {
iconPackFile.createNewFile(); iconPackFile.createNewFile();
} }
try (OutputStream out = new FileOutputStream(iconPackFile); try (OutputStream out = new BufferedOutputStream(new FileOutputStream(iconPackFile));
InputStream in = context.getContentResolver().openInputStream(uri)) { InputStream in = context.getContentResolver().openInputStream(uri)) {
if(in == null) throw new IconPackException("Failed to read icon pack"); if(in == null) throw new IconPackException("Failed to read icon pack");
byte[] bytes = IOUtil.readBytes(in); byte[] bytes = IOUtil.readBytes(in);
@ -99,8 +101,54 @@ public class IconUtil {
}catch(IOException e) { }catch(IOException e) {
throw new IconPackException("Failed to import icon pack", e); throw new IconPackException("Failed to import icon pack", e);
} }
}
return meta; 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) { public static void removeIconPack(Context context, String uuid) {
@ -109,12 +157,14 @@ public class IconUtil {
loadedPacks.remove(uuid); loadedPacks.remove(uuid);
} }
private static IconPackMetadata loadPackMetadata(Context context, Uri uri) throws IconPackException { public static IconPackMetadata loadPackMetadata(Context context, Uri uri) throws IconPackException {
try(InputStream in = context.getContentResolver().openInputStream(uri)) { try(InputStream in = context.getContentResolver().openInputStream(uri)) {
if(in == null) throw new IconPackException("Failed to read icon pack"); if(in == null) throw new IconPackException("Failed to read icon pack");
try(ZipInputStream zIn = new ZipInputStream(in)) { try(ZipInputStream zIn = new ZipInputStream(in)) {
ZipEntry en; ZipEntry en;
while((en = zIn.getNextEntry()) != null) { while((en = zIn.getNextEntry()) != null) {
if(en.isDirectory()) continue;
if(en.getName().equals("pack.json")) { if(en.getName().equals("pack.json")) {
byte[] entryBytes = readEntry(zIn, en); byte[] entryBytes = readEntry(zIn, en);
return SettingsUtil.GSON.fromJson(new String(entryBytes, StandardCharsets.UTF_8), IconPackMetadata.class); // TODO: validate metadata return SettingsUtil.GSON.fromJson(new String(entryBytes, StandardCharsets.UTF_8), IconPackMetadata.class); // TODO: validate metadata
@ -128,6 +178,28 @@ public class IconUtil {
throw new IconPackException("No pack.json"); 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) { public static Map<String, List<Icon>> loadAllIcons(Context context) {
List<IconPack> packs = loadAllIconPacks(context); List<IconPack> packs = loadAllIconPacks(context);
@ -148,7 +220,7 @@ public class IconUtil {
return icons; return icons;
} }
public static List<IconPack> loadAllIconPacks(Context context) { public static List<IconPack> loadAllIconPacks(Context context, Consumer<String> brokenPack) {
File iconPacksDir = getIconPacksDir(context); File iconPacksDir = getIconPacksDir(context);
String[] packIDs = iconPacksDir.list(); String[] packIDs = iconPacksDir.list();
@ -157,15 +229,24 @@ public class IconUtil {
List<IconPack> packs = new ArrayList<>(); List<IconPack> packs = new ArrayList<>();
for(String pack : packIDs) { for(String pack : packIDs) {
try { try {
packs.add(loadIconPack(context, pack)); 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) { }catch(IconPackException e) {
DialogUtil.showErrorDialog(context, "An icon pack failed to load", e); e.printStackTrace();
if(brokenPack != null) brokenPack.accept(pack);
//DialogUtil.showErrorDialog(context, "An icon pack failed to load", e);
} }
} }
return packs; return packs;
} }
public static List<IconPack> loadAllIconPacks(Context context) {
return loadAllIconPacks(context, null);
}
public static IconPack loadIconPack(Context context, String uuid) throws IconPackException { public static IconPack loadIconPack(Context context, String uuid) throws IconPackException {
if(loadedPacks.containsKey(uuid)) return loadedPacks.get(uuid); if(loadedPacks.containsKey(uuid)) return loadedPacks.get(uuid);
@ -179,12 +260,14 @@ public class IconUtil {
private static IconPack loadIconPack(File file) throws IconPackException { private static IconPack loadIconPack(File file) throws IconPackException {
if(!file.exists()) return null; if(!file.exists()) return null;
try(ZipInputStream in = new ZipInputStream(new FileInputStream(file))) { try(ZipInputStream in = new ZipInputStream(new BufferedInputStream(new FileInputStream(file)))) {
IconPackMetadata metadata = null; IconPackMetadata metadata = null;
Map<String, byte[]> files = new HashMap<>(); Map<String, byte[]> files = new HashMap<>();
ZipEntry en; ZipEntry en;
while((en = in.getNextEntry()) != null) { while((en = in.getNextEntry()) != null) {
if(en.isDirectory()) continue;
byte[] entryBytes = readEntry(in, en); byte[] entryBytes = readEntry(in, en);
if(en.getName().equals("pack.json")) { if(en.getName().equals("pack.json")) {
@ -214,18 +297,11 @@ public class IconUtil {
} }
private static byte[] readEntry(ZipInputStream in, ZipEntry en) throws IOException { private static byte[] readEntry(ZipInputStream in, ZipEntry en) throws IOException {
if (en.getSize() < 0 || en.getSize() > Integer.MAX_VALUE) { if (en.getSize() > Integer.MAX_VALUE) {
throw new IOException("Invalid ZIP entry"); throw new IOException("Invalid ZIP entry");
} }
byte[] entryBytes = new byte[(int) en.getSize()]; return IOUtil.readBytes(in);
int totalRead = 0;
while (totalRead < entryBytes.length) {
totalRead += in.read(entryBytes, totalRead, entryBytes.length - totalRead);
}
return entryBytes;
} }
public static Bitmap generateCodeImage(String issuer, String name) { public static Bitmap generateCodeImage(String issuer, String name) {

View File

@ -1,9 +1,9 @@
package com.cringe_studios.code_guard.util; package com.cringe_studios.code_guard.util;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@ -11,7 +11,7 @@ import java.nio.ByteBuffer;
public class IOUtil { public class IOUtil {
public static byte[] readBytes(File file) throws IOException { public static byte[] readBytes(File file) throws IOException {
try(FileInputStream fIn = new FileInputStream(file)) { try(InputStream fIn = new BufferedInputStream(new FileInputStream(file))) {
ByteBuffer fileBuffer = ByteBuffer.allocate((int) file.length()); ByteBuffer fileBuffer = ByteBuffer.allocate((int) file.length());
byte[] buffer = new byte[1024]; byte[] buffer = new byte[1024];
int len; int len;
@ -34,10 +34,4 @@ public class IOUtil {
return bOut.toByteArray(); return bOut.toByteArray();
} }
public static void writeBytes(File file, byte[] bytes) throws IOException {
try(FileOutputStream fOut = new FileOutputStream(file)) {
fOut.write(bytes);
}
}
} }

View File

@ -11,9 +11,11 @@ import com.cringe_studios.code_guard.model.OTPData;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException; import com.google.gson.JsonSyntaxException;
import java.io.BufferedOutputStream;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -148,7 +150,7 @@ public class OTPDatabase {
byte[] dbBytes = convertToEncryptedBytes(loadedDatabase, loadedKey, parameters); byte[] dbBytes = convertToEncryptedBytes(loadedDatabase, loadedKey, parameters);
try(FileOutputStream fOut = new FileOutputStream(file)) { try(OutputStream fOut = new BufferedOutputStream(new FileOutputStream(file))) {
fOut.write(dbBytes); fOut.write(dbBytes);
} catch (IOException e) { } catch (IOException e) {
throw new OTPDatabaseException(e); throw new OTPDatabaseException(e);

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

@ -123,4 +123,13 @@
<string name="patreon_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_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_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.\n\nImported: %s (version %d)\nExisting: %s (version %d)\n\nWhat 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.\n\nDo you want to delete the broken icon packs?</string>
<string name="icon_pack_imported">Icon pack with %d icon(s) imported</string>
<string-array name="error_icon_pack_exists_choices">
<item>Override</item>
<item>Rename existing</item>
<item>Rename imported</item>
</string-array>
</resources> </resources>