From 2b4928d7d3581cc73e3d85a7f917b87cce688ba8 Mon Sep 17 00:00:00 2001 From: Muteeb Ur Rehman <139336141+muteeburrehman@users.noreply.github.com> Date: Wed, 29 May 2024 19:41:55 +0500 Subject: [PATCH] Add android example app (#32) * Add example app using SecretBox and SealedBox * remove unnecessary files * Fix build * Fix existing files * minor changes * Fix themes * Remove unnecesssary permissions * Add constants for Shared Preference keys * Fix backstack * Add missing EOF * Add missing EOF * Remove unnecessary code * Handle backstack --------- Co-authored-by: Muzzammil --- android-example/build.gradle | 13 +- android-example/src/main/AndroidManifest.xml | 21 +- .../io/xconn/androidexample/MainActivity.java | 113 +++++++- .../fragment/CameraFragment.java | 171 +++++++++++ .../fragment/GalleryFragment.java | 266 ++++++++++++++++++ .../io/xconn/androidexample/util/App.java | 52 ++++ .../io/xconn/androidexample/util/Helpers.java | 137 +++++++++ .../src/main/res/drawable/broken_image.xml | 5 + .../src/main/res/drawable/camera.xml | 5 + .../main/res/drawable/edittext_background.xml | 8 + .../src/main/res/drawable/gallery.xml | 5 + .../src/main/res/drawable/image_icon.xml | 5 + .../src/main/res/layout/activity_main.xml | 41 ++- .../src/main/res/layout/custom_dialog_box.xml | 25 ++ .../src/main/res/layout/fragment_camera.xml | 53 ++++ .../src/main/res/layout/fragment_gallery.xml | 18 ++ .../src/main/res/layout/gallery_item.xml | 15 + .../main/res/menu/bottom_navigation_menu.xml | 12 + .../src/main/res/values-night/themes.xml | 12 +- .../src/main/res/values/colors.xml | 1 + .../src/main/res/values/strings.xml | 11 + .../src/main/res/values/themes.xml | 12 +- 22 files changed, 975 insertions(+), 26 deletions(-) create mode 100644 android-example/src/main/java/io/xconn/androidexample/fragment/CameraFragment.java create mode 100644 android-example/src/main/java/io/xconn/androidexample/fragment/GalleryFragment.java create mode 100644 android-example/src/main/java/io/xconn/androidexample/util/App.java create mode 100644 android-example/src/main/java/io/xconn/androidexample/util/Helpers.java create mode 100644 android-example/src/main/res/drawable/broken_image.xml create mode 100644 android-example/src/main/res/drawable/camera.xml create mode 100644 android-example/src/main/res/drawable/edittext_background.xml create mode 100644 android-example/src/main/res/drawable/gallery.xml create mode 100644 android-example/src/main/res/drawable/image_icon.xml create mode 100644 android-example/src/main/res/layout/custom_dialog_box.xml create mode 100644 android-example/src/main/res/layout/fragment_camera.xml create mode 100644 android-example/src/main/res/layout/fragment_gallery.xml create mode 100644 android-example/src/main/res/layout/gallery_item.xml create mode 100644 android-example/src/main/res/menu/bottom_navigation_menu.xml diff --git a/android-example/build.gradle b/android-example/build.gradle index d32ce5a..2bf46c4 100644 --- a/android-example/build.gradle +++ b/android-example/build.gradle @@ -28,6 +28,17 @@ android { dependencies { implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'com.google.android.material:material:1.12.0' implementation 'androidx.activity:activity:1.9.0' + + implementation project(path: ':cryptology') + + // Jetpack Navigation Component + implementation "androidx.navigation:navigation-fragment-ktx:2.7.7" + implementation "androidx.navigation:navigation-ui-ktx:2.7.7" + + // Material Components for Android + implementation "com.google.android.material:material:1.12.0" + + // DrawerLayout + implementation "androidx.drawerlayout:drawerlayout:1.2.0" } diff --git a/android-example/src/main/AndroidManifest.xml b/android-example/src/main/AndroidManifest.xml index 7abf83b..bb75985 100644 --- a/android-example/src/main/AndroidManifest.xml +++ b/android-example/src/main/AndroidManifest.xml @@ -1,16 +1,29 @@ - + + + + + + + + + android:theme="@style/Theme.Cryptology" + tools:targetApi="31"> + + android:exported="true"> diff --git a/android-example/src/main/java/io/xconn/androidexample/MainActivity.java b/android-example/src/main/java/io/xconn/androidexample/MainActivity.java index e830cad..f235167 100644 --- a/android-example/src/main/java/io/xconn/androidexample/MainActivity.java +++ b/android-example/src/main/java/io/xconn/androidexample/MainActivity.java @@ -1,14 +1,125 @@ package io.xconn.androidexample; +import static io.xconn.androidexample.util.Helpers.bytesToHex; +import static io.xconn.androidexample.util.Helpers.convertTo32Bytes; + import android.os.Bundle; + import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; + +import com.google.android.material.bottomnavigation.BottomNavigationView; + +import java.util.Objects; + +import io.xconn.cryptology.KeyPair; +import io.xconn.cryptology.SealedBox; +import io.xconn.cryptology.SecretBox; +import io.xconn.androidexample.fragment.CameraFragment; +import io.xconn.androidexample.fragment.GalleryFragment; +import io.xconn.androidexample.util.App; +import io.xconn.androidexample.util.Helpers; -public class MainActivity extends AppCompatActivity { +public class MainActivity extends AppCompatActivity implements Helpers.PasswordDialogListener { + + private FragmentManager fragmentManager; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); + + fragmentManager = getSupportFragmentManager(); + BottomNavigationView bottomNavigationView = findViewById(R.id.bottom_navigation); + + Fragment cameraFragment = new CameraFragment(); + fragmentManager.beginTransaction().replace(R.id.frameLayout, cameraFragment).commit(); + + bottomNavigationView.setOnItemSelectedListener(item -> { + Fragment fragment = null; + if (item.getItemId() == R.id.menu_camera) { + fragment = new CameraFragment(); + } else if (item.getItemId() == R.id.menu_gallery) { + fragment = new GalleryFragment(); + } + + if (fragment != null) { + Fragment currentFragment = fragmentManager.findFragmentById(R.id.frameLayout); + + // Do nothing if already on the selected fragment + if (currentFragment != null && currentFragment.getClass().equals(fragment.getClass())) { + return true; + } + + // Clear the backstack + fragmentManager.popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE); + + FragmentTransaction transaction = fragmentManager.beginTransaction() + .replace(R.id.frameLayout, fragment); + + // Add to backstack if it's GalleryFragment + if (fragment instanceof GalleryFragment) { + transaction.addToBackStack(null); + } + + transaction.commit(); + return true; + } + return false; + }); + + fragmentManager.addOnBackStackChangedListener(() -> { + Fragment currentFragment = fragmentManager.findFragmentById(R.id.frameLayout); + + if (currentFragment instanceof CameraFragment) { + bottomNavigationView.setSelectedItemId(R.id.menu_camera); + } else if (currentFragment instanceof GalleryFragment) { + bottomNavigationView.setSelectedItemId(R.id.menu_gallery); + } + }); + + if (!App.getBoolean(App.PREF_IS_DIALOG_SHOWN)) { + Helpers.showPasswordDialog(this, this, false); + } + } + + + @Override + public boolean onPasswordSubmit(String password) { + if (password.isEmpty()) { + return false; + } + + // Generate key pair + KeyPair keyPair = SealedBox.generateKeyPair(); + + // Convert public key to hexadecimal string and save it + String publicKey = bytesToHex(keyPair.getPublicKey()); + App.saveString(App.PREF_PUBLIC_KEY, publicKey); + + // Generate nonce and save it + byte[] nonce = SecretBox.generateNonce(); + App.saveString(App.PREF_NONCE, bytesToHex(nonce)); + + // Encrypt private key with entered password and save it + byte[] encryptedPrivateKey = SecretBox.box(nonce, keyPair.getPrivateKey(), + Objects.requireNonNull(convertTo32Bytes(password))); + App.saveString(App.PREF_PRIVATE_KEY, bytesToHex(encryptedPrivateKey)); + + App.saveBoolean(App.PREF_IS_DIALOG_SHOWN, true); + return true; + } + + @Override + public void onPasswordCancel() { + + } + + @Override + public void onDismissed(boolean dismissedAfterSubmit) { + } } diff --git a/android-example/src/main/java/io/xconn/androidexample/fragment/CameraFragment.java b/android-example/src/main/java/io/xconn/androidexample/fragment/CameraFragment.java new file mode 100644 index 0000000..ec5397b --- /dev/null +++ b/android-example/src/main/java/io/xconn/androidexample/fragment/CameraFragment.java @@ -0,0 +1,171 @@ +package io.xconn.androidexample.fragment; + +import static android.app.Activity.RESULT_OK; +import static io.xconn.androidexample.util.Helpers.bytesToHex; +import static io.xconn.androidexample.util.Helpers.hexToBytes; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.content.Intent; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.provider.MediaStore; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Objects; + +import io.xconn.androidexample.R; +import io.xconn.androidexample.util.App; +import io.xconn.cryptology.SealedBox; + +public class CameraFragment extends Fragment { + + private ActivityResultLauncher cameraPermissionLauncher; + private ActivityResultLauncher cameraLauncher; + private ActivityResultLauncher galleryLauncher; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_camera, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + view.findViewById(R.id.button_capture).setOnClickListener(v -> + cameraPermissionLauncher.launch(Manifest.permission.CAMERA)); + view.findViewById(R.id.button_select_photo).setOnClickListener(v -> openGallery()); + + // Initialize ActivityResultLaunchers + cameraLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == RESULT_OK) { + handleCameraResult(result.getData()); + } + } + ); + + galleryLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == RESULT_OK) { + handleGalleryResult(result.getData()); + } + } + ); + + // Initialize camera permission launcher + cameraPermissionLauncher = registerForActivityResult( + new ActivityResultContracts.RequestPermission(), + isGranted -> { + if (isGranted) { + startCamera(); + } else { + Toast.makeText(requireContext(), "Camera permission denied", + Toast.LENGTH_SHORT).show(); + } + } + ); + } + + @SuppressLint("QueryPermissionsNeeded") + private void startCamera() { + Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + if (takePictureIntent.resolveActivity(requireActivity().getPackageManager()) != null) { + cameraLauncher.launch(takePictureIntent); + } else { + Toast.makeText(requireContext(), "No camera app found", Toast.LENGTH_SHORT).show(); + } + } + + private void openGallery() { + Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); + galleryLauncher.launch(intent); + } + + private void handleCameraResult(@Nullable Intent data) { + assert data != null; + Bitmap bitmap = (Bitmap) Objects.requireNonNull(data.getExtras()).get("data"); + assert bitmap != null; + byte[] imageData = bitmapToByteArray(bitmap); + + byte[] publicKey = hexToBytes(App.getString(App.PREF_PUBLIC_KEY)); + Log.d("PublicKey", "Public Key: " + bytesToHex(publicKey)); + + byte[] encryptedImageData = SealedBox.seal(imageData, publicKey); + saveImageToFile(encryptedImageData); + } + + private void handleGalleryResult(@Nullable Intent data) { + try { + if (data != null && data.getData() != null) { + Bitmap bitmap = MediaStore.Images.Media.getBitmap( + requireActivity().getContentResolver(), data.getData()); + byte[] imageData = bitmapToByteArray(bitmap); + + byte[] publicKey = hexToBytes(App.getString(App.PREF_PUBLIC_KEY)); + + byte[] encryptedImageData = SealedBox.seal(imageData, publicKey); + saveImageToFile(encryptedImageData); + } + } catch (IOException e) { + Log.w("IOException", e.getMessage(), e); + } + } + + private byte[] bitmapToByteArray(Bitmap bitmap) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos); + return baos.toByteArray(); + } + + private void saveImageToFile(byte[] data) { + File directory = new File(requireContext().getFilesDir(), "cryptology"); + if (!directory.exists()) { + if (!directory.mkdirs()) { + Toast.makeText(requireContext(), "Failed to create directory", + Toast.LENGTH_SHORT).show(); + return; + } + } + + String fileName = "image_" + System.currentTimeMillis() + ".dat"; + File file = new File(directory, fileName); + + FileOutputStream fos = null; + try { + fos = new FileOutputStream(file); + fos.write(data); + Toast.makeText(requireContext(), "Image saved: " + file.getAbsolutePath(), + Toast.LENGTH_SHORT).show(); + Log.d("ImagePath", "Image saved: " + file.getAbsolutePath()); + } catch (IOException e) { + Log.w("IOException", e.getMessage(), e); + } finally { + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + Log.w("IOException", e.getMessage(), e); + } + } + } + } +} diff --git a/android-example/src/main/java/io/xconn/androidexample/fragment/GalleryFragment.java b/android-example/src/main/java/io/xconn/androidexample/fragment/GalleryFragment.java new file mode 100644 index 0000000..1846ed8 --- /dev/null +++ b/android-example/src/main/java/io/xconn/androidexample/fragment/GalleryFragment.java @@ -0,0 +1,266 @@ +package io.xconn.androidexample.fragment; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.GridView; +import android.widget.ImageView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import io.xconn.androidexample.R; +import io.xconn.androidexample.util.App; +import io.xconn.androidexample.util.Helpers; +import io.xconn.cryptology.SealedBox; +import io.xconn.cryptology.SecretBox; + +public class GalleryFragment extends Fragment implements Helpers.PasswordDialogListener { + + private GridView gridView; + private static byte[] privateKey; + + private final ExecutorService executorService = Executors.newFixedThreadPool(4); + private final Handler mainHandler = new Handler(Looper.getMainLooper()); + + @Override + public View onCreateView(LayoutInflater inflater, + ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_gallery, container, false); + gridView = view.findViewById(R.id.gridView); + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + requireActivity().findViewById(R.id.frameLayout).setFocusableInTouchMode(true); + requireActivity().findViewById(R.id.frameLayout).requestFocus(); + Helpers.showPasswordDialog(requireContext(), this, + true); + } + + @Override + public boolean onPasswordSubmit(String password) { + boolean isPrivateKeyDecrypted = decryptPrivateKey(password); + if (isPrivateKeyDecrypted) { + loadImages(); + return true; + } else { + Toast.makeText(requireContext(), "Incorrect password", Toast.LENGTH_SHORT).show(); + return false; + } + } + + @Override + public void onPasswordCancel() { + if (isCameraFragmentFocused()) { + focusOnCameraFragment(); + } + navigateToCameraFragment(); + } + + + @Override + public void onDismissed(boolean dismissedAfterSubmit) { + if (!dismissedAfterSubmit) { + if (isCameraFragmentFocused()) { + focusOnCameraFragment(); + } + navigateToCameraFragment(); + } + } + + + private boolean decryptPrivateKey(String password) { + String encryptedPrivateKeyHex = App.getString(App.PREF_PRIVATE_KEY); + String nonceHex = App.getString(App.PREF_NONCE); + + if (!TextUtils.isEmpty(encryptedPrivateKeyHex) && !TextUtils.isEmpty(nonceHex)) { + byte[] encryptedPrivateKey = Helpers.hexToBytes(encryptedPrivateKeyHex); + byte[] nonce = Helpers.hexToBytes(nonceHex); + + try { + privateKey = SecretBox.boxOpen(nonce, encryptedPrivateKey, + Objects.requireNonNull(Helpers.convertTo32Bytes(password))); + return true; + } catch (Exception e) { + Log.e("IOException", "Error reading or decrypting image data", e); + return false; + } + } else { + Toast.makeText(requireContext(), "Private key or nonce not found", + Toast.LENGTH_SHORT).show(); + return false; + } + } + + private boolean isCameraFragmentFocused() { + if (isAdded()) { + return !requireActivity().findViewById(R.id.menu_camera).isSelected(); + } + return false; + } + + + private void focusOnCameraFragment() { + requireActivity().findViewById(R.id.menu_camera).performClick(); + } + + private void navigateToCameraFragment() { + if (isAdded()) { + FragmentManager fragmentManager = requireActivity().getSupportFragmentManager(); + FragmentTransaction transaction = fragmentManager.beginTransaction(); + transaction.replace(R.id.frameLayout, new CameraFragment()); + transaction.addToBackStack(null); + transaction.commit(); + } + } + + + private void loadImages() { + File directory = new File(requireContext().getFilesDir(), "cryptology"); + if (!directory.exists()) { + if (!directory.mkdirs()) { + Toast.makeText(requireContext(), "Failed to create directory", + Toast.LENGTH_SHORT).show(); + return; + } + } + + List imageFiles = new ArrayList<>(); + File[] files = directory.listFiles(); + + if (files != null) { + for (File file : files) { + if (file.isFile()) { + imageFiles.add(file); + } + } + } + + ImageAdapter adapter = new ImageAdapter( + requireContext(), + imageFiles, + executorService, + mainHandler); + gridView.setAdapter(adapter); + } + + private static class ImageAdapter extends BaseAdapter { + + private final List imageFiles; + private final LayoutInflater inflater; + private final ExecutorService executorService; + private final Handler mainHandler; + + ImageAdapter( + Context context, + List imageFiles, + ExecutorService executorService, + Handler mainHandler) { + this.imageFiles = imageFiles; + this.inflater = LayoutInflater.from(context); + this.executorService = executorService; + this.mainHandler = mainHandler; + } + + @Override + public int getCount() { + return imageFiles.size(); + } + + @Override + public Object getItem(int position) { + return imageFiles.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + ViewHolder holder; + if (convertView == null) { + convertView = inflater.inflate(R.layout.gallery_item, parent, false); + holder = new ViewHolder(); + holder.imageView = convertView.findViewById(R.id.imageView); + convertView.setTag(holder); + } else { + holder = (ViewHolder) convertView.getTag(); + } + + holder.imageView.setImageResource(R.drawable.image_icon); + + File imageFile = imageFiles.get(position); + decodeAndDecryptImageDataAsync(imageFile, holder.imageView); + + return convertView; + } + + private static class ViewHolder { + ImageView imageView; + } + + private void decodeAndDecryptImageDataAsync(final File imageFile, final ImageView imageView) { + executorService.execute(() -> { + Bitmap bitmap = decryptImageData(imageFile); + mainHandler.post(() -> { + if (bitmap != null) { + imageView.setImageBitmap(bitmap); + } else { + imageView.setImageResource(R.drawable.broken_image); + } + }); + }); + } + + private Bitmap decryptImageData(File imageFile) { + try (FileInputStream fis = new FileInputStream(imageFile)) { + byte[] encryptedData = new byte[(int) imageFile.length()]; + int bytesRead = fis.read(encryptedData); + if (bytesRead == -1) { + Log.e("FileInputStream", "No bytes were read from the file"); + return null; + } + + byte[] decryptedData = SealedBox.sealOpen(encryptedData, privateKey); + return BitmapFactory.decodeByteArray(decryptedData, 0, decryptedData.length); + } catch (IOException e) { + Log.e("IOException", "Error reading or decrypting image data", e); + return null; + } + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + executorService.shutdown(); + } +} diff --git a/android-example/src/main/java/io/xconn/androidexample/util/App.java b/android-example/src/main/java/io/xconn/androidexample/util/App.java new file mode 100644 index 0000000..9c3a8ab --- /dev/null +++ b/android-example/src/main/java/io/xconn/androidexample/util/App.java @@ -0,0 +1,52 @@ +package io.xconn.androidexample.util; + +import android.annotation.SuppressLint; +import android.app.Application; +import android.content.Context; +import android.content.SharedPreferences; + +public class App extends Application { + @SuppressLint("StaticFieldLeak") + public static Context context; + + public static final String PREF_PUBLIC_KEY = "public_key"; + public static final String PREF_PRIVATE_KEY = "private_key"; + public static final String PREF_IS_DIALOG_SHOWN = "isDialogShown"; + public static final String PREF_NONCE = "nonce"; + + @Override + public void onCreate() { + super.onCreate(); + + context = getApplicationContext(); + } + + public static Context getContext() { + return context; + } + + public static SharedPreferences getPreferenceManager() { + return getContext().getSharedPreferences("shared_prefs", MODE_PRIVATE); + } + + public static void saveString(String key, String value) { + SharedPreferences sharedPreferences = getPreferenceManager(); + sharedPreferences.edit().putString(key, value).apply(); + } + + public static String getString(String key) { + SharedPreferences sharedPreferences = getPreferenceManager(); + return sharedPreferences.getString(key, ""); + } + + public static void saveBoolean(String key, boolean value) { + SharedPreferences sharedPreferences = getPreferenceManager(); + sharedPreferences.edit().putBoolean(key, value).apply(); + } + + public static boolean getBoolean(String key) { + SharedPreferences sharedPreferences = getPreferenceManager(); + return sharedPreferences.getBoolean(key, false); + } + +} diff --git a/android-example/src/main/java/io/xconn/androidexample/util/Helpers.java b/android-example/src/main/java/io/xconn/androidexample/util/Helpers.java new file mode 100644 index 0000000..adf2ec0 --- /dev/null +++ b/android-example/src/main/java/io/xconn/androidexample/util/Helpers.java @@ -0,0 +1,137 @@ +package io.xconn.androidexample.util; + +import android.app.Application; +import android.content.Context; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.EditText; + +import androidx.appcompat.app.AlertDialog; + +import com.google.android.material.textfield.TextInputLayout; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import io.xconn.androidexample.R; + +public class Helpers extends Application { + + public static byte[] hexToBytes(String hexString) { + int len = hexString.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) + + Character.digit(hexString.charAt(i + 1), 16)); + } + return data; + } + + public static String bytesToHex(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(String.format("%02X", b)); + } + return sb.toString(); + } + + public static byte[] convertTo32Bytes(String input) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(input.getBytes()); + + + byte[] result = new byte[32]; + System.arraycopy(hash, 0, result, 0, Math.min(hash.length, 32)); + + return result; + } catch (NoSuchAlgorithmException e) { + Log.e("IOException", "Error reading or decrypting image data", e); + return null; + } + } + + public interface PasswordDialogListener { + boolean onPasswordSubmit(String password); + + void onPasswordCancel(); + void onDismissed(boolean dismissedAfterSubmit); + } + + public static void showPasswordDialog(Context context, PasswordDialogListener listener, + boolean isCancelButtonVisible) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + LayoutInflater inflater = LayoutInflater.from(context); + View dialogView = inflater.inflate(R.layout.custom_dialog_box, null); + TextInputLayout textInputLayoutPassword = dialogView.findViewById(R.id.enterPassword); + EditText editTextPassword = textInputLayoutPassword.getEditText(); + + builder.setView(dialogView) + .setTitle("Enter Password") + .setPositiveButton("Submit", null) + .setCancelable(false); + + if (isCancelButtonVisible) { + builder.setNegativeButton("Cancel", (dialog, which) -> { + if (listener != null) { + listener.onPasswordCancel(); + } + }); + } + + AlertDialog passwordDialog = builder.create(); + passwordDialog.show(); + + boolean[] dismissedAfterSubmit = {false}; + + passwordDialog.setOnDismissListener(dialog -> { + if (listener != null) { + listener.onDismissed(dismissedAfterSubmit[0]); + } + }); + + if (editTextPassword != null) { + editTextPassword.addTextChangedListener(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) { + passwordDialog.getButton(AlertDialog.BUTTON_POSITIVE) + .setEnabled(!TextUtils.isEmpty(s)); + } + + @Override + public void afterTextChanged(Editable s) { + + } + }); + + passwordDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); + } + + passwordDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> { + String password = editTextPassword != null ? + editTextPassword.getText().toString().trim() : null; + if (password != null && !password.isEmpty()) { + if (listener != null) { + if (listener.onPasswordSubmit(password)) { + dismissedAfterSubmit[0] = true; + passwordDialog.dismiss(); + } else { + // Show error message + textInputLayoutPassword.setError("Incorrect password"); + editTextPassword.requestFocus(); + } + } + } + }); + } +} + diff --git a/android-example/src/main/res/drawable/broken_image.xml b/android-example/src/main/res/drawable/broken_image.xml new file mode 100644 index 0000000..fa7a353 --- /dev/null +++ b/android-example/src/main/res/drawable/broken_image.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android-example/src/main/res/drawable/camera.xml b/android-example/src/main/res/drawable/camera.xml new file mode 100644 index 0000000..e5d01a2 --- /dev/null +++ b/android-example/src/main/res/drawable/camera.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android-example/src/main/res/drawable/edittext_background.xml b/android-example/src/main/res/drawable/edittext_background.xml new file mode 100644 index 0000000..e7c290d --- /dev/null +++ b/android-example/src/main/res/drawable/edittext_background.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/android-example/src/main/res/drawable/gallery.xml b/android-example/src/main/res/drawable/gallery.xml new file mode 100644 index 0000000..d710d27 --- /dev/null +++ b/android-example/src/main/res/drawable/gallery.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android-example/src/main/res/drawable/image_icon.xml b/android-example/src/main/res/drawable/image_icon.xml new file mode 100644 index 0000000..20f23bd --- /dev/null +++ b/android-example/src/main/res/drawable/image_icon.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android-example/src/main/res/layout/activity_main.xml b/android-example/src/main/res/layout/activity_main.xml index 6ff0233..4a49be3 100644 --- a/android-example/src/main/res/layout/activity_main.xml +++ b/android-example/src/main/res/layout/activity_main.xml @@ -1,9 +1,42 @@ - - - + + + + + + + + + + + + + diff --git a/android-example/src/main/res/layout/custom_dialog_box.xml b/android-example/src/main/res/layout/custom_dialog_box.xml new file mode 100644 index 0000000..d73ef81 --- /dev/null +++ b/android-example/src/main/res/layout/custom_dialog_box.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + diff --git a/android-example/src/main/res/layout/fragment_camera.xml b/android-example/src/main/res/layout/fragment_camera.xml new file mode 100644 index 0000000..03885de --- /dev/null +++ b/android-example/src/main/res/layout/fragment_camera.xml @@ -0,0 +1,53 @@ + + + + + + + + + + diff --git a/android-example/src/main/res/layout/fragment_gallery.xml b/android-example/src/main/res/layout/fragment_gallery.xml new file mode 100644 index 0000000..80f518d --- /dev/null +++ b/android-example/src/main/res/layout/fragment_gallery.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/android-example/src/main/res/layout/gallery_item.xml b/android-example/src/main/res/layout/gallery_item.xml new file mode 100644 index 0000000..bb32169 --- /dev/null +++ b/android-example/src/main/res/layout/gallery_item.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/android-example/src/main/res/menu/bottom_navigation_menu.xml b/android-example/src/main/res/menu/bottom_navigation_menu.xml new file mode 100644 index 0000000..5360f21 --- /dev/null +++ b/android-example/src/main/res/menu/bottom_navigation_menu.xml @@ -0,0 +1,12 @@ + + + + diff --git a/android-example/src/main/res/values-night/themes.xml b/android-example/src/main/res/values-night/themes.xml index c00c1b8..791872f 100644 --- a/android-example/src/main/res/values-night/themes.xml +++ b/android-example/src/main/res/values-night/themes.xml @@ -1,10 +1,6 @@ - - - diff --git a/android-example/src/main/res/values/colors.xml b/android-example/src/main/res/values/colors.xml index ca1931b..adad3bf 100644 --- a/android-example/src/main/res/values/colors.xml +++ b/android-example/src/main/res/values/colors.xml @@ -7,4 +7,5 @@ #FF018786 #FF000000 #FFFFFFFF + #039BE5 diff --git a/android-example/src/main/res/values/strings.xml b/android-example/src/main/res/values/strings.xml index 491742a..10f8bef 100644 --- a/android-example/src/main/res/values/strings.xml +++ b/android-example/src/main/res/values/strings.xml @@ -1,3 +1,14 @@ CryptologyExample + + open + close + Select Photo + Capture + Password: + Enter password… + OK + + Enter + Gallary Image diff --git a/android-example/src/main/res/values/themes.xml b/android-example/src/main/res/values/themes.xml index 35c2b82..791872f 100644 --- a/android-example/src/main/res/values/themes.xml +++ b/android-example/src/main/res/values/themes.xml @@ -1,10 +1,6 @@ - - -