Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
e264e43
GA-122 set up the project
YuvalAnteby Jul 25, 2025
60bc444
GA-122 Merge pull request #88 from YuvalAnteby/GA-122-Android-Setup
YuvalAnteby Jul 25, 2025
2ec3c8c
GA-123 created api and model files
YuvalAnteby Jul 25, 2025
2181ee7
GA-123 Merge pull request #89 from YuvalAnteby/GA-123-Retrofit-Networ…
YuvalAnteby Jul 25, 2025
18dc4e5
GA-124 Model classes for retrofit
YuvalAnteby Jul 25, 2025
f5086ed
GA-124 Merge pull request #90 from YuvalAnteby/GA-124-Base-Models
YuvalAnteby Jul 25, 2025
ea51e47
GA-125 util token manager and refactor to api client singleton
YuvalAnteby Jul 31, 2025
ec2ef9f
GA-125 Merge pull request #91 from YuvalAnteby/GA-125-JWT-Tokens
YuvalAnteby Jul 31, 2025
d3b1775
GA-126 classes for mails
YuvalAnteby Jul 31, 2025
d18bb90
GA-126 changes to mail repo class
YuvalAnteby Jul 31, 2025
b848a3e
GA-126 changes to mail view model
YuvalAnteby Jul 31, 2025
9959d20
GA-126 repo and view model for users
YuvalAnteby Jul 31, 2025
2f48f18
GA-126 Merge pull request #92 from YuvalAnteby/GA-126-Repository-patt…
YuvalAnteby Jul 31, 2025
e7e5c50
GA-127 basic UI login fragment
YuvalAnteby Jul 31, 2025
6476aa8
GA-127 basic UI signup fragment
YuvalAnteby Aug 1, 2025
07f89e7
GA-127 Material UI and logic of login and signup
YuvalAnteby Aug 1, 2025
e7cfa74
GA-127 dark mode
YuvalAnteby Aug 1, 2025
05f4ec2
GA-127 error fixes
YuvalAnteby Aug 1, 2025
97a1806
GA-127 Merge pull request #93 from YuvalAnteby
YuvalAnteby Aug 1, 2025
6ff9664
GA-128 inbox screen ui and logic
YuvalAnteby Aug 1, 2025
5fe7273
GA-128 Merge pull request #94 from YuvalAnteby
YuvalAnteby Aug 1, 2025
aacf605
GA-129 basic reading page ui and logic
YuvalAnteby Aug 1, 2025
17f95db
GA-129 star checkbox
YuvalAnteby Aug 1, 2025
c428d84
GA-129 recipients and time
YuvalAnteby Aug 1, 2025
9cb9475
GA-129 cleanup and better formatting
YuvalAnteby Aug 1, 2025
aaceb07
GA-129 Attachment list
YuvalAnteby Aug 1, 2025
7c87945
GA-129 reading page action bar
YuvalAnteby Aug 1, 2025
6fb51e5
GA-129 reading page action bar
YuvalAnteby Aug 1, 2025
9ebff64
GA-129 auto change of the isRead flag
YuvalAnteby Aug 1, 2025
4ec8599
Bugfix
YuvalAnteby Aug 1, 2025
96fd0ea
GA-129 HTML formatting removal in previews, supported in reading page
YuvalAnteby Aug 1, 2025
b7d3f7f
GA-129 Merge pull request #95 from YuvalAnteby
YuvalAnteby Aug 1, 2025
871b92b
GA-132 actions buttons in tool bar and drawer
YuvalAnteby Aug 1, 2025
e6d6c99
GA-132 search bar functionality
YuvalAnteby Aug 1, 2025
1689531
GA-132 star checkbox
YuvalAnteby Aug 1, 2025
af4499a
GA-132 user menu
YuvalAnteby Aug 1, 2025
3cfb0ba
GA-132 DRYing auth screens using the picker util
YuvalAnteby Aug 1, 2025
674f54d
GA-132 Merge pull request #96 from YuvalAnteby
YuvalAnteby Aug 1, 2025
0f53fb5
GA-131 auto login and logout functionalities
YuvalAnteby Aug 1, 2025
ee63dae
GA-131 Merge pull request #97 from YuvalAnteby
YuvalAnteby Aug 1, 2025
a2074ab
GA-139 API related classes (repo, view model, api and models)
YuvalAnteby Aug 2, 2025
bccf235
GA-139 typo in routes
YuvalAnteby Aug 2, 2025
6b9a084
GA-139 load labels to drawer menu
YuvalAnteby Aug 2, 2025
3a883e2
GA-139 show matching inbox when picking a label
YuvalAnteby Aug 2, 2025
12d2883
GA-139 bugfix when editing mail other attributes gets removed
YuvalAnteby Aug 2, 2025
badc00a
GA-139 creating new labels
YuvalAnteby Aug 2, 2025
8bb8dc1
GA-139 sub labels shown below their parent with indentation
YuvalAnteby Aug 2, 2025
0f6cadd
GA-139 set labels for mails from recycler view
YuvalAnteby Aug 2, 2025
1b1fde5
GA-139 set labels for mail from reading activity
YuvalAnteby Aug 2, 2025
74c9c2f
GA-139 Merge pull request #98 from YuvalAnteby
YuvalAnteby Aug 2, 2025
7bc709c
fixed bug where profile image wouldn't load on auto login
YuvalAnteby Aug 2, 2025
04e9a19
Merge pull request #99 from YuvalAnteby/profile-bugfix
YuvalAnteby Aug 2, 2025
4baae7c
GA-130 compose ui
YuvalAnteby Aug 11, 2025
6c9cccf
GA-130 compose logic and backend calls (aside from attachments)
YuvalAnteby Aug 11, 2025
5442c92
GA-130 added attachments support when composing mail and indication f…
YuvalAnteby Aug 11, 2025
04b4084
GA-130 editing a draft
YuvalAnteby Aug 11, 2025
0fc5b4a
GA-130 reply and forward
YuvalAnteby Aug 11, 2025
535e90d
GA-130 removed log
YuvalAnteby Aug 11, 2025
452c231
GA-130 Merge pull request #100 from YuvalAnteby
YuvalAnteby Aug 11, 2025
12edc58
GA-133 Added room caching for mails
YuvalAnteby Aug 16, 2025
170507a
GA-133 bug fixes
YuvalAnteby Aug 16, 2025
87d9f65
GA-133 Merge pull request #101 from YuvalAnteby
YuvalAnteby Aug 16, 2025
39c13eb
fixed bug of not navigating to edit draft
YuvalAnteby Aug 29, 2025
eba6ef3
added a check of JWT expiration when trying to login
YuvalAnteby Aug 29, 2025
6ba6d77
prevented a crash sometimes when using login page
YuvalAnteby Aug 29, 2025
4ad6783
fixed crash when showing snackbar
YuvalAnteby Aug 29, 2025
b0ae26e
fixed bug of stale token
YuvalAnteby Aug 30, 2025
9813427
Merge pull request #102 from YuvalAnteby/inbox-actionbar-bugfix
YuvalAnteby Aug 30, 2025
7d41818
showing nice UI in empty inbox case
YuvalAnteby Aug 31, 2025
0c59e95
improvements to compose fragment UI
YuvalAnteby Aug 31, 2025
25ab82f
fixed bug of showing stale cached data
YuvalAnteby Aug 31, 2025
5bb5ae6
GA-140-Finalizing-the-app
YuvalAnteby Aug 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions android_app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
1 change: 1 addition & 0 deletions android_app/app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
55 changes: 55 additions & 0 deletions android_app/app/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
plugins {
alias(libs.plugins.android.application)
}

android {
namespace 'com.asp.android_app'
compileSdk 35

buildFeatures {
buildConfig true
}

defaultConfig {
applicationId "com.asp.android_app"
minSdk 24
targetSdk 35
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
buildConfigField "String", "API_BASE_URL", "\"${API_BASE_URL}\""
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
}

dependencies {
implementation libs.appcompat
implementation libs.material
implementation libs.constraintlayout
implementation libs.navigation.fragment
implementation libs.navigation.ui
implementation libs.activity
implementation libs.swiperefreshlayout
testImplementation libs.junit
androidTestImplementation libs.ext.junit
androidTestImplementation libs.espresso.core
// Retrofit & Gson
implementation libs.retrofit
implementation libs.gson.converter
// Lifecycle ViewModel + LiveData
implementation libs.lifecycle.viewmodel
implementation libs.lifecycle.livedata
// Room
implementation libs.room.runtime
annotationProcessor libs.room.compiler
}
21 changes: 21 additions & 0 deletions android_app/app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.asp.android_app;

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("com.asp.android_app", appContext.getPackageName());
}
}
64 changes: 64 additions & 0 deletions android_app/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />

<application
android:usesCleartextTraffic="true"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.MyApplication"
tools:targetApi="31">

<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>

<activity
android:name=".ui.inbox_activity.InboxActivity"
android:exported="false" />

<activity
android:name=".ui.ReadingActivity"
android:exported="false" />

<activity
android:name=".ui.auth_activity.AuthActivity"
android:exported="true"
android:theme="@style/Theme.MyApplication">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<activity
android:name=".ui.compose_activity.ComposeMailActivity"
android:windowSoftInputMode="stateVisible|adjustResize"
android:exported="true">
<!-- Optional deep link for editing drafts: myapp://draft/123 -->
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="myapp" android:host="draft"/>
</intent-filter>
</activity>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.asp.android_app.api;

import android.content.Context;
import android.content.Intent;

import com.asp.android_app.BuildConfig;
import com.asp.android_app.ui.auth_activity.AuthActivity;
import com.asp.android_app.utils.TokenManager;

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

/**
* Singleton class responsible for providing a configured Retrofit instance.
* - Uses `http://10.0.2.2:3001/api/` as the base URL (emulator-friendly localhost)
* - Automatically adds a Bearer token to all requests using an OkHttp interceptor
* - Applies `GsonConverterFactory` to handle JSON serialization/deserialization
*/
public class ApiClient {
private static final String BASE_URL = BuildConfig.API_BASE_URL;
private static Retrofit retrofit = null;
private static Context appContext = null;

public static Retrofit getClient(Context context) {
if (appContext == null) {
appContext = context.getApplicationContext();
}

if (retrofit == null) {
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(chain -> {
Request original = chain.request();
Request.Builder requestBuilder = original.newBuilder();
// Get token fresh for each request
TokenManager tokenManager = TokenManager.getInstance(appContext);
String token = tokenManager.getToken();
// Add token Authorization only if token exists, isn't empty and isn't expired
if (token != null && !token.isBlank())
requestBuilder.header("Authorization", "Bearer " + token);

// add the request's body
requestBuilder.method(original.method(), original.body());
Response response = chain.proceed(requestBuilder.build());

// check if the response includes unauthorized
String bodyStr = response.peekBody(Long.MAX_VALUE).string();
if (isTokenUnauthorized(bodyStr)) {
response.close();
tokenManager.clearToken();
tokenManager.clearUser();
moveToLogin(appContext);
}
return response;
}).build();

retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build();
}
return retrofit;
}

/**
* @param bodyStr response body as a string
* @return true if includes an error message from the backend of invalid/expired/missing token,
* otherwise false
*/
private static boolean isTokenUnauthorized(String bodyStr) {
return bodyStr.contains("\"error\":\"Invalid or expired token\"") ||
bodyStr.contains("\"error\":\"Authorization header missing\"");
}

/**
* Navigates the user to the login page (in case of unAuthorised request to server)
*
* @param context app context
*/
private static void moveToLogin(Context context) {
Intent i = new Intent(context, AuthActivity.class);
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
context.startActivity(i);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.asp.android_app.api;

import com.asp.android_app.model.Label;
import com.asp.android_app.model.request.LabelRequest;

import java.util.List;

import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.DELETE;
import retrofit2.http.GET;
import retrofit2.http.PATCH;
import retrofit2.http.POST;
import retrofit2.http.Path;

/**
* Retrofit interface for managing label-related operations.
* Connects to endpoints defined in the NodeJS server.
* All calls require authentication via Authorization header.
*/
public interface LabelApi {

/**
* Fetch all labels belonging to the authenticated user.
* @return A list of LabelResponse objects.
*/
@GET("labels/")
Call<List<Label>> getAllLabels();

/**
* Create a new root-level label.
* @param labelRequest The request body containing the label name.
* @return The newly created LabelResponse.
*/
@POST("labels/")
Call<Label> createLabel(@Body LabelRequest labelRequest);

/**
* Create a new sub-label under a parent label.
* @param parentId The ID of the parent label.
* @param labelRequest The request body containing the sub-label name.
* @return The newly created sub-label.
*/
@POST("labels/{id}/sublabel")
Call<Label> createSublabel(@Path("id") int parentId, @Body LabelRequest labelRequest);

/**
* Edit the name of an existing label.
* @param labelId The ID of the label to edit.
* @param labelRequest The new name.
* @return An empty response with status 204 on success.
*/
@PATCH("labels/{id}")
Call<Void> editLabel(@Path("id") int labelId, @Body LabelRequest labelRequest);

/**
* Delete a label by ID. May also delete sub-labels depending on server logic.
* @param labelId The ID of the label to delete.
* @return An empty response with status 204 on success.
*/
@DELETE("labels/{id}")
Call<Void> deleteLabel(@Path("id") int labelId);
}
Loading