Skip to content

Commit

Permalink
Add android example app (#32)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
muteeburrehman and muzzammilshahid authored May 29, 2024
1 parent 4627e46 commit 2b4928d
Show file tree
Hide file tree
Showing 22 changed files with 975 additions and 26 deletions.
13 changes: 12 additions & 1 deletion android-example/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
21 changes: 17 additions & 4 deletions android-example/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

<uses-feature
android:name="android.hardware.camera"
android:required="false" />

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

<application
android:allowBackup="true"
android:name=".util.App"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Cryptology">
android:theme="@style/Theme.Cryptology"
tools:targetApi="31">

<activity
android:name=".MainActivity"
android:exported="true" >
android:exported="true">

<intent-filter>
<action android:name="android.intent.action.MAIN" />
Expand Down
Original file line number Diff line number Diff line change
@@ -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) {

}
}
Original file line number Diff line number Diff line change
@@ -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<String> cameraPermissionLauncher;
private ActivityResultLauncher<Intent> cameraLauncher;
private ActivityResultLauncher<Intent> 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);
}
}
}
}
}
Loading

0 comments on commit 2b4928d

Please sign in to comment.