to kotlit

This commit is contained in:
Joshua Perry 2024-06-12 14:05:19 +01:00
parent 0b29993073
commit 8051c2c4ed
63 changed files with 670 additions and 511 deletions

7
.gitignore vendored
View File

@ -1,7 +1,12 @@
*.iml *.iml
.gradle .gradle
/local.properties /local.properties
/.idea /.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store .DS_Store
/build /build
/captures /captures

View File

@ -1 +1 @@
dermy dermy_app

View File

@ -0,0 +1,123 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>

View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

View File

@ -4,14 +4,6 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2024-06-04T18:01:16.272682016Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=/home/r0r5chach/.config/.android/avd/Pixel_8_Pro_API_30.avd" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState> </SelectionState>
</selectionStates> </selectionStates>
</component> </component>

View File

@ -1,11 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings"> <component name="GradleSettings">
<option name="linkedExternalProjectsSettings"> <option name="linkedExternalProjectsSettings">
<GradleProjectSettings> <GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleHome" value="/usr/share/java/gradle" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" /> <option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules"> <option name="modules">
<set> <set>

6
.idea/kotlinc.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.9.0" />
</component>
</project>

View File

@ -1,14 +1,15 @@
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.jetbrains.kotlin.android)
} }
android { android {
namespace = "xyz.r0r5chach.dermy" namespace = "xyz.r0r5chach.dermy_app"
compileSdk = 34 compileSdk = 34
defaultConfig { defaultConfig {
applicationId = "xyz.r0r5chach.dermy" applicationId = "xyz.r0r5chach.dermy_app"
minSdk = 22 minSdk = 24
targetSdk = 34 targetSdk = 34
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "1.0"
@ -23,27 +24,33 @@ android {
} }
} }
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_16
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_16
}
kotlinOptions {
jvmTarget = "1.8"
} }
} }
dependencies { dependencies {
implementation(libs.appcompat) implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material) implementation(libs.material)
implementation(libs.androidx.preference)
implementation(libs.androidx.room.common)
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.ext.junit) androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.espresso.core) androidTestImplementation(libs.androidx.espresso.core)
// CameraX core library //FragmentX
implementation(libs.camera.core) implementation(libs.androidx.fragment.ktx)
implementation(libs.camera.camera2)
implementation(libs.camera.lifecycle)
implementation(libs.camera.view)
implementation(libs.camera.extensions)
//TFLite //CameraX
implementation(libs.tensorflow.lite) implementation(libs.androidx.camera.view)
implementation(libs.androidx.camera.core)
implementation(libs.androidx.camera.lifecycle)
//MongoDB
implementation(libs.mongodb.driver.kotlin.sync)
} }

View File

@ -1,26 +0,0 @@
package xyz.r0r5chach.dermy;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("xyz.r0r5chach.dermy", appContext.getPackageName());
}
}

View File

@ -0,0 +1,24 @@
package xyz.r0r5chach.dermy_app
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("xyz.r0r5chach.dermy_app", appContext.packageName)
}
}

View File

@ -2,9 +2,6 @@
<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"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!--TODO: Update to more modern permissions-->
<uses-feature android:name="android.hardware.camera" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
@ -13,17 +10,14 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.Dermy" android:theme="@style/Theme.Dermy_app"
tools:targetApi="31" > tools:targetApi="31">
<activity android:name=".MainActivity" <activity android:name=".MainActivity"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
</activity> </activity>
</application> </application>
</manifest> </manifest>

View File

@ -1,47 +0,0 @@
package xyz.r0r5chach.dermy;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import xyz.r0r5chach.dermy.fragments.CameraFragment;
public class MainActivity extends AppCompatActivity {
public static final int REQUEST_CAMERA_PERMISSION = 200;
public MainActivity() {
super(R.layout.activity_main);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState == null) {
startCamera();
}
}
private void startCamera() {
getSupportFragmentManager().beginTransaction()
.setReorderingAllowed(true)
.add(R.id.fragment_container, CameraFragment.class, null)
.commit();
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { //FIXME: deprecated listener fir request permissions
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CAMERA_PERMISSION) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
startCamera();
} else {
Toast.makeText(this, "Camera permission is required", Toast.LENGTH_SHORT).show();
}
}
}
}

View File

@ -1,21 +0,0 @@
package xyz.r0r5chach.dermy.entities;
import java.time.LocalDateTime;
import xyz.r0r5chach.dermy.entities.locations.Location;
import xyz.r0r5chach.dermy.entities.logs.LogBook;
public class Mole {
private Location location;
private LocalDateTime dateCreated;
private LogBook logEntries;
public Location getLocation() {
return location;
}
public LogBook getLogBook() {
return logEntries;
}
}

View File

@ -1,4 +0,0 @@
package xyz.r0r5chach.dermy.entities.locations;
public interface Location {
}

View File

@ -1,22 +0,0 @@
package xyz.r0r5chach.dermy.entities.locations;
public enum RightSideLocation implements Location {
Head,
Ear,
Cheek,
Neck,
Shoulder,
UpperArm,
Elbow,
LowerArm,
Wrist,
Hand,
UpperTorso,
LowerTorso,
Hip,
UpperLeg,
Knee,
LowerLeg,
Ankle,
Foot
}

View File

@ -1,43 +0,0 @@
package xyz.r0r5chach.dermy.entities.logs;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
public class LogBook {
private final List<LogEntry> logEntries;
public LogBook() {
logEntries = new ArrayList<>();
}
public void addEntry(LogEntry entry) {
logEntries.add(entry);
} //TODO: Add to db
//TODO: Remove Entry
//TODO: Update Entry
private LogBook getEntriesBy(Object obj) {
LogBook results = new LogBook();
for (LogEntry entry: logEntries) {
if (entry.getDateCreated().equals(obj) || entry.getNotes().contains((CharSequence) obj)) {
results.addEntry(entry);
}
else {
return null;
}
}
return results;
}
public LogBook getEntriesByDate(LocalDateTime dateCreated) {
return getEntriesBy(dateCreated);
}
public LogBook getEntriesByNote(String noteFragment) {
return getEntriesBy(noteFragment);
}
}

View File

@ -1,16 +0,0 @@
package xyz.r0r5chach.dermy.entities.logs;
import java.time.LocalDateTime;
public class LogEntry {
private LocalDateTime dateCreated;
private String imagePath, notes;
public LocalDateTime getDateCreated() {
return dateCreated;
}
public String getNotes() {
return notes;
}
}

View File

@ -1,120 +0,0 @@
package xyz.r0r5chach.dermy.fragments;
import static xyz.r0r5chach.dermy.MainActivity.REQUEST_CAMERA_PERMISSION;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureException;
import androidx.camera.core.Preview;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.camera.view.PreviewView;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import com.google.common.util.concurrent.ListenableFuture;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import xyz.r0r5chach.dermy.R;
import xyz.r0r5chach.dermy.models.Model;
import xyz.r0r5chach.dermy.models.binary_classifiers.BinaryMobileNetV2;
public class CameraFragment extends Fragment {
private PreviewView previewView;
private ImageCapture imageCapture;
private ExecutorService cameraExecutor;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_camera, container, false);
previewView = view.findViewById(R.id.camera_preview);
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
startCamera();
} else {
ActivityCompat.requestPermissions(requireActivity(), new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA_PERMISSION);
}
previewView.setOnClickListener(v -> takePhoto());
cameraExecutor = Executors.newSingleThreadExecutor();
return view;
}
private void startCamera() {
ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext());
cameraProviderFuture.addListener(() -> {
try {
ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
bindPreview(cameraProvider);
} catch (Exception e) {
e.printStackTrace(); //TODO: Replace with better logging
}
}, ContextCompat.getMainExecutor(requireContext()));
}
private void bindPreview(@NonNull ProcessCameraProvider cameraProvider) {
Preview preview = new Preview.Builder().build();
preview.setSurfaceProvider(previewView.getSurfaceProvider());
imageCapture = new ImageCapture.Builder().build();
CameraSelector cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA;
cameraProvider.unbindAll();
cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture);
}
private void takePhoto() {
File photoFile = new File(requireContext().getExternalFilesDir(null), System.currentTimeMillis() + ".jpg");
ImageCapture.OutputFileOptions outputFileOptions = new ImageCapture.OutputFileOptions.Builder(photoFile).build();
imageCapture.takePicture(outputFileOptions, ContextCompat.getMainExecutor(requireContext()), new ImageCapture.OnImageSavedCallback() {
@Override
public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {
Bitmap photo = BitmapFactory.decodeFile(photoFile.getAbsolutePath());
try {
//TODO: Change Model based on preference
Model model = new BinaryMobileNetV2(getResources());
float[] results = model.runInference(photo, 2);
Toast.makeText(requireContext(), "Results = " + Arrays.toString(results), Toast.LENGTH_LONG).show();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void onError(@NonNull ImageCaptureException exception) {
exception.printStackTrace(); //TODO: Replace with better logging
Toast.makeText(requireContext(), "Error saving photo: " + exception.getMessage(), Toast.LENGTH_SHORT).show();
}
});
}
@Override
public void onDestroy() {
super.onDestroy();
cameraExecutor.shutdown();
}
}

View File

@ -1,4 +0,0 @@
package xyz.r0r5chach.dermy.fragments;
public class MapFragment {
}

View File

@ -1,4 +0,0 @@
package xyz.r0r5chach.dermy.fragments;
public class NearbyFragment {
}

View File

@ -1,9 +0,0 @@
package xyz.r0r5chach.dermy.fragments;
public class PreferencesFragment {
//TODO: Disclaimers
//TODO: Useful Links
//TODO: Export File
//TODO: Backup data
//TODO: Recover Account
}

View File

@ -1,67 +0,0 @@
package xyz.r0r5chach.dermy.models;
import android.content.res.AssetFileDescriptor;
import android.content.res.Resources;
import android.graphics.Bitmap;
import org.tensorflow.lite.Interpreter;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import xyz.r0r5chach.dermy.R;
public class Model {
protected final Interpreter modelInterpreter;
public Model(Resources resources, int modelId) throws IOException {
modelInterpreter = new Interpreter(loadModel(resources, modelId));
}
public float[] runInference(Bitmap image, int classes) {
ByteBuffer inputBuffer = preprocessImage(image);
float[][] output = new float[1][classes];
modelInterpreter.run(inputBuffer, output);
return output[0];
}
public MappedByteBuffer loadModel(Resources resources, int modelId) throws IOException {
AssetFileDescriptor fileDescriptor = resources.openRawResourceFd(modelId);
FileInputStream inputStream = new FileInputStream(fileDescriptor.getFileDescriptor()); //FIXME: add try-with-resources
FileChannel fileChannel = inputStream.getChannel();
long startOffset = fileDescriptor.getStartOffset();
long declaredLength = fileDescriptor.getDeclaredLength();
return fileChannel.map(MapMode.READ_ONLY, startOffset, declaredLength);
}
public static ByteBuffer preprocessImage(Bitmap image) {
ByteBuffer buffer = ByteBuffer.allocateDirect(4 * 280 * 280 * 3);
buffer.order(ByteOrder.nativeOrder());
int[] intValues = new int[280 * 280];
image.getPixels(intValues, 0, image.getWidth(), 0, 0, image.getWidth(), image.getHeight());
int pixel = 0;
for (int i = 0; i < 280; ++i) {
for (int j = 0; j < 280; ++j) {
int val = intValues[pixel++];
buffer.putFloat((((val >> 16) & 0xFF) - 127) / 128.0f);
buffer.putFloat((((val >> 8) & 0xFF) - 127) / 128.0f);
buffer.putFloat(((val & 0xFF) - 127) / 128.0f);
}
}
return buffer;
}
}

View File

@ -1,16 +0,0 @@
package xyz.r0r5chach.dermy.models.binary_classifiers;
import android.content.res.AssetManager;
import android.content.res.Resources;
import java.io.IOException;
import xyz.r0r5chach.dermy.R;
import xyz.r0r5chach.dermy.models.Model;
public class BinaryEfficientNetLite3 extends Model {
public BinaryEfficientNetLite3(Resources resources) throws IOException {
super(resources, R.raw.binary_efficientnet_lite3);
}
}

View File

@ -1,16 +0,0 @@
package xyz.r0r5chach.dermy.models.binary_classifiers;
import android.content.res.AssetManager;
import android.content.res.Resources;
import java.io.IOException;
import xyz.r0r5chach.dermy.R;
import xyz.r0r5chach.dermy.models.Model;
public class BinaryEfficientNetLite4 extends Model {
public BinaryEfficientNetLite4(Resources resources) throws IOException {
super(resources, R.raw.binary_efficientnet_lite4);
}
}

View File

@ -1,18 +0,0 @@
package xyz.r0r5chach.dermy.models.binary_classifiers;
import android.content.res.AssetManager;
import android.content.res.Resources;
import java.io.IOException;
import xyz.r0r5chach.dermy.R;
import xyz.r0r5chach.dermy.models.Model;
public class BinaryMobileNetV2 extends Model {
public BinaryMobileNetV2(Resources resources) throws IOException {
super(resources, R.raw.binary_mobilenet_v2);
}
//TODO: Add runInference method that runs super but transforms output into usable format
}

View File

@ -0,0 +1,19 @@
package xyz.r0r5chach.dermy_app
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.add
import androidx.fragment.app.commit
import xyz.r0r5chach.dermy_app.fragments.HomeFragment
class MainActivity: AppCompatActivity(R.layout.activity_main) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
supportFragmentManager.commit {
setReorderingAllowed(true)
add<HomeFragment>(R.id.fragment_container, null)
}
}
}
}

View File

@ -0,0 +1,24 @@
package xyz.r0r5chach.dermy_app.db
import android.util.Log
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import java.time.LocalDateTime
interface LogDao {
@Query("SELECT * FROM logs")
fun getAll(): List<LogEntry>
@Query("SELECT * FROM logs WHERE moleId = :moleId")
fun findMoleLogs(moleId: String?): List<LogEntry>
@Query("SELECT * FROM logs WHERE dateCreated = :date")
fun findByDate(date: LocalDateTime): List<LogEntry>
@Insert
fun insertOne(log: Log?)
@Delete
fun deleteOne(log: Log?)
}

View File

@ -0,0 +1,26 @@
package xyz.r0r5chach.dermy_app.db
import androidx.room.Entity
import androidx.room.PrimaryKey
import org.bson.BsonType
import org.bson.codecs.pojo.annotations.BsonId
import org.bson.codecs.pojo.annotations.BsonProperty
import org.bson.codecs.pojo.annotations.BsonRepresentation
import java.time.LocalDate
import java.time.LocalDateTime
@Entity(tableName = "logs")
data class LogEntry(
@PrimaryKey
@BsonId
@BsonRepresentation(BsonType.OBJECT_ID)
val id: String,
@BsonRepresentation(BsonType.OBJECT_ID)
@BsonProperty("_mole_id")
val moleId: String,
@BsonRepresentation(BsonType.DATE_TIME)
@BsonProperty("_date_created")
val dateCreated: LocalDateTime,
val contents: String
)

View File

@ -0,0 +1,22 @@
package xyz.r0r5chach.dermy_app.db
import androidx.room.Entity
import androidx.room.PrimaryKey
import org.bson.BsonType
import org.bson.codecs.pojo.annotations.BsonId
import org.bson.codecs.pojo.annotations.BsonProperty
import org.bson.codecs.pojo.annotations.BsonRepresentation
import xyz.r0r5chach.dermy_app.db.locations.Location
@Entity(tableName = "moles")
data class Mole(
@PrimaryKey
@BsonId
@BsonRepresentation(BsonType.OBJECT_ID)
val id: String,
@BsonRepresentation(BsonType.OBJECT_ID)
@BsonProperty("_user_id")
val userId: String,
@BsonRepresentation(BsonType.STRING)
val location: Location
)

View File

@ -0,0 +1,20 @@
package xyz.r0r5chach.dermy_app.db
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import xyz.r0r5chach.dermy_app.db.locations.Location
interface MoleDao {
@Query("SELECT * FROM moles")
fun getAll(): List<Mole>
@Query("SELECT * FROM moles WHERE location = :location")
fun findByLocation(location: Location): List<Mole>
@Insert
fun insertOne(mole: Mole)
@Delete
fun deleteOne(mole: Mole)
}

View File

@ -1,6 +1,6 @@
package xyz.r0r5chach.dermy.entities.locations; package xyz.r0r5chach.dermy_app.db.locations
public enum BackLocation implements Location { enum class BackLocation : Location {
Head, Head,
Neck, Neck,
LeftShoulder, LeftShoulder,

View File

@ -1,6 +1,6 @@
package xyz.r0r5chach.dermy.entities.locations; package xyz.r0r5chach.dermy_app.db.locations
public enum FrontLocation implements Location { enum class FrontLocation : Location {
Forehead, Forehead,
Nose, Nose,
LeftEye, LeftEye,

View File

@ -0,0 +1,3 @@
package xyz.r0r5chach.dermy_app.db.locations
interface Location

View File

@ -1,6 +1,6 @@
package xyz.r0r5chach.dermy.entities.locations; package xyz.r0r5chach.dermy_app.db.locations
public enum LeftSideLocation implements Location { enum class SideLocation : Location {
Head, Head,
Ear, Ear,
Cheek, Cheek,

View File

@ -0,0 +1,5 @@
package xyz.r0r5chach.dermy_app.fragments;
public class AccountFragment {
//TODO: Login if no API Key
}

View File

@ -0,0 +1,109 @@
package xyz.r0r5chach.dermy_app.fragments
import android.Manifest
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Bundle
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCapture.OutputFileOptions
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import com.google.common.util.concurrent.ListenableFuture
import xyz.r0r5chach.dermy_app.R
import java.io.File
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
class CameraFragment : Fragment(R.layout.fragment_camera) {
private lateinit var previewView: PreviewView
private lateinit var imageCapture: ImageCapture
private lateinit var cameraExecutor: ExecutorService
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) {isGranted: Boolean ->
if (isGranted) {
startCamera()
}
else {
Toast.makeText(context, "Camera permission is required", Toast.LENGTH_SHORT).show()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
previewView = view?.findViewById(R.id.camera_preview)!!
if (context?.checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
requestPermissionLauncher.launch(Manifest.permission.CAMERA)
}
else {
startCamera()
}
previewView.setOnClickListener { takePhoto() }
cameraExecutor = Executors.newSingleThreadExecutor()
}
private fun startCamera() {
val cameraProviderFuture: ListenableFuture<ProcessCameraProvider> = ProcessCameraProvider.getInstance(requireContext())
cameraProviderFuture.addListener({
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
val preview = Preview.Builder().build().also {
it.setSurfaceProvider(previewView.surfaceProvider)
}
imageCapture = ImageCapture.Builder().build()
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture)
} catch (exc: Exception) {
exc.printStackTrace()
}
}, ContextCompat.getMainExecutor(requireContext()))
}
private fun takePhoto() {
val photoFile = File(context?.getExternalFilesDir(null), "${System.currentTimeMillis()}.jpg") //TODO: Save image as MoleId
val outputFileOptions: OutputFileOptions = OutputFileOptions.Builder(photoFile).build()
imageCapture.takePicture(
outputFileOptions,
ContextCompat.getMainExecutor(requireContext()),
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
val photo: Bitmap = BitmapFactory.decodeFile(photoFile.absolutePath)
//TODO: redirect to next fragment
}
override fun onError(exception: ImageCaptureException) {
exception.printStackTrace() //TODO: Replace with better logging
Toast.makeText(
requireContext(),
"Error saving photo: ${exception.message}",
Toast.LENGTH_SHORT
).show()
}
})
}
override fun onDestroy() {
super.onDestroy()
cameraExecutor.shutdown()
}
}

View File

@ -0,0 +1,53 @@
package xyz.r0r5chach.dermy_app.fragments
import android.os.Bundle
import android.view.View
import android.widget.Button
import androidx.fragment.app.Fragment
import androidx.fragment.app.commit
import androidx.fragment.app.replace
import xyz.r0r5chach.dermy_app.R
class HomeFragment : Fragment(R.layout.fragment_home) {
private var buttons: IntArray = intArrayOf(R.id.add_mole_button, R.id.map_button, R.id.links_button, R.id.settings_button)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
for (button in buttons) {
view.findViewById<Button>(button).setOnClickListener {
initButton(button)
}
}
}
private fun initButton(button: Int) {
val manager = requireActivity().supportFragmentManager
when (button) {
R.id.add_mole_button -> {
manager.commit {
setReorderingAllowed(true)
replace<MoleFragment>(R.id.fragment_container, null)
}
}
R.id.map_button -> {
manager.commit {
setReorderingAllowed(true)
replace<MapFragment>(R.id.fragment_container, null)
}
}
R.id.links_button -> {
manager.commit {
setReorderingAllowed(true)
replace<LinksFragment>(R.id.fragment_container, null)
}
}
R.id.settings_button -> {
manager.commit {
setReorderingAllowed(true)
replace<SettingsFragment>(R.id.fragment_container, null)
}
}
}
}
}

View File

@ -0,0 +1,7 @@
package xyz.r0r5chach.dermy_app.fragments;
import androidx.fragment.app.Fragment;
public class LinksFragment extends Fragment {
//TODO: NHS links etc
}

View File

@ -0,0 +1,5 @@
package xyz.r0r5chach.dermy_app.fragments;
public class LocationFragment {
//TODO: Recycler View of Moles at location from last fragment
}

View File

@ -0,0 +1,8 @@
package xyz.r0r5chach.dermy_app.fragments;
import androidx.fragment.app.Fragment;
public class MapFragment extends Fragment {
//TODO: Map of all locations
//When pressed route to LocationFragment with specified location
}

View File

@ -0,0 +1,6 @@
package xyz.r0r5chach.dermy_app.fragments;
import androidx.fragment.app.Fragment;
public class MoleFragment extends Fragment {
}

View File

@ -0,0 +1,17 @@
package xyz.r0r5chach.dermy_app.fragments
import android.os.Bundle
import androidx.preference.PreferenceFragmentCompat
import xyz.r0r5chach.dermy_app.R
class SettingsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.preferences, rootKey)
} //TODO: Disclaimers
//TODO: Useful Links
//TODO: Export File
//TODO: Backup data
//TODO: Recover Account
// Login button
}

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,66 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/add_mole_button"
android:layout_width="0dp"
android:layout_height="0dp"
android:text="@string/string_add_mole"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@+id/links_button"
app:layout_constraintEnd_toStartOf="@+id/map_button"
app:layout_constraintHeight_percent="0.1"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_max="320dp"
app:layout_constraintWidth_percent="0.28" />
<Button
android:id="@+id/map_button"
android:layout_width="0dp"
android:layout_height="0dp"
android:text="@string/string_body_map"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@+id/settings_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.1"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/add_mole_button"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_max="320dp"
app:layout_constraintWidth_percent="0.28" />
<Button
android:id="@+id/links_button"
android:layout_width="0dp"
android:layout_height="0dp"
android:text="@string/string_links"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/settings_button"
app:layout_constraintHeight_percent="0.1"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/add_mole_button"
app:layout_constraintWidth_max="320dp"
app:layout_constraintWidth_percent="0.28" />
<Button
android:id="@+id/settings_button"
android:layout_width="0dp"
android:layout_height="0dp"
android:text="@string/string_settings"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.1"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/links_button"
app:layout_constraintTop_toBottomOf="@+id/map_button"
app:layout_constraintWidth_max="320dp"
app:layout_constraintWidth_percent="0.28" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,6 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="Theme.Dermy" parent="Theme.MaterialComponents.DayNight.DarkActionBar"> <style name="Theme.Dermy_app" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. --> <!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item> <item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item> <item name="colorPrimaryVariant">@color/purple_700</item>

View File

@ -1,4 +1,5 @@
<resources> <resources>
<string name="app_name" translatable="false">dermy</string> <string name="app_name">dermy_app</string>
<string name="image_capture_button_text">Capture</string> <!-- TODO: Remove or change this placeholder text -->
<string name="hello_blank_fragment">Hello blank fragment</string>
</resources> </resources>

View File

@ -1,6 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="Theme.Dermy" parent="Theme.MaterialComponents.DayNight.DarkActionBar"> <style name="Theme.Dermy_app" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. --> <!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item> <item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item> <item name="colorPrimaryVariant">@color/purple_700</item>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
</androidx.preference.PreferenceScreen>

View File

@ -1,17 +0,0 @@
package xyz.r0r5chach.dermy;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}

View File

@ -0,0 +1,17 @@
package xyz.r0r5chach.dermy_app
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

View File

@ -1,4 +1,5 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins { plugins {
alias(libs.plugins.android.application) apply false alias(libs.plugins.android.application) apply false
alias(libs.plugins.jetbrains.kotlin.android) apply false
} }

View File

@ -15,6 +15,8 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# Android operating system, and which are packaged with your app's APK # Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn # https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the # Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies, # resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library # thereby reducing the size of the R class for that library

View File

@ -1,26 +1,36 @@
[versions] [versions]
agp = "8.4.1" agp = "8.4.1"
cameraCore = "1.3.3" fragmentKtx = "1.7.1"
kotlin = "1.9.0"
coreKtx = "1.13.1"
junit = "4.13.2" junit = "4.13.2"
junitVersion = "1.1.5" junitVersion = "1.1.5"
espressoCore = "3.5.1" espressoCore = "3.5.1"
appcompat = "1.7.0" appcompat = "1.7.0"
material = "1.12.0" material = "1.12.0"
tensorflowLite = "2.6.0" cameraView = "1.3.3"
cameraCore = "1.3.3"
cameraLifecycle = "1.3.3"
mongodbDriverKotlinSync = "5.1.1"
preference = "1.2.1"
roomCommon = "2.6.1"
[libraries] [libraries]
camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "cameraCore" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
camera-core = { module = "androidx.camera:camera-core", version.ref = "cameraCore" } androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragmentKtx" }
camera-extensions = { module = "androidx.camera:camera-extensions", version.ref = "cameraCore" }
camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "cameraCore" }
camera-view = { module = "androidx.camera:camera-view", version.ref = "cameraCore" }
junit = { group = "junit", name = "junit", version.ref = "junit" } junit = { group = "junit", name = "junit", version.ref = "junit" }
ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" } material = { group = "com.google.android.material", name = "material", version.ref = "material" }
tensorflow-lite = { module = "org.tensorflow:tensorflow-lite", version.ref = "tensorflowLite" } androidx-camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "cameraView" }
androidx-camera-core = { group = "androidx.camera", name = "camera-core", version.ref = "cameraCore" }
androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "cameraLifecycle" }
androidx-preference = { group = "androidx.preference", name = "preference", version.ref = "preference" }
androidx-room-common = { group = "androidx.room", name = "room-common", version.ref = "roomCommon" }
mongodb-driver-kotlin-sync = { module = "org.mongodb:mongodb-driver-kotlin-sync", version.ref = "mongodbDriverKotlinSync" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }

View File

@ -1,4 +1,4 @@
#Mon Jun 03 19:20:03 BST 2024 #Wed Jun 12 12:41:18 BST 2024
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip

View File

@ -19,6 +19,6 @@ dependencyResolutionManagement {
} }
} }
rootProject.name = "dermy" rootProject.name = "dermy_app"
include(":app") include(":app")