diff --git a/app/src/main/java/com/cringe_studios/code_guard/MainActivity.java b/app/src/main/java/com/cringe_studios/code_guard/MainActivity.java index 9076092..a64f8e9 100644 --- a/app/src/main/java/com/cringe_studios/code_guard/MainActivity.java +++ b/app/src/main/java/com/cringe_studios/code_guard/MainActivity.java @@ -19,6 +19,7 @@ import androidx.core.util.Consumer; import androidx.fragment.app.Fragment; 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.fragment.AboutFragment; 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.NamedFragment; 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.IconPackMetadata; import com.cringe_studios.code_guard.icon.IconUtil; @@ -146,7 +148,9 @@ public class MainActivity extends BaseActivity { try { 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()) { DialogUtil.showErrorDialog(this, getString(R.string.error_icon_pack_invalid)); @@ -158,7 +162,57 @@ public class MainActivity extends BaseActivity { 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) { DialogUtil.showErrorDialog(this, "Failed to import icon pack", e); } diff --git a/app/src/main/java/com/cringe_studios/code_guard/fragment/SettingsFragment.java b/app/src/main/java/com/cringe_studios/code_guard/fragment/SettingsFragment.java index c6976bc..9bd0748 100644 --- a/app/src/main/java/com/cringe_studios/code_guard/fragment/SettingsFragment.java +++ b/app/src/main/java/com/cringe_studios/code_guard/fragment/SettingsFragment.java @@ -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.Theme; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; @@ -264,7 +265,17 @@ public class SettingsFragment extends NamedFragment { binding.settingsLoadIconPack.setOnClickListener(v -> ((MainActivity) requireActivity()).promptPickIconPackFile()); binding.settingsManageIconPacks.setOnClickListener(v -> { - List packs = IconUtil.loadAllIconPacks(requireContext()); + List brokenPacks = new ArrayList<>(); + List 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; diff --git a/app/src/main/java/com/cringe_studios/code_guard/icon/IconPackListAdapter.java b/app/src/main/java/com/cringe_studios/code_guard/icon/IconPackListAdapter.java index fc61f10..b0a8aa5 100644 --- a/app/src/main/java/com/cringe_studios/code_guard/icon/IconPackListAdapter.java +++ b/app/src/main/java/com/cringe_studios/code_guard/icon/IconPackListAdapter.java @@ -41,7 +41,13 @@ public class IconPackListAdapter extends RecyclerView.Adapter { 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()), 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); }); } diff --git a/app/src/main/java/com/cringe_studios/code_guard/icon/IconPackMetadata.java b/app/src/main/java/com/cringe_studios/code_guard/icon/IconPackMetadata.java index 74cf4e8..68fc2d0 100644 --- a/app/src/main/java/com/cringe_studios/code_guard/icon/IconPackMetadata.java +++ b/app/src/main/java/com/cringe_studios/code_guard/icon/IconPackMetadata.java @@ -9,10 +9,18 @@ public class IconPackMetadata { 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; } diff --git a/app/src/main/java/com/cringe_studios/code_guard/icon/IconUtil.java b/app/src/main/java/com/cringe_studios/code_guard/icon/IconUtil.java index 8c93884..ff4a49c 100644 --- a/app/src/main/java/com/cringe_studios/code_guard/icon/IconUtil.java +++ b/app/src/main/java/com/cringe_studios/code_guard/icon/IconUtil.java @@ -17,13 +17,15 @@ import com.caverock.androidsvg.SVG; import com.caverock.androidsvg.SVGImageView; import com.caverock.androidsvg.SVGParseException; 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.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; @@ -38,6 +40,7 @@ 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 { @@ -79,10 +82,9 @@ public class IconUtil { 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); - // TODO: check for existing icon pack File iconPackFile = new File(getIconPacksDir(context), meta.getUuid()); try { @@ -90,7 +92,7 @@ public class IconUtil { iconPackFile.createNewFile(); } - try (OutputStream out = new FileOutputStream(iconPackFile); + 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); @@ -99,8 +101,54 @@ public class IconUtil { }catch(IOException 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) { @@ -109,12 +157,14 @@ public class IconUtil { 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)) { 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 @@ -128,6 +178,28 @@ public class IconUtil { 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> loadAllIcons(Context context) { List packs = loadAllIconPacks(context); @@ -148,7 +220,7 @@ public class IconUtil { return icons; } - public static List loadAllIconPacks(Context context) { + public static List loadAllIconPacks(Context context, Consumer brokenPack) { File iconPacksDir = getIconPacksDir(context); String[] packIDs = iconPacksDir.list(); @@ -157,15 +229,24 @@ public class IconUtil { List packs = new ArrayList<>(); for(String pack : packIDs) { 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) { - 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; } + public static List 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); @@ -179,12 +260,14 @@ public class IconUtil { private static IconPack loadIconPack(File file) throws IconPackException { 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; Map 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")) { @@ -214,18 +297,11 @@ public class IconUtil { } 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"); } - byte[] entryBytes = new byte[(int) en.getSize()]; - - int totalRead = 0; - while (totalRead < entryBytes.length) { - totalRead += in.read(entryBytes, totalRead, entryBytes.length - totalRead); - } - - return entryBytes; + return IOUtil.readBytes(in); } public static Bitmap generateCodeImage(String issuer, String name) { diff --git a/app/src/main/java/com/cringe_studios/code_guard/util/IOUtil.java b/app/src/main/java/com/cringe_studios/code_guard/util/IOUtil.java index 9d8f0e8..13e0cc1 100644 --- a/app/src/main/java/com/cringe_studios/code_guard/util/IOUtil.java +++ b/app/src/main/java/com/cringe_studios/code_guard/util/IOUtil.java @@ -1,9 +1,9 @@ package com.cringe_studios.code_guard.util; +import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; @@ -11,7 +11,7 @@ import java.nio.ByteBuffer; public class IOUtil { 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()); byte[] buffer = new byte[1024]; int len; @@ -34,10 +34,4 @@ public class IOUtil { return bOut.toByteArray(); } - public static void writeBytes(File file, byte[] bytes) throws IOException { - try(FileOutputStream fOut = new FileOutputStream(file)) { - fOut.write(bytes); - } - } - } diff --git a/app/src/main/java/com/cringe_studios/code_guard/util/OTPDatabase.java b/app/src/main/java/com/cringe_studios/code_guard/util/OTPDatabase.java index 130f0c6..22663a4 100644 --- a/app/src/main/java/com/cringe_studios/code_guard/util/OTPDatabase.java +++ b/app/src/main/java/com/cringe_studios/code_guard/util/OTPDatabase.java @@ -11,9 +11,11 @@ import com.cringe_studios.code_guard.model.OTPData; import com.google.gson.JsonObject; import com.google.gson.JsonSyntaxException; +import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; @@ -148,7 +150,7 @@ public class OTPDatabase { byte[] dbBytes = convertToEncryptedBytes(loadedDatabase, loadedKey, parameters); - try(FileOutputStream fOut = new FileOutputStream(file)) { + try(OutputStream fOut = new BufferedOutputStream(new FileOutputStream(file))) { fOut.write(dbBytes); } catch (IOException e) { throw new OTPDatabaseException(e); diff --git a/app/src/main/res/layout/dialog_icon_pack_exists.xml b/app/src/main/res/layout/dialog_icon_pack_exists.xml new file mode 100644 index 0000000..d3246f0 --- /dev/null +++ b/app/src/main/res/layout/dialog_icon_pack_exists.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 47c7227..2ee3b75 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -123,4 +123,13 @@ https://git.cringe-studios.com/CringeStudios/Code-Guard The icon pack doesn\'t contain any icons Pack contains invalid metadata. Make sure you selected the correct file + 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? + Broken icon packs + Some icon packs failed to load.\n\nDo you want to delete the broken icon packs? + Icon pack with %d icon(s) imported + + Override + Rename existing + Rename imported + \ No newline at end of file