diff --git a/README.md b/README.md index e2d67555..da3c84a4 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ Daily meeting summaries are uploaded to `Issues` tab Roee's miluim service documents are uploaded to `Issues` tab if needed, Tzvika was informed about it. +## Dear TA please check main-Ex5 for the final version of Ex5 ## Dear TA please check main-Ex4 for the final version of Ex4 --- @@ -12,6 +13,7 @@ Roee's miluim service documents are uploaded to `Issues` tab if needed, Tzvika w - [Testing bloom filter server and python client](#to-test-the-python-client-and-bloom-filter-server) - [Running as web application project](#running-the-entire-web-app) - [env variables](#env-variables) + - [Running the android app](#running-the-android-app) - [Screenshots](#screenshots) - [Useful links](#useful-links) - [CPP server README](https://github.com/YuvalAnteby/Gmail-AdvancedSystemProgramming/tree/main-Exe/server_cpp) @@ -39,10 +41,10 @@ This will build and run only the test related containers (CPP server, gtest, pyt ### Running the entire web app This will build and run only the web application related containers (React, Node.js, CPP server) ```bash - docker-compose --profile web_app up --build + docker compose --profile web_app up -d --build web_server mongo ``` -### env variables +#### env variables In Node.js and React root folders you can find .env files with default values to help you check the project.
In a real world application these wouldn't be uploaded, we did it for easier set up for the checkers :) @@ -57,21 +59,37 @@ In a real world application these wouldn't be uploaded, we did it for easier set control+c ``` +### Running the android app +0. ensure the backend is running using docker using the previous instructions +1. Download and install the official android studio software +2. Set up an emulator device using an API version of 26 or greater (most modern phones will suffice) +3. You can install the project's app using 2 versions: + - Easy way: + 1. get from the root folder the given APK file + 2. ensure the emulator's device is up and running + 3. drag the file to the emulator's window, it will be installed in it + - The Custom way: + 1. Open android studio when targeting the folder `android_app` instead of the root folder + 2. Wait for android studio to finish the intial gradle build of the project + 3. click on the green run button using the built in tools and set up to your desire + +- in case you need to change the backend's API URL you can do so in `gradle.properties` located in the sub root folder `android_app` --- ### Screenshots
-Click to expand Ex4 screenshots +Click to expand Ex5 screenshots -light_login -dark_inbox -light_compose -dark_reading -dark_signup + + + +inbox_dark +inbox_light +reading_dark
-For more screenshots [click here](https://github.com/YuvalAnteby/Gmail-AdvancedSystemProgramming/tree/main-Ex4/screenshots/ex4) +For more screenshots [click here](https://github.com/YuvalAnteby/Gmail-AdvancedSystemProgramming/tree/main-Ex5/screenshots/ex5) --- diff --git a/android_app/.gitignore b/android_app/.gitignore new file mode 100644 index 00000000..aa724b77 --- /dev/null +++ b/android_app/.gitignore @@ -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 diff --git a/android_app/app/.gitignore b/android_app/app/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/android_app/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/android_app/app/build.gradle b/android_app/app/build.gradle new file mode 100644 index 00000000..23c8c8ab --- /dev/null +++ b/android_app/app/build.gradle @@ -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 26 + 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 +} \ No newline at end of file diff --git a/android_app/app/proguard-rules.pro b/android_app/app/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/android_app/app/proguard-rules.pro @@ -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 \ No newline at end of file diff --git a/android_app/app/src/androidTest/java/com/asp/android_app/ExampleInstrumentedTest.java b/android_app/app/src/androidTest/java/com/asp/android_app/ExampleInstrumentedTest.java new file mode 100644 index 00000000..470e37b6 --- /dev/null +++ b/android_app/app/src/androidTest/java/com/asp/android_app/ExampleInstrumentedTest.java @@ -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 Testing documentation + */ +@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()); + } +} \ No newline at end of file diff --git a/android_app/app/src/main/AndroidManifest.xml b/android_app/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..bf60f27b --- /dev/null +++ b/android_app/app/src/main/AndroidManifest.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android_app/app/src/main/java/com/asp/android_app/api/ApiClient.java b/android_app/app/src/main/java/com/asp/android_app/api/ApiClient.java new file mode 100644 index 00000000..4233a408 --- /dev/null +++ b/android_app/app/src/main/java/com/asp/android_app/api/ApiClient.java @@ -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); + } +} \ No newline at end of file diff --git a/android_app/app/src/main/java/com/asp/android_app/api/LabelApi.java b/android_app/app/src/main/java/com/asp/android_app/api/LabelApi.java new file mode 100644 index 00000000..1f5fbb8c --- /dev/null +++ b/android_app/app/src/main/java/com/asp/android_app/api/LabelApi.java @@ -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> getAllLabels(); + + /** + * Create a new root-level label. + * @param labelRequest The request body containing the label name. + * @return The newly created LabelResponse. + */ + @POST("labels/") + Call