diff --git a/app/build.gradle b/app/build.gradle index ce0e08a..6ddd215 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,5 +1,8 @@ apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' apply plugin: 'com.google.firebase.crashlytics' +apply plugin: 'androidx.navigation.safeargs' android { compileSdkVersion 33 @@ -7,10 +10,16 @@ android { applicationId "com.nagpal.shivam.vtucslab" minSdkVersion 19 targetSdkVersion 33 - versionCode 8 - versionName "6.2" + versionCode 9 + versionName "7.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" multiDexEnabled true + + javaCompileOptions { + annotationProcessorOptions { + arguments += ["room.schemaLocation": "$projectDir/schemas".toString()] + } + } } buildTypes { release { @@ -18,35 +27,63 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + + kotlin { + jvmToolchain(11) + } + + android { + compileOptions { + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = 11 + } + } buildFeatures { - dataBinding true + viewBinding true } namespace 'com.nagpal.shivam.vtucslab' } dependencies { + implementation platform("com.google.firebase:firebase-bom:$versions.firebase_bom") implementation fileTree(include: ['*.jar'], dir: 'libs') - implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - implementation 'com.google.android.material:material:1.8.0' - implementation "androidx.multidex:multidex:2.0.1" + implementation "androidx.core:core-ktx:$versions.core_ktx" + implementation "androidx.appcompat:appcompat:$versions.appcompat" + implementation "androidx.constraintlayout:constraintlayout:$versions.constraintlayout" + implementation "com.google.android.material:material:$versions.material" + implementation "androidx.multidex:multidex:$versions.multidex" -// GSON - implementation 'com.google.code.gson:gson:2.8.9' + // Navigation + implementation "androidx.navigation:navigation-fragment-ktx:$versions.navigation" + implementation "androidx.navigation:navigation-ui-ktx:$versions.navigation" -// Firebase SDK - implementation('com.google.firebase:firebase-core:21.1.1') { - exclude module: 'support-v4' - } - implementation('com.google.firebase:firebase-messaging:23.1.2') { - exclude module: 'support-v4' - } - implementation 'com.google.firebase:firebase-crashlytics:18.3.5' + implementation "androidx.room:room-runtime:$versions.room" + kapt "androidx.room:room-compiler:$versions.room" + + // https://mvnrepository.com/artifact/com.fasterxml.jackson.module/jackson-module-kotlin + implementation "com.fasterxml.jackson.module:jackson-module-kotlin:$versions.jackson_module_kotlin" + + // https://mvnrepository.com/artifact/androidx.swiperefreshlayout/swiperefreshlayout + implementation "androidx.swiperefreshlayout:swiperefreshlayout:$versions.swipe_refresh_layout" + + // Firebase SDK + implementation 'com.google.firebase:firebase-messaging-ktx' + implementation 'com.google.firebase:firebase-crashlytics-ktx' + + // Retrofit + implementation "com.squareup.retrofit2:retrofit:$versions.retrofit" + implementation "com.squareup.retrofit2:converter-jackson:$versions.retrofit" + implementation "com.squareup.retrofit2:converter-scalars:$versions.retrofit" - testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test:runner:1.5.2' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + testImplementation "junit:junit:$versions.junit" + androidTestImplementation "androidx.test:runner:$versions.test_runner" + androidTestImplementation "androidx.test.espresso:espresso-core:$versions.espresso_core" } apply plugin: 'com.google.gms.google-services' +apply plugin: 'org.jetbrains.kotlin.android' diff --git a/app/schemas/com.nagpal.shivam.vtucslab.data.local.AppDatabase/1.json b/app/schemas/com.nagpal.shivam.vtucslab.data.local.AppDatabase/1.json new file mode 100644 index 0000000..ddc9153 --- /dev/null +++ b/app/schemas/com.nagpal.shivam.vtucslab.data.local.AppDatabase/1.json @@ -0,0 +1,52 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "f78e881b1af6632ac37cd5f3188a2360", + "entities": [ + { + "tableName": "lab_response", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `response` TEXT NOT NULL, `response_type` TEXT NOT NULL, `fetched_at` INTEGER NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "response", + "columnName": "response", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "responseType", + "columnName": "response_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fetchedAt", + "columnName": "fetched_at", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f78e881b1af6632ac37cd5f3188a2360')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6186b9e..fb387c3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,7 +6,7 @@ - - - + android:name=".activities.MainActivity" + android:exported="true" + android:theme="@style/AppTheme"> - - - - - - - - - + - diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/Activity/DisplayActivity.java b/app/src/main/java/com/nagpal/shivam/vtucslab/Activity/DisplayActivity.java deleted file mode 100644 index c3abfa0..0000000 --- a/app/src/main/java/com/nagpal/shivam/vtucslab/Activity/DisplayActivity.java +++ /dev/null @@ -1,250 +0,0 @@ -package com.nagpal.shivam.vtucslab.Activity; - -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.Intent; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.text.TextUtils; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.widget.Toast; - -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AppCompatActivity; -import androidx.core.app.NavUtils; -import androidx.databinding.DataBindingUtil; -import androidx.loader.app.LoaderManager; -import androidx.loader.content.Loader; - -import com.nagpal.shivam.vtucslab.Loader.RawStreamLoader; -import com.nagpal.shivam.vtucslab.R; -import com.nagpal.shivam.vtucslab.Utility.ConstantVariables; -import com.nagpal.shivam.vtucslab.databinding.ActivityDisplayBinding; - -public class DisplayActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks { - - private static final int LOADER_ID = 1; - private static final String SUCCEEDED_KEY = "succeeded_key"; - private static final String SCROLL_X_KEY = "scroll_x_key"; - private static final String SCROLL_Y_KEY = "scroll_y_key"; - - private static final String LOG_TAG = DisplayActivity.class.getSimpleName(); - - - // private TextView displayTextView; -// private PageView mDisplayPageView; -// private TextView mEmptyTextView; -// private ProgressBar mProgressBar; - - private Boolean mSucceeded = false; - private String mUrl; - private String mTitle; - private String mCode; - - - private ActivityDisplayBinding mBinding; - private int mScrollX; - private int mScrollY; - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.menu_display_activity, menu); - if (mSucceeded) { - MenuItem copyMenuItem = menu.findItem(R.id.menu_item_copy_display_activity); - copyMenuItem.setEnabled(true); - } - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - NavUtils.navigateUpFromSameTask(DisplayActivity.this); - return true; - - case R.id.display_menu_item_refresh: - recreate(); - return true; - - case R.id.menu_item_copy_display_activity: - ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); - ClipData clipData = new ClipData(ClipData.newPlainText("Code", mCode)); - if (clipboardManager != null) { - clipboardManager.setPrimaryClip(clipData); - Toast.makeText(DisplayActivity.this, "Code copied to Clipboard.", Toast.LENGTH_SHORT).show(); - } - return true; - } - return super.onOptionsItemSelected(item); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - mBinding = DataBindingUtil.setContentView(this, R.layout.activity_display); -// setContentView(R.layout.activity_display); -// logd("OnCreate Called"); - ActionBar actionBar = getSupportActionBar(); - - initViews(); - - if (savedInstanceState != null) { - mSucceeded = savedInstanceState.getBoolean(SUCCEEDED_KEY, false); - mScrollX = savedInstanceState.getInt(SCROLL_X_KEY, 0); - mScrollY = savedInstanceState.getInt(SCROLL_Y_KEY, 0); - } - - Intent intent = getIntent(); - mTitle = intent.getStringExtra(ConstantVariables.title_intent_tag); - mUrl = intent.getStringExtra(ConstantVariables.url_intent_tag); - - if (actionBar != null) { - actionBar.setDisplayHomeAsUpEnabled(true); - } - - DisplayActivity.this.setTitle(mTitle); - - ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE); - NetworkInfo networkInfo = connectivityManager != null ? connectivityManager.getActiveNetworkInfo() : null; - LoaderManager loaderManager = LoaderManager.getInstance(this); - - if (!mSucceeded) { - loaderManager.destroyLoader(LOADER_ID); - } - - if (networkInfo != null && networkInfo.isConnected()) { - loaderManager.initLoader(LOADER_ID, null, DisplayActivity.this); - } else { -// mProgressBar.setVisibility(View.GONE); - mBinding.progressBarDisplay.setVisibility(View.GONE); -// mEmptyTextView.setVisibility(View.VISIBLE); - mBinding.emptyTextViewDisplay.setVisibility(View.VISIBLE); -// mEmptyTextView.setText(R.string.no_internet_connection); - mBinding.emptyTextViewDisplay.setText(R.string.no_internet_connection); - mSucceeded = false; - } - } - - private void initViews() { -// mDisplayPageView = findViewById(R.id.display_page_view); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { -// mDisplayPageView.setLetterSpacing(0.1f); - mBinding.displayTextView.setLetterSpacing(0.1f); - } -// mEmptyTextView = findViewById(R.id.empty_text_view_display); -// mProgressBar = findViewById(R.id.progress_bar_display); - - //setupScrolling(); - } - -// private void setupScrolling() { -// mBinding.horizontalScroll.setOnTouchListener(new View.OnTouchListener() { -// @Override -// public boolean onTouch(View v, MotionEvent event) { -// return false; -// } -// }); -// mBinding.verticalScroll.setOnTouchListener(new View.OnTouchListener() { -// @Override -// public boolean onTouch(View v, MotionEvent event) { -// return false; -// } -// }); -// mBinding.displayTextView.setOnTouchListener(new View.OnTouchListener() { -// private float prevX, prevY, curX, curY; -// private boolean handled = false; -// -// @Override -// public boolean onTouch(View v, MotionEvent event) { -// Log.v("Ontouch Called", ""); -// mBinding.verticalScroll.requestDisallowInterceptTouchEvent(true); -// mBinding.horizontalScroll.requestDisallowInterceptTouchEvent(true); -// switch (event.getAction()) { -// case MotionEvent.ACTION_DOWN: -// handled = false; -// prevX = event.getX(); -// prevY = event.getY(); -// Log.v("Action Down", prevX + " " + prevY); -// break; -// case MotionEvent.ACTION_MOVE: -// handled = true; -// curX = event.getX(); -// curY = event.getY(); -// int dy = (int) (prevY - curY); -// int dx = (int) (prevX - curX); -// -// prevX = curX; -// prevY = curY; -// -// -// Log.v("Action Move Cur", curX + " " + curY); -//// Log.v("Action Move Delta", dx + " " + dy); -// -// mBinding.verticalScroll.scrollBy(0, dy); -// mBinding.horizontalScroll.scrollBy(dx, 0); -// break; -// case MotionEvent.ACTION_UP: -// Log.v("Action Up", event.getX() + " " + event.getY()); -// break; -// } -// return true; -// } -// }); -// } - - @Override - public Loader onCreateLoader(int i, Bundle bundle) { - return new RawStreamLoader(DisplayActivity.this, mUrl); - } - - @Override - public void onLoadFinished(Loader loader, String s) { -// mProgressBar.setVisibility(View.GONE); - mBinding.progressBarDisplay.setVisibility(View.GONE); - if (TextUtils.isEmpty(s)) { - Toast.makeText(DisplayActivity.this, getString(R.string.error_occurred), Toast.LENGTH_LONG).show(); - mSucceeded = false; - return; - } - - mSucceeded = true; - mCode = s; - s = s.replaceAll("\t", "\t\t"); -// displayTextView.setText(s); -// mDisplayPageView.setText(s); - mBinding.displayTextView.setText(s); - new Handler(getMainLooper()).postDelayed(new Runnable() { - @Override - public void run() { - mBinding.horizontalScroll.setScrollX(mScrollX); - mBinding.verticalScroll.setScrollY(mScrollY); - } - }, 500); - invalidateOptionsMenu(); - } - - @Override - public void onLoaderReset(Loader loader) { -// displayTextView.setText(null); -// mDisplayPageView.setText(null); - mBinding.displayTextView.setText(null); - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - outState.putBoolean(SUCCEEDED_KEY, mSucceeded); - outState.putInt(SCROLL_X_KEY, mBinding.horizontalScroll.getScrollX()); - outState.putInt(SCROLL_Y_KEY, mBinding.verticalScroll.getScrollY()); - super.onSaveInstanceState(outState); - } - -// private void logd(String str) { -// Log.d(LOG_TAG, str); -// } -} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/Activity/ProgramActivity.java b/app/src/main/java/com/nagpal/shivam/vtucslab/Activity/ProgramActivity.java deleted file mode 100644 index 8edb2af..0000000 --- a/app/src/main/java/com/nagpal/shivam/vtucslab/Activity/ProgramActivity.java +++ /dev/null @@ -1,178 +0,0 @@ -package com.nagpal.shivam.vtucslab.Activity; - - -import android.content.Intent; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.os.Bundle; -import android.view.MenuItem; -import android.view.View; -import android.widget.ProgressBar; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.appcompat.app.AppCompatActivity; -import androidx.core.app.NavUtils; -import androidx.loader.app.LoaderManager; -import androidx.loader.content.Loader; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import com.nagpal.shivam.vtucslab.Adapter.ContentAdapter; -import com.nagpal.shivam.vtucslab.Loader.InfoLoader; -import com.nagpal.shivam.vtucslab.Model.ContentFile; -import com.nagpal.shivam.vtucslab.Model.LabExperiment; -import com.nagpal.shivam.vtucslab.Model.LabResponse; -import com.nagpal.shivam.vtucslab.Model.Laboratory; -import com.nagpal.shivam.vtucslab.R; -import com.nagpal.shivam.vtucslab.Utility.ConstantVariables; - -import java.util.ArrayList; - -public class ProgramActivity - extends AppCompatActivity - implements LoaderManager.LoaderCallbacks, - ContentAdapter.ItemClickHandler { - - public static final String INTENT_LABORATORY = "PROGRAM_INTENT_LABORATORY"; - public static final String INTENT_LABORATORY_BASE_URL = "PROGRAM_INTENT_LABORATORY_BASE_URL"; - - private static final int REPO_LOADER_ID = 2; - private static final String SUCCEEDED_KEY = "succeeded_key"; - - private ContentAdapter mProgramAdapter; - private ProgressBar mProgressBar; - private RecyclerView mProgramRecyclerView; - private TextView mEmptyTextView; - private boolean mSucceeded; - private String mProgramBaseUrl; - private LoaderManager mLoaderManager; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_program); - - if (savedInstanceState != null) { - mSucceeded = savedInstanceState.getBoolean(SUCCEEDED_KEY, false); - } - - initAndSetupViews(); - - setupProgramAdapter(); - - loadPrograms(); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - NavUtils.navigateUpFromSameTask(ProgramActivity.this); - return true; - - } - return super.onOptionsItemSelected(item); - } - - private void initAndSetupViews() { - mProgramRecyclerView = findViewById(R.id.activity_program_recycler_view_program); - mProgramRecyclerView.setLayoutManager(new LinearLayoutManager(ProgramActivity.this, LinearLayoutManager.VERTICAL, false)); - mProgramRecyclerView.setHasFixedSize(true); - - mEmptyTextView = findViewById(R.id.activity_program_text_view_empty); - - mProgressBar = findViewById(R.id.activity_program_progress_bar); - } - - private void setupProgramAdapter() { - mProgramAdapter = new ContentAdapter(ProgramActivity.this, new ArrayList()); - mProgramRecyclerView.setAdapter(mProgramAdapter); - mProgramAdapter.setItemClickHandler(this); - } - - private void loadPrograms() { - Intent intent = getIntent(); - - Laboratory laboratory = (Laboratory) intent.getSerializableExtra(INTENT_LABORATORY); - String laboratoryBaseUrl = intent.getStringExtra(INTENT_LABORATORY_BASE_URL); - - ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE); - NetworkInfo networkInfo = connectivityManager != null ? connectivityManager.getActiveNetworkInfo() : null; - mLoaderManager = LoaderManager.getInstance(this); - - if (!mSucceeded) { - mLoaderManager.destroyLoader(REPO_LOADER_ID); - } - - mEmptyTextView.setVisibility(View.GONE); - - setTitle(laboratory.getTitle()); - if (networkInfo != null && networkInfo.isConnected()) { - Bundle bundle = new Bundle(); - String url = laboratoryBaseUrl + "/" + laboratory.getFileName(); - - bundle.putString("URL", url); - mLoaderManager.initLoader(REPO_LOADER_ID, bundle, ProgramActivity.this); - } else { - mProgressBar.setVisibility(View.GONE); - mSucceeded = false; - showErrorMessage(getString(R.string.no_internet_connection)); - } - } - - @Override - public Loader onCreateLoader(int id, Bundle args) { - return new InfoLoader(ProgramActivity.this, args.getString("URL")); - } - - @Override - public void onLoadFinished(Loader loader, LabResponse labResponse) { - mProgressBar.setVisibility(View.GONE); - - if (labResponse == null) { - mSucceeded = false; - Toast.makeText(ProgramActivity.this, getString(R.string.error_occurred), Toast.LENGTH_LONG).show(); - return; - } - - if (labResponse.isValid()) { - mProgramBaseUrl = labResponse.getGithub_raw_content() + "/" + - labResponse.getOrganization() + "/" + - labResponse.getRepository() + "/" + - labResponse.getBranch(); - mSucceeded = true; - mProgramAdapter.clear(); - mProgramAdapter.addAll(labResponse.getLabExperiments()); -// mLinkToRepo = labResponse.getLinkToRepo(); -// invalidateOptionsMenu(); - } else { - mSucceeded = false; - showErrorMessage(labResponse.getInvalidationMessage()); - } - } - - @Override - public void onLoaderReset(Loader loader) { - mProgramAdapter.clear(); - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - outState.putBoolean(SUCCEEDED_KEY, mSucceeded); - super.onSaveInstanceState(outState); - } - - @Override - public void onContentFileClick(ContentFile file) { - Intent intent = new Intent(ProgramActivity.this, DisplayActivity.class); - intent.putExtra(ConstantVariables.title_intent_tag, file.getFileName()); - intent.putExtra(ConstantVariables.url_intent_tag, mProgramBaseUrl + "/" + file.getFileName()); - startActivity(intent); - } - - private void showErrorMessage(String error) { - mEmptyTextView.setVisibility(View.VISIBLE); - mEmptyTextView.setText(error); - } -} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/Activity/RepositoryActivity.java b/app/src/main/java/com/nagpal/shivam/vtucslab/Activity/RepositoryActivity.java deleted file mode 100644 index 5164dea..0000000 --- a/app/src/main/java/com/nagpal/shivam/vtucslab/Activity/RepositoryActivity.java +++ /dev/null @@ -1,247 +0,0 @@ -package com.nagpal.shivam.vtucslab.Activity; - -import android.content.Intent; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.net.Uri; -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.view.Gravity; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.widget.ImageButton; -import android.widget.ProgressBar; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.ActionBarDrawerToggle; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; -import androidx.drawerlayout.widget.DrawerLayout; -import androidx.loader.app.LoaderManager; -import androidx.loader.content.Loader; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import com.google.android.material.navigation.NavigationView; -import com.nagpal.shivam.vtucslab.Adapter.NavigationAdapter; -import com.nagpal.shivam.vtucslab.Loader.InfoLoader; -import com.nagpal.shivam.vtucslab.Model.LabResponse; -import com.nagpal.shivam.vtucslab.Model.Laboratory; -import com.nagpal.shivam.vtucslab.R; - -import java.util.ArrayList; - -public class RepositoryActivity - extends AppCompatActivity - implements NavigationView.OnNavigationItemSelectedListener, - LoaderManager.LoaderCallbacks, - NavigationAdapter.NavigationAdapterItemClickHandler { - - private static final int NAV_LOADER_ID = 1; - private static final String SUCCEEDED_KEY = "succeeded_key"; - private static final String URL = "https://raw.githubusercontent.com/vtucs/Index_v3/master/Index_v3.json"; -// private static final String URL = "https://raw.githubusercontent.com/vtucs/Test_Index/master/TestIndex.json"; - - private DrawerLayout mDrawerLayout; - private LoaderManager mLoaderManager; - private NavigationAdapter mRepositoryAdapter; - private NavigationView mNavigationView; - private ProgressBar mProgressBar; - private RecyclerView mRepositoryRecyclerView; - private TextView mEmptyTextView; - private Toolbar mToolbar; - private View mHeaderView; - private boolean mSucceeded; - private String mLaboratoryBaseUrl; - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.menu_main_activity, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.main_menu_item_privacy: - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse("https://github.com/ShivamNagpal/Privacy_Policies/blob/master/VTU_CS_LAB_MANUAL.md")); - startActivity(intent); - return true; - } - return super.onOptionsItemSelected(item); - } - - - @Override - public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) { - boolean flag = false; - switch (menuItem.getItemId()) { - case R.id.menu_item_repository: - flag = true; - break; - case R.id.menu_item_exit: - exitApplication(); - flag = true; - break; - } - if (flag) { - closeNavigationDrawer(); - } - return flag; - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_repository); - mToolbar = findViewById(R.id.toolbar); - setSupportActionBar(mToolbar); - - if (savedInstanceState != null) { - mSucceeded = savedInstanceState.getBoolean(SUCCEEDED_KEY, false); - } - - initAndSetupViews(); - - setupRepositoryAdapter(); - - loadRepositories(); - } - - private void loadRepositories() { - ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE); - NetworkInfo networkInfo = connectivityManager != null ? connectivityManager.getActiveNetworkInfo() : null; - mLoaderManager = LoaderManager.getInstance(this); - - if (!mSucceeded) { - mLoaderManager.destroyLoader(NAV_LOADER_ID); - } - - if (networkInfo != null && networkInfo.isConnected()) { - mLoaderManager.initLoader(NAV_LOADER_ID, null, RepositoryActivity.this); - } else { - mProgressBar.setVisibility(View.GONE); - mSucceeded = false; - showErrorMessage(getString(R.string.no_internet_connection)); - } - - mRepositoryAdapter.setNavigationAdapterItemClickHandler(this); - } - - private void initAndSetupViews() { - mDrawerLayout = findViewById(R.id.activity_repository_drawer_layout); - setUpDrawerToggle(); - - mNavigationView = findViewById(R.id.activity_repository_navigation_view); - mNavigationView.setNavigationItemSelectedListener(this); - mNavigationView.getMenu().getItem(0).setChecked(true); - - mHeaderView = mNavigationView.getHeaderView(0); - - ImageButton navigationDrawerBackButton = mHeaderView.findViewById(R.id.activity_repository_image_button_close_drawer); - navigationDrawerBackButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - closeNavigationDrawer(); - } - }); - - mRepositoryRecyclerView = findViewById(R.id.activity_repository_recycler_view_repository); - mRepositoryRecyclerView.setLayoutManager(new LinearLayoutManager(RepositoryActivity.this, LinearLayoutManager.VERTICAL, false)); - mRepositoryRecyclerView.setHasFixedSize(true); - - mEmptyTextView = findViewById(R.id.empty_text_view_main); - - mProgressBar = findViewById(R.id.progress_bar_main); - } - - private void setUpDrawerToggle() { - ActionBarDrawerToggle actionBarDrawerToggle = - new ActionBarDrawerToggle(RepositoryActivity.this, - mDrawerLayout, - mToolbar, - R.string.drawer_open, - R.string.drawer_close); - mDrawerLayout.addDrawerListener(actionBarDrawerToggle); - actionBarDrawerToggle.syncState(); - } - - private void closeNavigationDrawer() { - mDrawerLayout.closeDrawer(Gravity.START, true); - } - - private void exitApplication() { - this.finishAffinity(); - new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { - @Override - public void run() { - System.exit(0); - } - }, 1000); - } - - private void setupRepositoryAdapter() { - mRepositoryAdapter = new NavigationAdapter(RepositoryActivity.this, new ArrayList()); - mRepositoryRecyclerView.setAdapter(mRepositoryAdapter); - } - - @Override - public Loader onCreateLoader(int id, Bundle args) { - return new InfoLoader(RepositoryActivity.this, URL); - - } - - @Override - public void onLoadFinished(Loader loader, LabResponse labResponse) { - mProgressBar.setVisibility(View.GONE); - - if (labResponse == null) { - mSucceeded = false; - Toast.makeText(RepositoryActivity.this, getString(R.string.error_occurred), Toast.LENGTH_LONG).show(); - return; - } - - if (labResponse.isValid()) { - mLaboratoryBaseUrl = labResponse.getGithub_raw_content() + "/" + - labResponse.getOrganization() + "/" + - labResponse.getRepository() + "/" + - labResponse.getBranch(); - - mRepositoryAdapter.clear(); - mRepositoryAdapter.addAll(labResponse.getLaboratories()); - } else { - mSucceeded = false; - showErrorMessage(labResponse.getInvalidationMessage()); - } - } - - @Override - public void onLoaderReset(Loader loader) { - mRepositoryAdapter.clear(); - } - - @Override - public void onNavigationAdapterItemClick(Laboratory laboratory, int i) { - Intent intent = new Intent(RepositoryActivity.this, ProgramActivity.class); - intent.putExtra(ProgramActivity.INTENT_LABORATORY, laboratory); - intent.putExtra(ProgramActivity.INTENT_LABORATORY_BASE_URL, mLaboratoryBaseUrl); - startActivity(intent); - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - outState.putBoolean(SUCCEEDED_KEY, mSucceeded); - super.onSaveInstanceState(outState); - } - - private void showErrorMessage(String error) { - mEmptyTextView.setVisibility(View.VISIBLE); - mEmptyTextView.setText(error); - } - -} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/Adapter/ContentAdapter.java b/app/src/main/java/com/nagpal/shivam/vtucslab/Adapter/ContentAdapter.java deleted file mode 100644 index 67e15f6..0000000 --- a/app/src/main/java/com/nagpal/shivam/vtucslab/Adapter/ContentAdapter.java +++ /dev/null @@ -1,209 +0,0 @@ -package com.nagpal.shivam.vtucslab.Adapter; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.databinding.DataBindingUtil; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import com.nagpal.shivam.vtucslab.Model.ContentFile; -import com.nagpal.shivam.vtucslab.Model.LabExperiment; -import com.nagpal.shivam.vtucslab.Model.LabExperimentSubPart; -import com.nagpal.shivam.vtucslab.R; -import com.nagpal.shivam.vtucslab.Utility.StaticMethods; -import com.nagpal.shivam.vtucslab.databinding.LayoutCardSeMspBinding; -import com.nagpal.shivam.vtucslab.databinding.LayoutCardSeSspMfBinding; -import com.nagpal.shivam.vtucslab.databinding.LayoutCardSeSspSfBinding; - -import java.util.ArrayList; -import java.util.Arrays; - -public class ContentAdapter extends RecyclerView.Adapter { - - private static final int VIEW_TYPE_SSP_SF = 0; - private static final int VIEW_TYPE_MSP_SF = 1; - private static final int VIEW_TYPE_SSP_MF = 2; - private static final int VIEW_TYPE_MSP_MF = 3; - private static final int VIEW_TYPE_INVALID = -1; - private Context mContext; - private ArrayList mLabExperimentArrayList; - private ItemClickHandler mItemClickHandler; - - public ContentAdapter(Context context, ArrayList labExperimentArrayList) { - this.mContext = context; - this.mLabExperimentArrayList = labExperimentArrayList; - } - - public void setItemClickHandler(ItemClickHandler itemClickHandler) { - mItemClickHandler = itemClickHandler; - } - - public void addAll(ArrayList labExperimentArrayList) { - int i = mLabExperimentArrayList.size(); - mLabExperimentArrayList.addAll(labExperimentArrayList); - notifyItemRangeInserted(i, labExperimentArrayList.size()); - } - - public void addAll(@NonNull LabExperiment[] labExperiments) { - int i = mLabExperimentArrayList.size(); - mLabExperimentArrayList.addAll(Arrays.asList(labExperiments)); - notifyItemRangeInserted(i, labExperiments.length); - } - - - public void clear() { - mLabExperimentArrayList.clear(); - notifyDataSetChanged(); - } - - @NonNull - @Override - public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - switch (viewType) { - case VIEW_TYPE_SSP_SF: - LayoutCardSeSspSfBinding sspSfBinding = DataBindingUtil.inflate(LayoutInflater.from(mContext), R.layout.layout_card_se_ssp_sf, parent, false); - return new SspSfViewHolder(sspSfBinding); - case VIEW_TYPE_MSP_SF: - LayoutCardSeMspBinding mspSfBinding = DataBindingUtil.inflate(LayoutInflater.from(mContext), R.layout.layout_card_se_msp, parent, false); - return new MspSfViewHolder(mspSfBinding); - case VIEW_TYPE_SSP_MF: - LayoutCardSeSspMfBinding sspMfBinding = DataBindingUtil.inflate(LayoutInflater.from(mContext), R.layout.layout_card_se_ssp_mf, parent, false); - return new SspMfViewHolder(sspMfBinding); - case VIEW_TYPE_MSP_MF: - LayoutCardSeMspBinding seMspBinding = DataBindingUtil.inflate(LayoutInflater.from(mContext), R.layout.layout_card_se_msp, parent, false); - return new MspMfViewHolder(seMspBinding); - default: - return new InvalidViewHolder(new View(mContext)); - - } - } - - @Override - public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { - LabExperiment labExperiment = mLabExperimentArrayList.get(position); - if (holder instanceof SspSfViewHolder) { - SspSfViewHolder sspSfViewHolder = (SspSfViewHolder) holder; - sspSfViewHolder.mBinding.serialOrder.setText(processSerialOrder(labExperiment.getSerialOrder())); - String[] parts = labExperiment.getLabExperimentSubParts()[0].getContentFiles()[0].getFileName().split("\\."); - if (parts.length >= 2) { - sspSfViewHolder.mBinding.programTitle.setText(StaticMethods.formatProgramName(parts[parts.length - 2])); - } - } else if (holder instanceof MspSfViewHolder) { - MspSfViewHolder mspSfViewHolder = (MspSfViewHolder) holder; - mspSfViewHolder.mBinding.serialOrder.setText(processSerialOrder(labExperiment.getSerialOrder())); - MultipleSubPartAdapter adapter = new MultipleSubPartAdapter(mContext, labExperiment.getLabExperimentSubParts(), false, mItemClickHandler); - mspSfViewHolder.mBinding.subPartContainer.setAdapter(adapter); - } else if (holder instanceof SspMfViewHolder) { - SspMfViewHolder sspMfViewHolder = (SspMfViewHolder) holder; - sspMfViewHolder.mBinding.serialOrder.setText(processSerialOrder(labExperiment.getSerialOrder())); - MultipleFileAdapter adapter = new MultipleFileAdapter(mContext, R.layout.layout_card_single_files_without_sub_parts, labExperiment.getLabExperimentSubParts()[0].getContentFiles(), mItemClickHandler); - sspMfViewHolder.mBinding.filesContainer.setAdapter(adapter); - } else if (holder instanceof MspMfViewHolder) { - MspMfViewHolder mspMfViewHolder = (MspMfViewHolder) holder; - mspMfViewHolder.mBinding.serialOrder.setText(processSerialOrder(labExperiment.getSerialOrder())); - MultipleSubPartAdapter adapter = new MultipleSubPartAdapter(mContext, labExperiment.getLabExperimentSubParts(), true, mItemClickHandler); - mspMfViewHolder.mBinding.subPartContainer.setAdapter(adapter); - } - } - - @Override - public int getItemCount() { - return mLabExperimentArrayList.size(); - } - - @Override - public int getItemViewType(int position) { - LabExperiment experiment = mLabExperimentArrayList.get(position); - int contentFileLength = 1; - LabExperimentSubPart[] labExperimentSubParts = experiment.getLabExperimentSubParts(); - for (int i = 0, labExperimentSubPartsLength = labExperimentSubParts.length; i < labExperimentSubPartsLength; i++) { - LabExperimentSubPart subPart = labExperimentSubParts[i]; - contentFileLength = Math.max(contentFileLength, subPart.getContentFiles().length); - } - if (labExperimentSubParts.length == 1 && contentFileLength == 1) - return VIEW_TYPE_SSP_SF; - else if (labExperimentSubParts.length > 1 && contentFileLength == 1) - return VIEW_TYPE_MSP_SF; - else if (labExperimentSubParts.length == 1 && contentFileLength > 1) - return VIEW_TYPE_SSP_MF; - else if (labExperimentSubParts.length > 1 && contentFileLength > 1) - return VIEW_TYPE_MSP_MF; - else - return VIEW_TYPE_INVALID; - } - - private String processSerialOrder(String order) { - try { - int i = Integer.parseInt(order); - return String.valueOf(i); - } catch (NumberFormatException e) { - return order; - } - } - - public interface ItemClickHandler { - void onContentFileClick(ContentFile file); - } - - class SspSfViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { - LayoutCardSeSspSfBinding mBinding; - - SspSfViewHolder(LayoutCardSeSspSfBinding binding) { - super(binding.getRoot()); - mBinding = binding; - binding.getRoot().setOnClickListener(this); - } - - @Override - public void onClick(View view) { - if (mItemClickHandler != null) { - int position = getAdapterPosition(); - ContentFile contentFile = mLabExperimentArrayList.get(position).getLabExperimentSubParts()[0].getContentFiles()[0]; - mItemClickHandler.onContentFileClick(contentFile); - } - } - } - - class MspSfViewHolder extends RecyclerView.ViewHolder { - LayoutCardSeMspBinding mBinding; - - MspSfViewHolder(LayoutCardSeMspBinding binding) { - super(binding.getRoot()); - mBinding = binding; - mBinding.subPartContainer.setLayoutManager(new LinearLayoutManager(mContext)); - mBinding.subPartContainer.setHasFixedSize(true); - } - } - - class SspMfViewHolder extends RecyclerView.ViewHolder { - LayoutCardSeSspMfBinding mBinding; - - SspMfViewHolder(LayoutCardSeSspMfBinding binding) { - super(binding.getRoot()); - mBinding = binding; - mBinding.filesContainer.setLayoutManager(new LinearLayoutManager(mContext)); - mBinding.filesContainer.setHasFixedSize(true); - } - } - - class MspMfViewHolder extends RecyclerView.ViewHolder { - LayoutCardSeMspBinding mBinding; - - MspMfViewHolder(LayoutCardSeMspBinding binding) { - super(binding.getRoot()); - mBinding = binding; - mBinding.subPartContainer.setLayoutManager(new LinearLayoutManager(mContext)); - mBinding.subPartContainer.setHasFixedSize(true); - } - } - - class InvalidViewHolder extends RecyclerView.ViewHolder { - InvalidViewHolder(@NonNull View itemView) { - super(itemView); - } - } -} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/Adapter/MultipleFileAdapter.java b/app/src/main/java/com/nagpal/shivam/vtucslab/Adapter/MultipleFileAdapter.java deleted file mode 100644 index 52b5161..0000000 --- a/app/src/main/java/com/nagpal/shivam/vtucslab/Adapter/MultipleFileAdapter.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.nagpal.shivam.vtucslab.Adapter; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.LayoutRes; -import androidx.annotation.NonNull; -import androidx.databinding.DataBindingUtil; -import androidx.recyclerview.widget.RecyclerView; - -import com.nagpal.shivam.vtucslab.Model.ContentFile; -import com.nagpal.shivam.vtucslab.Utility.StaticMethods; -import com.nagpal.shivam.vtucslab.databinding.LayoutCardSingleFilesWithoutSubPartsBinding; - -public class MultipleFileAdapter extends RecyclerView.Adapter { - private Context mContext; - private ContentFile[] mContentFiles; - private ContentAdapter.ItemClickHandler mItemClickHandler; - @LayoutRes - private int mLayoutId; - - public MultipleFileAdapter(Context context, @LayoutRes int layoutId, ContentFile[] contentFiles, ContentAdapter.ItemClickHandler itemClickHandler) { - mContext = context; - mLayoutId = layoutId; - mContentFiles = contentFiles; - mItemClickHandler = itemClickHandler; - } - - @NonNull - @Override - public ContentFileViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) { - LayoutCardSingleFilesWithoutSubPartsBinding binding = DataBindingUtil.inflate(LayoutInflater.from(mContext), mLayoutId, viewGroup, false); - return new ContentFileViewHolder(binding); - } - - @Override - public void onBindViewHolder(@NonNull ContentFileViewHolder contentFileViewHolder, int i) { - String[] parts = mContentFiles[i].getFileName().split("\\."); - if (parts.length >= 2) { - contentFileViewHolder.mBinding.programTitle.setText(StaticMethods.formatProgramName((parts[parts.length - 2]))); - } - } - - @Override - public int getItemCount() { - if (mContentFiles != null) { - return mContentFiles.length; - } else { - return 0; - } - } - - class ContentFileViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { - - LayoutCardSingleFilesWithoutSubPartsBinding mBinding; - - ContentFileViewHolder(LayoutCardSingleFilesWithoutSubPartsBinding binding) { - super(binding.getRoot()); - mBinding = binding; - binding.getRoot().setOnClickListener(this); - } - - @Override - public void onClick(View v) { - if (mItemClickHandler != null) { - int position = getAdapterPosition(); - ContentFile file = mContentFiles[position]; - mItemClickHandler.onContentFileClick(file); - } - } - } -} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/Adapter/MultipleSubPartAdapter.java b/app/src/main/java/com/nagpal/shivam/vtucslab/Adapter/MultipleSubPartAdapter.java deleted file mode 100644 index 1b94ac7..0000000 --- a/app/src/main/java/com/nagpal/shivam/vtucslab/Adapter/MultipleSubPartAdapter.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.nagpal.shivam.vtucslab.Adapter; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.databinding.DataBindingUtil; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import com.nagpal.shivam.vtucslab.Model.ContentFile; -import com.nagpal.shivam.vtucslab.Model.LabExperimentSubPart; -import com.nagpal.shivam.vtucslab.R; -import com.nagpal.shivam.vtucslab.Utility.StaticMethods; -import com.nagpal.shivam.vtucslab.databinding.LayoutCardSingleSubPartsWithFilesBinding; -import com.nagpal.shivam.vtucslab.databinding.LayoutCardSingleSubPartsWithoutFilesBinding; - -public class MultipleSubPartAdapter extends RecyclerView.Adapter { - private Context mContext; - private LabExperimentSubPart[] mSubParts; - private boolean mContainsMultipleFiles; - private ContentAdapter.ItemClickHandler mItemClickHandler; - - public MultipleSubPartAdapter(Context context, LabExperimentSubPart[] subParts, boolean containsMultipleFiles, ContentAdapter.ItemClickHandler itemClickHandler) { - mContext = context; - mSubParts = subParts; - mContainsMultipleFiles = containsMultipleFiles; - mItemClickHandler = itemClickHandler; - } - - @NonNull - @Override - public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) { - - if (!mContainsMultipleFiles) { - LayoutCardSingleSubPartsWithoutFilesBinding binding = DataBindingUtil.inflate(LayoutInflater.from(mContext), R.layout.layout_card_single_sub_parts_without_files, viewGroup, false); - return new SubPartWithoutFilesViewHolder(binding); - } else { - LayoutCardSingleSubPartsWithFilesBinding binding = DataBindingUtil.inflate(LayoutInflater.from(mContext), R.layout.layout_card_single_sub_parts_with_files, viewGroup, false); - return new SubPartWithFilesViewHolder(binding); - } - } - - @Override - public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int i) { - if (holder instanceof SubPartWithoutFilesViewHolder) { - SubPartWithoutFilesViewHolder subPartWithoutFilesViewHolder = (SubPartWithoutFilesViewHolder) holder; - subPartWithoutFilesViewHolder.mBinding.serialOrder.setText(mSubParts[i].getSubSerialOrder()); - String[] parts = mSubParts[i].getContentFiles()[0].getFileName().split("\\."); - if (parts.length >= 2) { - subPartWithoutFilesViewHolder.mBinding.programTitle.setText(StaticMethods.formatProgramName(parts[parts.length - 2])); - } - } else if (holder instanceof SubPartWithFilesViewHolder) { - SubPartWithFilesViewHolder subPartWithFilesViewHolder = (SubPartWithFilesViewHolder) holder; - subPartWithFilesViewHolder.mBinding.serialOrder.setText(mSubParts[i].getSubSerialOrder()); - MultipleFileAdapter adapter = new MultipleFileAdapter(mContext, R.layout.layout_card_single_files_without_sub_parts, mSubParts[i].getContentFiles(), mItemClickHandler); - subPartWithFilesViewHolder.mBinding.filesContainer.setAdapter(adapter); - } - } - - @Override - public int getItemCount() { - if (mSubParts != null) { - return mSubParts.length; - } - return 0; - } - - class SubPartWithoutFilesViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { - - LayoutCardSingleSubPartsWithoutFilesBinding mBinding; - - SubPartWithoutFilesViewHolder(LayoutCardSingleSubPartsWithoutFilesBinding binding) { - super(binding.getRoot()); - mBinding = binding; - binding.getRoot().setOnClickListener(this); - } - - @Override - public void onClick(View v) { - if (mItemClickHandler != null) { - int position = getAdapterPosition(); - ContentFile contentFile = mSubParts[position].getContentFiles()[0]; - mItemClickHandler.onContentFileClick(contentFile); - } - } - } - - class SubPartWithFilesViewHolder extends RecyclerView.ViewHolder { - - LayoutCardSingleSubPartsWithFilesBinding mBinding; - - SubPartWithFilesViewHolder(LayoutCardSingleSubPartsWithFilesBinding binding) { - super(binding.getRoot()); - mBinding = binding; - mBinding.filesContainer.setLayoutManager(new LinearLayoutManager(mContext)); - mBinding.filesContainer.setHasFixedSize(true); - } - - } -} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/Adapter/NavigationAdapter.java b/app/src/main/java/com/nagpal/shivam/vtucslab/Adapter/NavigationAdapter.java deleted file mode 100644 index cfc3a57..0000000 --- a/app/src/main/java/com/nagpal/shivam/vtucslab/Adapter/NavigationAdapter.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.nagpal.shivam.vtucslab.Adapter; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -import com.nagpal.shivam.vtucslab.Model.Laboratory; -import com.nagpal.shivam.vtucslab.R; - -import java.util.ArrayList; -import java.util.Arrays; - -public class NavigationAdapter extends RecyclerView.Adapter { - - private Context mContext; - private ArrayList mLaboratoryArrayList; - private NavigationAdapterItemClickHandler mNavigationAdapterItemClickHandler; - - public NavigationAdapter(@NonNull Context context, @NonNull ArrayList laboratoryArrayList) { - mContext = context; - mLaboratoryArrayList = laboratoryArrayList; - } - - public void addAll(ArrayList laboratoryArrayList) { - int i = mLaboratoryArrayList.size(); - mLaboratoryArrayList.addAll(laboratoryArrayList); - notifyItemRangeInserted(i, laboratoryArrayList.size()); - } - - public void addAll(Laboratory[] laboratories) { - int i = mLaboratoryArrayList.size(); - mLaboratoryArrayList.addAll(Arrays.asList(laboratories)); - notifyItemRangeInserted(i, laboratories.length); - } - - public void clear() { - mLaboratoryArrayList.clear(); - notifyDataSetChanged(); - } - - public void setNavigationAdapterItemClickHandler(NavigationAdapterItemClickHandler navigationAdapterItemClickHandler) { - mNavigationAdapterItemClickHandler = navigationAdapterItemClickHandler; - } - - @NonNull - @Override - public NavigationViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View view = LayoutInflater.from(mContext).inflate(R.layout.layout_card_repository, parent, false); - return new NavigationViewHolder(view); - } - - @Override - public void onBindViewHolder(@NonNull NavigationViewHolder holder, int position) { - Laboratory laboratory = mLaboratoryArrayList.get(position); - String fileName = laboratory.getFileName(); - String[] parts = fileName.split("\\."); - laboratory.setTitle(parts[0].replace('_', ' ')); - holder.mTextView.setText(laboratory.getTitle()); - } - - @Override - public int getItemCount() { - return mLaboratoryArrayList.size(); - } - - public interface NavigationAdapterItemClickHandler { - void onNavigationAdapterItemClick(Laboratory laboratory, int i); - } - - class NavigationViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { - TextView mTextView; - - NavigationViewHolder(View itemView) { - super(itemView); - mTextView = itemView.findViewById(R.id.text_view_layout); - itemView.setOnClickListener(this); - } - - @Override - public void onClick(View view) { - if (mNavigationAdapterItemClickHandler != null) { - int i = getAdapterPosition(); - mNavigationAdapterItemClickHandler.onNavigationAdapterItemClick(mLaboratoryArrayList.get(i), i); - } - } - } -} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/Loader/InfoLoader.java b/app/src/main/java/com/nagpal/shivam/vtucslab/Loader/InfoLoader.java deleted file mode 100644 index b0786d3..0000000 --- a/app/src/main/java/com/nagpal/shivam/vtucslab/Loader/InfoLoader.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.nagpal.shivam.vtucslab.Loader; - - -import android.content.Context; -import android.text.TextUtils; - -import androidx.loader.content.AsyncTaskLoader; - -import com.google.gson.Gson; -import com.nagpal.shivam.vtucslab.Model.LabResponse; -import com.nagpal.shivam.vtucslab.Utility.FetchUtil; - -public class InfoLoader extends AsyncTaskLoader { - private String mUrl; - private LabResponse mLabResponse; - - public InfoLoader(Context context, String url) { - super(context); - mUrl = url; - } - - @Override - protected void onStartLoading() { - if (mLabResponse != null) { - deliverResult(mLabResponse); - } else { - forceLoad(); - } - - } - - @Override - public LabResponse loadInBackground() { - if (mUrl == null) { - return null; - } - String jsonResponse = FetchUtil.fetchData(mUrl); - mLabResponse = extractFeaturesFromJson(jsonResponse); - return mLabResponse; - } - - private LabResponse extractFeaturesFromJson(String jsonResponse) { - if (TextUtils.isEmpty(jsonResponse)) { - return null; - } - Gson gson = new Gson(); - LabResponse labResponse = gson.fromJson(jsonResponse, LabResponse.class); - return labResponse; - } -} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/Loader/RawStreamLoader.java b/app/src/main/java/com/nagpal/shivam/vtucslab/Loader/RawStreamLoader.java deleted file mode 100644 index ddf22ca..0000000 --- a/app/src/main/java/com/nagpal/shivam/vtucslab/Loader/RawStreamLoader.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.nagpal.shivam.vtucslab.Loader; - -import android.content.Context; - -import androidx.loader.content.AsyncTaskLoader; - -import com.nagpal.shivam.vtucslab.Utility.FetchUtil; - -public class RawStreamLoader extends AsyncTaskLoader { - private static final String RAW_STREAM_LOADER = "raw_stream_loader"; - private String mUrl; - private String fetchedData; - - // private SharedPreferences sharedPreferences; -// TODO: Implement Local Data Saving. - public RawStreamLoader(Context context, String url) { - super(context); - mUrl = url; -// sharedPreferences = context.getSharedPreferences(RAW_STREAM_LOADER, Context.MODE_PRIVATE); -// fetchedData = sharedPreferences.getString(mUrl, null); - } - - @Override - protected void onStartLoading() { - if (fetchedData != null) { - deliverResult(fetchedData); - } else { - forceLoad(); - } - } - - @Override - public String loadInBackground() { - fetchedData = FetchUtil.fetchData(mUrl); -// TODO: If fetched data is not null. -// SharedPreferences.Editor editor = sharedPreferences.edit(); -// editor.putString(mUrl, fetchedData); -// editor.apply(); - return fetchedData; - } -} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/Model/ContentFile.java b/app/src/main/java/com/nagpal/shivam/vtucslab/Model/ContentFile.java deleted file mode 100644 index e6e087d..0000000 --- a/app/src/main/java/com/nagpal/shivam/vtucslab/Model/ContentFile.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.nagpal.shivam.vtucslab.Model; - -public class ContentFile { - private String fileName; - - public String getFileName() { - return fileName; - } - - public void setFileName(String fileName) { - this.fileName = fileName; - } -} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/Model/LabExperiment.java b/app/src/main/java/com/nagpal/shivam/vtucslab/Model/LabExperiment.java deleted file mode 100644 index 236555b..0000000 --- a/app/src/main/java/com/nagpal/shivam/vtucslab/Model/LabExperiment.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.nagpal.shivam.vtucslab.Model; - -public class LabExperiment { - - private String serialOrder; - private LabExperimentSubPart[] labExperimentSubParts; - - public String getSerialOrder() { - return serialOrder; - } - - public LabExperiment setSerialOrder(String serialOrder) { - this.serialOrder = serialOrder; - return this; - } - - public LabExperimentSubPart[] getLabExperimentSubParts() { - return labExperimentSubParts; - } - - public LabExperiment setLabExperimentSubParts(LabExperimentSubPart[] labExperimentSubParts) { - this.labExperimentSubParts = labExperimentSubParts; - return this; - } -} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/Model/LabExperimentSubPart.java b/app/src/main/java/com/nagpal/shivam/vtucslab/Model/LabExperimentSubPart.java deleted file mode 100644 index 61683a0..0000000 --- a/app/src/main/java/com/nagpal/shivam/vtucslab/Model/LabExperimentSubPart.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.nagpal.shivam.vtucslab.Model; - -public class LabExperimentSubPart { - - private String subSerialOrder; - private ContentFile[] contentFiles; - - public String getSubSerialOrder() { - return subSerialOrder; - } - - public LabExperimentSubPart setSubSerialOrder(String subSerialOrder) { - this.subSerialOrder = subSerialOrder; - return this; - } - - public ContentFile[] getContentFiles() { - return contentFiles; - } - - public LabExperimentSubPart setContentFiles(ContentFile[] contentFiles) { - this.contentFiles = contentFiles; - return this; - } -} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/Model/LabResponse.java b/app/src/main/java/com/nagpal/shivam/vtucslab/Model/LabResponse.java deleted file mode 100644 index e7421d3..0000000 --- a/app/src/main/java/com/nagpal/shivam/vtucslab/Model/LabResponse.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.nagpal.shivam.vtucslab.Model; - -public class LabResponse { - private String context; - private boolean isValid = true; - private String invalidationMessage; - private Laboratory[] laboratories; - private LabExperiment[] labExperiments; - private String github_raw_content; - private String organization; - private String repository; - private String branch; - - public String getContext() { - return context; - } - - public void setContext(String context) { - this.context = context; - } - - public boolean isValid() { - return isValid; - } - - public void setValid(boolean valid) { - isValid = valid; - } - - public String getInvalidationMessage() { - return invalidationMessage; - } - - public void setInvalidationMessage(String invalidationMessage) { - this.invalidationMessage = invalidationMessage; - } - - public LabExperiment[] getLabExperiments() { - return labExperiments; - } - - public LabResponse setLabExperiments(LabExperiment[] labExperiments) { - this.labExperiments = labExperiments; - return this; - } - - public Laboratory[] getLaboratories() { - return laboratories; - } - - public LabResponse setLaboratories(Laboratory[] laboratories) { - this.laboratories = laboratories; - return this; - } - - public String getGithub_raw_content() { - return github_raw_content; - } - - public void setGithub_raw_content(String github_raw_content) { - this.github_raw_content = github_raw_content; - } - - public String getOrganization() { - return organization; - } - - public void setOrganization(String organization) { - this.organization = organization; - } - - public String getRepository() { - return repository; - } - - public void setRepository(String repository) { - this.repository = repository; - } - - public String getBranch() { - return branch; - } - - public void setBranch(String branch) { - this.branch = branch; - } -} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/Model/Laboratory.java b/app/src/main/java/com/nagpal/shivam/vtucslab/Model/Laboratory.java deleted file mode 100644 index 7ae3a03..0000000 --- a/app/src/main/java/com/nagpal/shivam/vtucslab/Model/Laboratory.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.nagpal.shivam.vtucslab.Model; - -import java.io.Serializable; - -public class Laboratory implements Serializable { - private String title; - private String fileName; - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getFileName() { - return fileName; - } - - public void setFileName(String fileName) { - this.fileName = fileName; - } -} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/Utility/ConstantVariables.java b/app/src/main/java/com/nagpal/shivam/vtucslab/Utility/ConstantVariables.java deleted file mode 100644 index c1738c5..0000000 --- a/app/src/main/java/com/nagpal/shivam/vtucslab/Utility/ConstantVariables.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.nagpal.shivam.vtucslab.Utility; - -public class ConstantVariables { - public static final String title_intent_tag = "title"; - public static final String url_intent_tag = "url"; -} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/Utility/FetchUtil.java b/app/src/main/java/com/nagpal/shivam/vtucslab/Utility/FetchUtil.java deleted file mode 100644 index 39889b3..0000000 --- a/app/src/main/java/com/nagpal/shivam/vtucslab/Utility/FetchUtil.java +++ /dev/null @@ -1,98 +0,0 @@ -package com.nagpal.shivam.vtucslab.Utility; - -import android.util.Log; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; -import java.nio.charset.Charset; - -import static java.net.HttpURLConnection.HTTP_OK; - -public class FetchUtil { - - private static final String LOG_TAG = "FetchUtils"; - - public FetchUtil() { - } - - public static String fetchData(String requestUrl) { - Log.i(LOG_TAG, "Fetching Data."); - URL url = createUrl(requestUrl); - String jsonResponse = ""; - try { - jsonResponse = makeHttpRequest(url); - } catch (IOException e) { - Log.e(LOG_TAG, "Error while making HTTP request.", e); - } - return jsonResponse; - } - - private static URL createUrl(String stringUrl) { - URL url = null; - try { - url = new URL(stringUrl); - } catch (MalformedURLException e) { - Log.e(LOG_TAG, "Error with url creation", e); - } - return url; - } - - private static String makeHttpRequest(URL url) throws IOException { - String jsonResponse = ""; - if (url == null) { - return jsonResponse; - } - - HttpURLConnection urlConnection = null; - InputStream inputStream = null; - try { - urlConnection = (HttpURLConnection) url.openConnection(); - urlConnection.setReadTimeout(10000); - urlConnection.setConnectTimeout(15000); - urlConnection.setRequestMethod("GET"); - urlConnection.connect(); - - int responseCode = urlConnection.getResponseCode(); - if (responseCode == HTTP_OK) { - inputStream = urlConnection.getInputStream(); - jsonResponse = readFromStream(inputStream); - } else { - Log.e(LOG_TAG, "Error Response Code: " + responseCode); - } - } catch (IOException e) { - Log.e(LOG_TAG, "Problem retrieving JSON response\n"); - } finally { - if (urlConnection != null) { - urlConnection.disconnect(); - } - if (inputStream != null) { - inputStream.close(); - } - } - return jsonResponse; - } - - private static String readFromStream(InputStream inputStream) throws IOException { - StringBuilder stringBuilder = new StringBuilder(); - if (inputStream != null) { - InputStreamReader inputStreamReader = new InputStreamReader(inputStream, Charset.forName("UTF-8")); - BufferedReader bufferedReader = new BufferedReader(inputStreamReader); - String line = bufferedReader.readLine(); - while (line != null) { -// line = line.replaceAll("\t", "\t\t"); - //TODO: Temporary Solution to add padding with white spaces - stringBuilder.append(" "); - stringBuilder.append(line); - stringBuilder.append(" \n"); - line = bufferedReader.readLine(); - } - } - return stringBuilder.toString(); - } - -} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/Utility/StaticMethods.java b/app/src/main/java/com/nagpal/shivam/vtucslab/Utility/StaticMethods.java deleted file mode 100644 index e466ed7..0000000 --- a/app/src/main/java/com/nagpal/shivam/vtucslab/Utility/StaticMethods.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.nagpal.shivam.vtucslab.Utility; - -public class StaticMethods { - - public static String formatProgramName(String programName) { - return programName.replace('_', ' '); - } -} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/VTUCSLabApplication.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/VTUCSLabApplication.kt new file mode 100644 index 0000000..d390c02 --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/VTUCSLabApplication.kt @@ -0,0 +1,42 @@ +package com.nagpal.shivam.vtucslab + +import androidx.multidex.MultiDexApplication +import androidx.room.Room +import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.nagpal.shivam.vtucslab.data.local.AppDatabase +import com.nagpal.shivam.vtucslab.repositories.VtuCsLabRepository +import com.nagpal.shivam.vtucslab.repositories.VtuCsLabRepositoryImpl +import com.nagpal.shivam.vtucslab.services.VtuCsLabService +import com.nagpal.shivam.vtucslab.utilities.Constants.VTU_CS_LAB +import com.nagpal.shivam.vtucslab.utilities.StaticMethods + +class VTUCSLabApplication : MultiDexApplication() { + private lateinit var _db: AppDatabase + val db: AppDatabase + get() = _db + + private lateinit var _vtuCsLabRepository: VtuCsLabRepository + val vtuCsLabRepository: VtuCsLabRepository + get() = _vtuCsLabRepository + + override fun onCreate() { + super.onCreate() + if (BuildConfig.DEBUG) { + FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(false) + } + + _db = Room.databaseBuilder( + applicationContext, + AppDatabase::class.java, + VTU_CS_LAB + ).build() + + val vtuCsLabService = VtuCsLabService.instance + _vtuCsLabRepository = VtuCsLabRepositoryImpl( + this, + vtuCsLabService, + _db.labResponseDao(), + StaticMethods.jsonMapper + ) + } +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/activities/MainActivity.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/activities/MainActivity.kt new file mode 100644 index 0000000..d859f82 --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/activities/MainActivity.kt @@ -0,0 +1,137 @@ +package com.nagpal.shivam.vtucslab.activities + +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.view.MenuItem +import android.view.View +import androidx.activity.addCallback +import androidx.appcompat.app.ActionBarDrawerToggle +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.GravityCompat +import androidx.drawerlayout.widget.DrawerLayout +import androidx.drawerlayout.widget.DrawerLayout.LOCK_MODE_LOCKED_CLOSED +import androidx.drawerlayout.widget.DrawerLayout.LOCK_MODE_UNLOCKED +import androidx.navigation.NavController +import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.ui.AppBarConfiguration +import androidx.navigation.ui.navigateUp +import androidx.navigation.ui.onNavDestinationSelected +import androidx.navigation.ui.setupActionBarWithNavController +import androidx.navigation.ui.setupWithNavController +import com.nagpal.shivam.vtucslab.R +import com.nagpal.shivam.vtucslab.databinding.ActivityMainBinding +import kotlin.system.exitProcess + +class MainActivity : AppCompatActivity() { + private lateinit var binding: ActivityMainBinding + private lateinit var navController: NavController + private lateinit var appBarConfiguration: AppBarConfiguration + private lateinit var actionBarDrawerToggle: ActionBarDrawerToggle + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + val navHostFragment = + supportFragmentManager.findFragmentById(R.id.navigation_host_fragment) as NavHostFragment + navController = navHostFragment.navController + + setupActionBar() + setupNavigationView() + setupDrawerToggle() + } + + private fun setupActionBar() { + appBarConfiguration = + AppBarConfiguration(setOf(R.id.repositoryFragment), binding.drawerLayout) + setupActionBarWithNavController(navController, appBarConfiguration) + } + + private fun setupNavigationView() { + binding.navigationView.setupWithNavController(navController) + binding.navigationView.setNavigationItemSelectedListener { + var flag = false + val itemId: Int = it.itemId + if (itemId == R.id.repositoryFragment) { + flag = true + } else if (itemId == R.id.menu_item_exit) { + exitApplication() + flag = true + } + if (flag) { + closeNavigationDrawer() + } + return@setNavigationItemSelectedListener flag + } + } + + private fun setupDrawerToggle() { + actionBarDrawerToggle = ActionBarDrawerToggle( + this, + binding.drawerLayout, + R.string.drawer_open, + R.string.drawer_close + ) + binding.drawerLayout.addDrawerListener(actionBarDrawerToggle) + actionBarDrawerToggle.syncState() + + val drawerBackPressedCallback = onBackPressedDispatcher.addCallback(this, false) { + closeNavigationDrawer() + } + binding.drawerLayout.addDrawerListener(object : DrawerLayout.DrawerListener { + override fun onDrawerSlide(drawerView: View, slideOffset: Float) { + } + + override fun onDrawerOpened(drawerView: View) { + drawerBackPressedCallback.isEnabled = true + } + + override fun onDrawerClosed(drawerView: View) { + drawerBackPressedCallback.isEnabled = false + } + + override fun onDrawerStateChanged(newState: Int) { + } + }) + navController.addOnDestinationChangedListener { _, destination, _ -> + if (appBarConfiguration.topLevelDestinations.contains(destination.id)) { + binding.drawerLayout.setDrawerLockMode(LOCK_MODE_UNLOCKED, GravityCompat.START) + } else { + binding.drawerLayout.setDrawerLockMode( + LOCK_MODE_LOCKED_CLOSED, + GravityCompat.START + ) + } + } + } + + override fun onSupportNavigateUp(): Boolean { + return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp() + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home && appBarConfiguration.topLevelDestinations.contains( + navController.currentDestination?.id + ) + ) { + actionBarDrawerToggle.syncState() + if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) { + binding.drawerLayout.closeDrawer(GravityCompat.START) + return true + } + } + return item.onNavDestinationSelected(navController) || super.onOptionsItemSelected(item) + } + + + private fun exitApplication() { + finishAffinity() + Handler(Looper.getMainLooper()).postDelayed({ + exitProcess(0) + }, 1000) + } + + private fun closeNavigationDrawer() { + binding.drawerLayout.closeDrawer(GravityCompat.START, true) + } +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/adapters/ContentAdapter.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/adapters/ContentAdapter.kt new file mode 100644 index 0000000..1601138 --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/adapters/ContentAdapter.kt @@ -0,0 +1,229 @@ +package com.nagpal.shivam.vtucslab.adapters + +import android.annotation.SuppressLint +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.nagpal.shivam.vtucslab.databinding.LayoutCardSeMspBinding +import com.nagpal.shivam.vtucslab.databinding.LayoutCardSeSspMfBinding +import com.nagpal.shivam.vtucslab.databinding.LayoutCardSeSspSfBinding +import com.nagpal.shivam.vtucslab.models.ContentFile +import com.nagpal.shivam.vtucslab.models.LabExperiment +import com.nagpal.shivam.vtucslab.utilities.StaticMethods.formatProgramName + +class ContentAdapter( + private val context: Context, + private val labExperimentArrayList: ArrayList +) : RecyclerView.Adapter() { + private var itemClickHandler: ItemClickHandler? = null + fun setItemClickHandler(itemClickHandler: ItemClickHandler) { + this.itemClickHandler = itemClickHandler + } + + fun addAll(labExperiments: List) { + val i = labExperimentArrayList.size + labExperimentArrayList.addAll(labExperiments) + notifyItemRangeInserted(i, labExperiments.size) + } + + @SuppressLint("NotifyDataSetChanged") + fun clear() { + labExperimentArrayList.clear() + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return when (viewType) { + VIEW_TYPE_SSP_SF -> { + val sspSfBinding = + LayoutCardSeSspSfBinding.inflate( + LayoutInflater.from(context), + parent, + false + ) + SspSfViewHolder(sspSfBinding) + } + + VIEW_TYPE_MSP_SF -> { + val mspSfBinding = + LayoutCardSeMspBinding.inflate( + LayoutInflater.from(context), + parent, + false + ) + MspSfViewHolder(mspSfBinding) + } + + VIEW_TYPE_SSP_MF -> { + val sspMfBinding = + LayoutCardSeSspMfBinding.inflate( + LayoutInflater.from(context), + parent, + false + ) + SspMfViewHolder(sspMfBinding) + } + + VIEW_TYPE_MSP_MF -> { + val seMspBinding = + LayoutCardSeMspBinding.inflate( + LayoutInflater.from(context), + parent, + false + ) + MspMfViewHolder(seMspBinding) + } + + else -> InvalidViewHolder(View(context)) + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val labExperiment = labExperimentArrayList[position] + val serialOrder = processSerialOrder(labExperiment.serialOrder) + when (holder) { + is SspSfViewHolder -> { + holder.binding.serialOrder.text = serialOrder + val parts = + labExperiment.labExperimentSubParts[0].contentFiles[0].fileName.split("\\.".toRegex()) + .dropLastWhile { it.isEmpty() }.toTypedArray() + if (parts.size >= 2) { + holder.binding.programTitle.text = + formatProgramName(parts[parts.size - 2]) + } + } + + is MspSfViewHolder -> { + holder.binding.serialOrder.text = serialOrder + val adapter = MultipleSubPartAdapter( + context, + labExperiment.labExperimentSubParts, + false, + itemClickHandler + ) + holder.binding.subPartContainer.adapter = adapter + } + + is SspMfViewHolder -> { + holder.binding.serialOrder.text = serialOrder + val adapter = MultipleFileAdapter( + context, + labExperiment.labExperimentSubParts[0].contentFiles, + itemClickHandler + ) + holder.binding.filesContainer.adapter = adapter + + } + + is MspMfViewHolder -> { + holder.binding.serialOrder.text = serialOrder + val adapter = MultipleSubPartAdapter( + context, + labExperiment.labExperimentSubParts, + true, + itemClickHandler + ) + holder.binding.subPartContainer.adapter = adapter + } + } + } + + override fun getItemCount(): Int { + return labExperimentArrayList.size + } + + override fun getItemViewType(position: Int): Int { + val experiment = labExperimentArrayList[position] + var contentFileLength = 1 + val labExperimentSubParts = experiment.labExperimentSubParts + for (subPart in labExperimentSubParts) { + contentFileLength = contentFileLength.coerceAtLeast(subPart.contentFiles.size) + } + val labExperimentSubPartsSize = labExperimentSubParts.size + return if (labExperimentSubPartsSize == 1 && contentFileLength == 1) + VIEW_TYPE_SSP_SF + else if (labExperimentSubPartsSize > 1 && contentFileLength == 1) + VIEW_TYPE_MSP_SF + else if (labExperimentSubPartsSize == 1) + VIEW_TYPE_SSP_MF + else if (labExperimentSubPartsSize > 1) + VIEW_TYPE_MSP_MF + else VIEW_TYPE_INVALID + } + + private fun processSerialOrder(order: String?): String { + if (order == null) { + return "#" + } + return try { + val i = order.toInt() + i.toString() + } catch (e: NumberFormatException) { + order + } + } + + interface ItemClickHandler { + fun onContentFileClick(file: ContentFile) + } + + internal class InvalidViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) + internal inner class SspSfViewHolder(var binding: LayoutCardSeSspSfBinding) : + RecyclerView.ViewHolder( + binding.root + ), View.OnClickListener { + init { + binding.root.setOnClickListener(this) + } + + override fun onClick(view: View) { + itemClickHandler?.let { + val position = adapterPosition + val contentFile = + labExperimentArrayList[position].labExperimentSubParts[0].contentFiles[0] + it.onContentFileClick(contentFile) + } + } + } + + internal inner class MspSfViewHolder(var binding: LayoutCardSeMspBinding) : + RecyclerView.ViewHolder( + binding.root + ) { + init { + binding.subPartContainer.layoutManager = LinearLayoutManager(context) + binding.subPartContainer.setHasFixedSize(true) + } + } + + internal inner class SspMfViewHolder(var binding: LayoutCardSeSspMfBinding) : + RecyclerView.ViewHolder( + binding.root + ) { + init { + binding.filesContainer.layoutManager = LinearLayoutManager(context) + binding.filesContainer.setHasFixedSize(true) + } + } + + internal inner class MspMfViewHolder(var binding: LayoutCardSeMspBinding) : + RecyclerView.ViewHolder( + binding.root + ) { + init { + binding.subPartContainer.layoutManager = LinearLayoutManager(context) + binding.subPartContainer.setHasFixedSize(true) + } + } + + companion object { + private const val VIEW_TYPE_SSP_SF = 0 + private const val VIEW_TYPE_MSP_SF = 1 + private const val VIEW_TYPE_SSP_MF = 2 + private const val VIEW_TYPE_MSP_MF = 3 + private const val VIEW_TYPE_INVALID = -1 + } +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/adapters/MultipleFileAdapter.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/adapters/MultipleFileAdapter.kt new file mode 100644 index 0000000..556f467 --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/adapters/MultipleFileAdapter.kt @@ -0,0 +1,54 @@ +package com.nagpal.shivam.vtucslab.adapters + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.nagpal.shivam.vtucslab.adapters.ContentAdapter.ItemClickHandler +import com.nagpal.shivam.vtucslab.adapters.MultipleFileAdapter.ContentFileViewHolder +import com.nagpal.shivam.vtucslab.databinding.LayoutCardSingleFilesWithoutSubPartsBinding +import com.nagpal.shivam.vtucslab.models.ContentFile +import com.nagpal.shivam.vtucslab.utilities.StaticMethods.formatProgramName + +class MultipleFileAdapter( + private val context: Context, + private val contentFiles: List, + private val itemClickHandler: ItemClickHandler? +) : RecyclerView.Adapter() { + override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): ContentFileViewHolder { + val binding = LayoutCardSingleFilesWithoutSubPartsBinding.inflate( + LayoutInflater.from(context), viewGroup, false + ) + return ContentFileViewHolder(binding) + } + + override fun onBindViewHolder(contentFileViewHolder: ContentFileViewHolder, i: Int) { + val parts = + contentFiles[i].fileName.split("\\.".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray() + if (parts.size >= 2) { + contentFileViewHolder.binding.programTitle.text = + formatProgramName(parts[parts.size - 2]) + } + } + + override fun getItemCount(): Int { + return contentFiles.size + } + + inner class ContentFileViewHolder(var binding: LayoutCardSingleFilesWithoutSubPartsBinding) : + RecyclerView.ViewHolder(binding.root), View.OnClickListener { + init { + binding.root.setOnClickListener(this) + } + + override fun onClick(v: View) { + itemClickHandler?.let { + val position = adapterPosition + val file = contentFiles[position] + it.onContentFileClick(file) + } + } + } +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/adapters/MultipleSubPartAdapter.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/adapters/MultipleSubPartAdapter.kt new file mode 100644 index 0000000..c92f026 --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/adapters/MultipleSubPartAdapter.kt @@ -0,0 +1,96 @@ +package com.nagpal.shivam.vtucslab.adapters + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.nagpal.shivam.vtucslab.adapters.ContentAdapter.ItemClickHandler +import com.nagpal.shivam.vtucslab.databinding.LayoutCardSingleSubPartsWithFilesBinding +import com.nagpal.shivam.vtucslab.databinding.LayoutCardSingleSubPartsWithoutFilesBinding +import com.nagpal.shivam.vtucslab.models.LabExperimentSubPart +import com.nagpal.shivam.vtucslab.utilities.StaticMethods.formatProgramName + +class MultipleSubPartAdapter( + private val context: Context, + private val subParts: List, + private val containsMultipleFiles: Boolean, + private val itemClickHandler: ItemClickHandler? +) : RecyclerView.Adapter() { + override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): RecyclerView.ViewHolder { + return if (!containsMultipleFiles) { + val binding = + LayoutCardSingleSubPartsWithoutFilesBinding.inflate( + LayoutInflater.from(context), + viewGroup, + false + ) + SubPartWithoutFilesViewHolder(binding) + } else { + val binding = + LayoutCardSingleSubPartsWithFilesBinding.inflate( + LayoutInflater.from(context), + viewGroup, + false + ) + SubPartWithFilesViewHolder(binding) + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, i: Int) { + when (holder) { + is SubPartWithoutFilesViewHolder -> { + holder.binding.serialOrder.text = subParts[i].subSerialOrder + val parts = subParts[i].contentFiles[0].fileName.split("\\.".toRegex()) + .dropLastWhile { it.isEmpty() }.toTypedArray() + if (parts.size >= 2) { + holder.binding.programTitle.text = formatProgramName( + parts[parts.size - 2] + ) + } + } + + is SubPartWithFilesViewHolder -> { + holder.binding.serialOrder.text = subParts[i].subSerialOrder + val adapter = MultipleFileAdapter( + context, + subParts[i].contentFiles, + itemClickHandler + ) + holder.binding.filesContainer.adapter = adapter + } + } + } + + override fun getItemCount(): Int { + return subParts.size + } + + internal inner class SubPartWithoutFilesViewHolder(var binding: LayoutCardSingleSubPartsWithoutFilesBinding) : + RecyclerView.ViewHolder( + binding.root + ), View.OnClickListener { + init { + binding.root.setOnClickListener(this) + } + + override fun onClick(v: View) { + itemClickHandler?.let { + val position = adapterPosition + val contentFile = subParts[position].contentFiles[0] + it.onContentFileClick(contentFile) + } + } + } + + internal inner class SubPartWithFilesViewHolder(var binding: LayoutCardSingleSubPartsWithFilesBinding) : + RecyclerView.ViewHolder( + binding.root + ) { + init { + binding.filesContainer.layoutManager = LinearLayoutManager(context) + binding.filesContainer.setHasFixedSize(true) + } + } +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/adapters/NavigationAdapter.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/adapters/NavigationAdapter.kt new file mode 100644 index 0000000..1a65a03 --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/adapters/NavigationAdapter.kt @@ -0,0 +1,73 @@ +package com.nagpal.shivam.vtucslab.adapters + +import android.annotation.SuppressLint +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.nagpal.shivam.vtucslab.R +import com.nagpal.shivam.vtucslab.adapters.NavigationAdapter.NavigationViewHolder +import com.nagpal.shivam.vtucslab.models.Laboratory + +class NavigationAdapter( + private val context: Context, + private val laboratoryArrayList: ArrayList +) : RecyclerView.Adapter() { + private var navigationAdapterItemClickHandler: NavigationAdapterItemClickHandler? = null + fun addAll(laboratories: List) { + val i = laboratoryArrayList.size + laboratoryArrayList.addAll(laboratories) + notifyItemRangeInserted(i, laboratories.size) + } + + @SuppressLint("NotifyDataSetChanged") + fun clear() { + laboratoryArrayList.clear() + notifyDataSetChanged() + } + + fun setNavigationAdapterItemClickHandler(navigationAdapterItemClickHandler: NavigationAdapterItemClickHandler?) { + this.navigationAdapterItemClickHandler = navigationAdapterItemClickHandler + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NavigationViewHolder { + val view = + LayoutInflater.from(context).inflate(R.layout.layout_card_repository, parent, false) + return NavigationViewHolder(view) + } + + override fun onBindViewHolder(holder: NavigationViewHolder, position: Int) { + val laboratory = laboratoryArrayList[position] + val fileName = laboratory.fileName + val parts = fileName.split("\\.".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + laboratory.title = parts[0].replace('_', ' ') + holder.textView.text = laboratory.title + } + + override fun getItemCount(): Int { + return laboratoryArrayList.size + } + + interface NavigationAdapterItemClickHandler { + fun onNavigationAdapterItemClick(laboratory: Laboratory, i: Int) + } + + inner class NavigationViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), + View.OnClickListener { + var textView: TextView + + init { + textView = itemView.findViewById(R.id.text_view_layout) + itemView.setOnClickListener(this) + } + + override fun onClick(view: View) { + navigationAdapterItemClickHandler?.let { + val i = adapterPosition + it.onNavigationAdapterItemClick(laboratoryArrayList[i], i) + } + } + } +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/core/ErrorType.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/core/ErrorType.kt new file mode 100644 index 0000000..5cee513 --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/core/ErrorType.kt @@ -0,0 +1,6 @@ +package com.nagpal.shivam.vtucslab.core + +enum class ErrorType { + NoActiveInternetConnection, + SomeErrorOccurred, +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/core/Resource.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/core/Resource.kt new file mode 100644 index 0000000..8e1523e --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/core/Resource.kt @@ -0,0 +1,7 @@ +package com.nagpal.shivam.vtucslab.core + +sealed class Resource { + class Loading(val data: D? = null) : Resource() + class Success(val data: D?) : Resource() + class Error(val error: E) : Resource() +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/core/UIMessage.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/core/UIMessage.kt new file mode 100644 index 0000000..2d09290 --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/core/UIMessage.kt @@ -0,0 +1,3 @@ +package com.nagpal.shivam.vtucslab.core + +data class UIMessage(val messageType: UIMessageType, val args: List = emptyList()) diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/core/UIMessageType.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/core/UIMessageType.kt new file mode 100644 index 0000000..d717723 --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/core/UIMessageType.kt @@ -0,0 +1,7 @@ +package com.nagpal.shivam.vtucslab.core + +enum class UIMessageType { + NoActiveInternetConnectionDetailed, + NoActiveInternetConnection, + SomeErrorOccurred, +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/data/local/AppDatabase.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/data/local/AppDatabase.kt new file mode 100644 index 0000000..2a39579 --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/data/local/AppDatabase.kt @@ -0,0 +1,11 @@ +package com.nagpal.shivam.vtucslab.data.local + +import androidx.room.Database +import androidx.room.RoomDatabase +import androidx.room.TypeConverters + +@Database(entities = [LabResponse::class], version = 1) +@TypeConverters(Converters::class) +abstract class AppDatabase : RoomDatabase() { + abstract fun labResponseDao(): LabResponseDao +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/data/local/Converters.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/data/local/Converters.kt new file mode 100644 index 0000000..996822a --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/data/local/Converters.kt @@ -0,0 +1,16 @@ +package com.nagpal.shivam.vtucslab.data.local + +import androidx.room.TypeConverter +import java.util.Date + +class Converters { + @TypeConverter + fun dateFromTimestamp(value: Long?): Date? { + return value?.let { Date(it) } + } + + @TypeConverter + fun dateToTimestamp(date: Date?): Long? { + return date?.time + } +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/data/local/DBConstants.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/data/local/DBConstants.kt new file mode 100644 index 0000000..57c0b6e --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/data/local/DBConstants.kt @@ -0,0 +1,12 @@ +package com.nagpal.shivam.vtucslab.data.local + +object Tables { + const val LAB_RESPONSE = "lab_response" +} + +object LabResponseAttributes { + const val URL = "url" + const val RESPONSE = "response" + const val RESPONSE_TYPE = "response_type" + const val FETCHED_AT = "fetched_at" +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/data/local/LabResponse.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/data/local/LabResponse.kt new file mode 100644 index 0000000..f0a9ea4 --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/data/local/LabResponse.kt @@ -0,0 +1,19 @@ +package com.nagpal.shivam.vtucslab.data.local + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.nagpal.shivam.vtucslab.data.local.LabResponseAttributes.FETCHED_AT +import com.nagpal.shivam.vtucslab.data.local.LabResponseAttributes.RESPONSE +import com.nagpal.shivam.vtucslab.data.local.LabResponseAttributes.RESPONSE_TYPE +import com.nagpal.shivam.vtucslab.data.local.LabResponseAttributes.URL +import com.nagpal.shivam.vtucslab.data.local.Tables.LAB_RESPONSE +import java.util.* + +@Entity(LAB_RESPONSE) +data class LabResponse( + @PrimaryKey @ColumnInfo(name = URL) val url: String, + @ColumnInfo(name = RESPONSE) val response: String, + @ColumnInfo(name = RESPONSE_TYPE) val responseType: LabResponseType, + @ColumnInfo(name = FETCHED_AT) val fetchedAt: Date, +) diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/data/local/LabResponseDao.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/data/local/LabResponseDao.kt new file mode 100644 index 0000000..f8d7b74 --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/data/local/LabResponseDao.kt @@ -0,0 +1,16 @@ +package com.nagpal.shivam.vtucslab.data.local + +import androidx.room.Dao +import androidx.room.Query +import androidx.room.Upsert +import com.nagpal.shivam.vtucslab.data.local.LabResponseAttributes.URL +import com.nagpal.shivam.vtucslab.data.local.Tables.LAB_RESPONSE + +@Dao +interface LabResponseDao { + @Upsert + fun upsert(labResponse: LabResponse) + + @Query("SELECT * FROM $LAB_RESPONSE where $URL = :url") + fun findByUrl(url: String): LabResponse? +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/data/local/LabResponseType.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/data/local/LabResponseType.kt new file mode 100644 index 0000000..90347b9 --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/data/local/LabResponseType.kt @@ -0,0 +1,7 @@ +package com.nagpal.shivam.vtucslab.data.local + +enum class LabResponseType { + LABORATORY, + EXPERIMENT, + CONTENT, +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/models/ContentFile.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/models/ContentFile.kt new file mode 100644 index 0000000..664538a --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/models/ContentFile.kt @@ -0,0 +1,5 @@ +package com.nagpal.shivam.vtucslab.models + +data class ContentFile( + var fileName: String, +) diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/models/LabExperiment.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/models/LabExperiment.kt new file mode 100644 index 0000000..92b3ccb --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/models/LabExperiment.kt @@ -0,0 +1,6 @@ +package com.nagpal.shivam.vtucslab.models + +data class LabExperiment( + var serialOrder: String, + var labExperimentSubParts: List, +) diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/models/LabExperimentSubPart.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/models/LabExperimentSubPart.kt new file mode 100644 index 0000000..80d2c34 --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/models/LabExperimentSubPart.kt @@ -0,0 +1,6 @@ +package com.nagpal.shivam.vtucslab.models + +data class LabExperimentSubPart( + var subSerialOrder: String?, + var contentFiles: List, +) diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/models/Laboratory.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/models/Laboratory.kt new file mode 100644 index 0000000..3c8cc29 --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/models/Laboratory.kt @@ -0,0 +1,6 @@ +package com.nagpal.shivam.vtucslab.models + +data class Laboratory( + var title: String?, + var fileName: String, +) diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/models/LaboratoryExperimentResponse.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/models/LaboratoryExperimentResponse.kt new file mode 100644 index 0000000..3611981 --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/models/LaboratoryExperimentResponse.kt @@ -0,0 +1,14 @@ +package com.nagpal.shivam.vtucslab.models + +import com.fasterxml.jackson.annotation.JsonAlias +import com.nagpal.shivam.vtucslab.utilities.Constants + +data class LaboratoryExperimentResponse( + @field:JsonAlias(Constants.GITHUB_RAW_CONTENT) val githubRawContent: String, + val organization: String, + val repository: String, + val branch: String, + val isValid: Boolean = true, + val labExperiments: List, + val invalidationMessage: String? = null, +) diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/models/LaboratoryResponse.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/models/LaboratoryResponse.kt new file mode 100644 index 0000000..f41cc2e --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/models/LaboratoryResponse.kt @@ -0,0 +1,14 @@ +package com.nagpal.shivam.vtucslab.models + +import com.fasterxml.jackson.annotation.JsonAlias +import com.nagpal.shivam.vtucslab.utilities.Constants + +data class LaboratoryResponse( + @field:JsonAlias(Constants.GITHUB_RAW_CONTENT) val githubRawContent: String, + val organization: String, + val repository: String, + val branch: String, + val isValid: Boolean = true, + val laboratories: List, + val invalidationMessage: String? = null, +) diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/repositories/VtuCsLabRepository.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/repositories/VtuCsLabRepository.kt new file mode 100644 index 0000000..9e4c62c --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/repositories/VtuCsLabRepository.kt @@ -0,0 +1,24 @@ +package com.nagpal.shivam.vtucslab.repositories + +import com.nagpal.shivam.vtucslab.core.ErrorType +import com.nagpal.shivam.vtucslab.core.Resource +import com.nagpal.shivam.vtucslab.models.LaboratoryExperimentResponse +import com.nagpal.shivam.vtucslab.models.LaboratoryResponse +import kotlinx.coroutines.flow.Flow + +interface VtuCsLabRepository { + fun fetchLaboratories( + url: String, + forceRefresh: Boolean, + ): Flow> + + fun fetchExperiments( + url: String, + forceRefresh: Boolean, + ): Flow> + + fun fetchContent( + url: String, + forceRefresh: Boolean, + ): Flow> +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/repositories/VtuCsLabRepositoryImpl.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/repositories/VtuCsLabRepositoryImpl.kt new file mode 100644 index 0000000..4017951 --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/repositories/VtuCsLabRepositoryImpl.kt @@ -0,0 +1,172 @@ +package com.nagpal.shivam.vtucslab.repositories + +import android.app.Application +import com.fasterxml.jackson.core.JsonParseException +import com.fasterxml.jackson.databind.json.JsonMapper +import com.nagpal.shivam.vtucslab.core.ErrorType +import com.nagpal.shivam.vtucslab.core.Resource +import com.nagpal.shivam.vtucslab.data.local.LabResponse +import com.nagpal.shivam.vtucslab.data.local.LabResponseDao +import com.nagpal.shivam.vtucslab.data.local.LabResponseType +import com.nagpal.shivam.vtucslab.models.LaboratoryExperimentResponse +import com.nagpal.shivam.vtucslab.models.LaboratoryResponse +import com.nagpal.shivam.vtucslab.retrofit.ApiResult +import com.nagpal.shivam.vtucslab.services.VtuCsLabService +import com.nagpal.shivam.vtucslab.utilities.Configurations +import com.nagpal.shivam.vtucslab.utilities.NetworkUtils +import com.nagpal.shivam.vtucslab.utilities.StaticMethods +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.flow +import java.util.Date + +private val LOG_TAG: String = VtuCsLabRepositoryImpl::class.java.name + +class VtuCsLabRepositoryImpl( + private val application: Application, + private val vtuCsLabService: VtuCsLabService, + private val labResponseDao: LabResponseDao, + private val jsonMapper: JsonMapper, +) : VtuCsLabRepository { + override fun fetchLaboratories( + url: String, + forceRefresh: Boolean + ): Flow> = + flow { + fetch( + flow = this, + url, + LabResponseType.LABORATORY, + vtuCsLabService::getLaboratoryResponse, + { data -> jsonMapper.writeValueAsString(data) }, + { stringContent -> + jsonMapper.readValue( + stringContent, + LaboratoryResponse::class.java + ) + }, + forceRefresh, + ) + } + + override fun fetchExperiments( + url: String, + forceRefresh: Boolean + ): Flow> = + flow { + fetch( + flow = this, + url, + LabResponseType.EXPERIMENT, + vtuCsLabService::getLaboratoryExperimentsResponse, + { data -> jsonMapper.writeValueAsString(data) }, + { stringContent -> + jsonMapper.readValue( + stringContent, + LaboratoryExperimentResponse::class.java + ) + }, + forceRefresh, + ) + } + + override fun fetchContent( + url: String, + forceRefresh: Boolean + ): Flow> = flow { + fetch( + flow = this, + url, + LabResponseType.CONTENT, + vtuCsLabService::fetchRawResponse, + { stringContent -> stringContent }, + { stringContent -> stringContent }, + forceRefresh, + ) + } + + private suspend fun fetch( + flow: FlowCollector>, + url: String, + labResponseType: LabResponseType, + fetchFromNetwork: suspend (String) -> ApiResult, + encodeToString: (D) -> String, + decodeFromString: (String) -> D, + forceRefresh: Boolean, + ) { + flow.emit(Resource.Loading()) + + var foundInDB = false + if (!forceRefresh) { + val labResponse = labResponseDao.findByUrl(url) + labResponse?.let { + try { + flow.emit(Resource.Success(decodeFromString.invoke(it.response))) + foundInDB = true + if (it.fetchedAt.after( + StaticMethods.getCurrentDateMinusSeconds(Configurations.RESPONSE_FRESHNESS_TIME) + ) + ) { + return + } + } catch (_: JsonParseException) { + } + } + } + + if (!NetworkUtils.isNetworkConnected(application)) { + emitNetworkErrors( + flow, + forceRefresh, + foundInDB, + Resource.Error(ErrorType.NoActiveInternetConnection), + ) + return + } + when (val apiResult = fetchFromNetwork.invoke(url)) { + is ApiResult.ApiSuccess -> { + val data = apiResult.data + labResponseDao.upsert( + LabResponse( + url, + encodeToString(data), + labResponseType, + Date(), + ) + ) + flow.emit(Resource.Success(data)) + } + + is ApiResult.ApiError -> { + StaticMethods.logNetworkResultError(LOG_TAG, url, apiResult.code, apiResult.message) + emitNetworkErrors( + flow, + forceRefresh, + foundInDB, + Resource.Error(ErrorType.SomeErrorOccurred), + ) + } + + is ApiResult.ApiException -> { + StaticMethods.logNetworkResultException(LOG_TAG, url, apiResult.throwable) + emitNetworkErrors( + flow, + forceRefresh, + foundInDB, + Resource.Error(ErrorType.SomeErrorOccurred), + ) + } + } + } + + private suspend fun emitNetworkErrors( + flow: FlowCollector>, + forceRefresh: Boolean, + foundInDB: Boolean, + errorResource: Resource.Error + ) { + if (forceRefresh || !foundInDB) { + flow.emit(errorResource) + } + } +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/retrofit/ApiResult.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/retrofit/ApiResult.kt new file mode 100644 index 0000000..641baf2 --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/retrofit/ApiResult.kt @@ -0,0 +1,7 @@ +package com.nagpal.shivam.vtucslab.retrofit + +sealed class ApiResult { + class ApiSuccess(val data: T) : ApiResult() + class ApiError(val code: Int, val message: String?) : ApiResult() + class ApiException(val throwable: Throwable) : ApiResult() +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/retrofit/ApiResultCall.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/retrofit/ApiResultCall.kt new file mode 100644 index 0000000..27cd0c8 --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/retrofit/ApiResultCall.kt @@ -0,0 +1,39 @@ +package com.nagpal.shivam.vtucslab.retrofit + +import com.nagpal.shivam.vtucslab.retrofit.ApiResult.ApiException +import okhttp3.Request +import okio.Timeout +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + +class ApiResultCall(private val proxy: Call) : Call> { + override fun enqueue(callback: Callback>) { + proxy.enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + val apiResult = handleApiResult { response } + callback.onResponse(this@ApiResultCall, Response.success(apiResult)) + } + + override fun onFailure(call: Call, t: Throwable) { + val apiResult = ApiException(t) + callback.onResponse(this@ApiResultCall, Response.success(apiResult)) + } + }) + } + + override fun execute(): Response> = throw NotImplementedError() + + override fun clone(): Call> = ApiResultCall(proxy.clone()) + + override fun isExecuted(): Boolean = proxy.isExecuted + + override fun cancel() = proxy.cancel() + + override fun isCanceled(): Boolean = proxy.isCanceled + + override fun request(): Request = proxy.request() + + override fun timeout(): Timeout = proxy.timeout() + +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/retrofit/ApiResultCallAdapter.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/retrofit/ApiResultCallAdapter.kt new file mode 100644 index 0000000..5778538 --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/retrofit/ApiResultCallAdapter.kt @@ -0,0 +1,15 @@ +package com.nagpal.shivam.vtucslab.retrofit + +import retrofit2.Call +import retrofit2.CallAdapter +import java.lang.reflect.Type + +class ApiResultCallAdapter(private val resultType: Type) : + CallAdapter>> { + override fun responseType(): Type = resultType + + override fun adapt(call: Call): Call> { + return ApiResultCall(call) + } + +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/retrofit/ApiResultCallAdapterFactory.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/retrofit/ApiResultCallAdapterFactory.kt new file mode 100644 index 0000000..0bbe9fc --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/retrofit/ApiResultCallAdapterFactory.kt @@ -0,0 +1,31 @@ +package com.nagpal.shivam.vtucslab.retrofit + +import retrofit2.Call +import retrofit2.CallAdapter +import retrofit2.Retrofit +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type + +class ApiResultCallAdapterFactory private constructor() : CallAdapter.Factory() { + override fun get( + returnType: Type, + annotations: Array, + retrofit: Retrofit + ): CallAdapter<*, *>? { + if (getRawType(returnType) != Call::class.java) { + return null + } + + val callType = getParameterUpperBound(0, returnType as ParameterizedType) + if (getRawType(callType) != ApiResult::class.java) { + return null + } + + val resultType = getParameterUpperBound(0, callType as ParameterizedType) + return ApiResultCallAdapter(resultType) + } + + companion object { + fun create(): ApiResultCallAdapterFactory = ApiResultCallAdapterFactory() + } +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/retrofit/RetrofitConfigurations.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/retrofit/RetrofitConfigurations.kt new file mode 100644 index 0000000..ff865c4 --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/retrofit/RetrofitConfigurations.kt @@ -0,0 +1,37 @@ +package com.nagpal.shivam.vtucslab.retrofit + +import com.nagpal.shivam.vtucslab.retrofit.ApiResult.ApiError +import com.nagpal.shivam.vtucslab.retrofit.ApiResult.ApiException +import com.nagpal.shivam.vtucslab.retrofit.ApiResult.ApiSuccess +import com.nagpal.shivam.vtucslab.utilities.StaticMethods +import retrofit2.HttpException +import retrofit2.Response +import retrofit2.Retrofit +import retrofit2.converter.jackson.JacksonConverterFactory +import retrofit2.converter.scalars.ScalarsConverterFactory + + +fun getRetrofitBuilder(): Retrofit.Builder { + return Retrofit.Builder() + .addConverterFactory(ScalarsConverterFactory.create()) + .addConverterFactory(JacksonConverterFactory.create(StaticMethods.jsonMapper)) + .addCallAdapterFactory(ApiResultCallAdapterFactory.create()) +} + +fun handleApiResult( + execute: () -> Response +): ApiResult { + return try { + val response = execute() + val body = response.body() + if (response.isSuccessful && body != null) { + ApiSuccess(body) + } else { + ApiError(code = response.code(), message = response.message()) + } + } catch (e: HttpException) { + ApiError(code = e.code(), message = e.message()) + } catch (e: Throwable) { + ApiException(e) + } +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/screens/ContentState.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/screens/ContentState.kt new file mode 100644 index 0000000..5b3fbce --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/screens/ContentState.kt @@ -0,0 +1,11 @@ +package com.nagpal.shivam.vtucslab.screens + +import com.nagpal.shivam.vtucslab.core.UIMessage + +data class ContentState( + val stage: String, + val data: T? = null, + val errorMessage: UIMessage? = null, + val toast: UIMessage? = null, + val baseUrl: String? = null, +) diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/screens/EventEmitter.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/screens/EventEmitter.kt new file mode 100644 index 0000000..8f784a9 --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/screens/EventEmitter.kt @@ -0,0 +1,5 @@ +package com.nagpal.shivam.vtucslab.screens + +interface EventEmitter { + fun onEvent(event: T) +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/screens/UiEvent.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/screens/UiEvent.kt new file mode 100644 index 0000000..7197d72 --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/screens/UiEvent.kt @@ -0,0 +1,7 @@ +package com.nagpal.shivam.vtucslab.screens + +sealed class UiEvent { + class LoadContent(val url: String) : UiEvent() + class RefreshContent(val url: String) : UiEvent() + object ResetToast : UiEvent() +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/screens/Utils.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/screens/Utils.kt new file mode 100644 index 0000000..f0bf5ad --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/screens/Utils.kt @@ -0,0 +1,111 @@ +package com.nagpal.shivam.vtucslab.screens + +import android.content.Context +import android.widget.Toast +import com.nagpal.shivam.vtucslab.R +import com.nagpal.shivam.vtucslab.core.ErrorType +import com.nagpal.shivam.vtucslab.core.Resource +import com.nagpal.shivam.vtucslab.core.UIMessage +import com.nagpal.shivam.vtucslab.core.UIMessageType +import com.nagpal.shivam.vtucslab.utilities.Stages +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +object Utils { + fun loadContent( + uiStateFlow: MutableStateFlow>, + fetchJob: Job?, + viewModelScope: CoroutineScope, + fetchExecutable: (String, Boolean) -> Flow>, + getBaseUrl: (T) -> String?, + url: String, + forceRefresh: Boolean, + ): Job? { + if (!forceRefresh && uiStateFlow.value.stage == Stages.SUCCEEDED) { + return fetchJob + } + + fetchJob?.cancel() + return viewModelScope.launch(Dispatchers.IO) { + fetchExecutable.invoke(url, forceRefresh) + .onEach { resource -> + when (resource) { + is Resource.Loading -> { + uiStateFlow.update { + uiStateFlow.value.copy(stage = Stages.LOADING) + } + } + + is Resource.Success -> { + uiStateFlow.update { + ContentState( + Stages.SUCCEEDED, + data = resource.data, + baseUrl = getBaseUrl(resource.data!!), + ) + } + } + + is Resource.Error -> { + uiStateFlow.update { + val uiMessage: UIMessage = when (resource.error) { + ErrorType.NoActiveInternetConnection -> if (forceRefresh) UIMessage( + UIMessageType.NoActiveInternetConnection + ) else UIMessage(UIMessageType.NoActiveInternetConnectionDetailed) + + ErrorType.SomeErrorOccurred -> UIMessage(UIMessageType.SomeErrorOccurred) + } + if (forceRefresh) { + uiStateFlow.value.copy( + stage = if (uiStateFlow.value.data != null) Stages.SUCCEEDED else Stages.FAILED, + toast = uiMessage, + ) + } else { + ContentState( + stage = Stages.FAILED, + errorMessage = uiMessage, + ) + } + + } + } + } + }.launchIn(this) + } + } + + fun UIMessage.asString(context: Context): String { + return when (this.messageType) { + UIMessageType.NoActiveInternetConnectionDetailed -> context.getString(R.string.no_internet_connection_detailed) + UIMessageType.SomeErrorOccurred -> context.getString(R.string.error_occurred) + UIMessageType.NoActiveInternetConnection -> context.getString(R.string.no_internet_connection) + } + } + + fun showToast( + context: Context, + toast: Toast?, + toastUIMessage: UIMessage?, + eventEmitter: EventEmitter, + event: T + ): Toast? { + return toastUIMessage?.let { uiMessage -> + toast?.cancel() + val newToast = Toast.makeText( + context, + uiMessage.asString(context), + Toast.LENGTH_LONG + ) + newToast.show() + eventEmitter.onEvent(event) + newToast + } ?: toast + } +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/screens/display/DisplayFragment.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/screens/display/DisplayFragment.kt new file mode 100644 index 0000000..2d01a68 --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/screens/display/DisplayFragment.kt @@ -0,0 +1,157 @@ +package com.nagpal.shivam.vtucslab.screens.display + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.core.view.MenuProvider +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.navigation.fragment.navArgs +import com.nagpal.shivam.vtucslab.R +import com.nagpal.shivam.vtucslab.databinding.FragmentDisplayBinding +import com.nagpal.shivam.vtucslab.screens.UiEvent +import com.nagpal.shivam.vtucslab.screens.Utils +import com.nagpal.shivam.vtucslab.screens.Utils.asString +import com.nagpal.shivam.vtucslab.utilities.Constants +import com.nagpal.shivam.vtucslab.utilities.Stages +import kotlinx.coroutines.launch + + +class DisplayFragment : Fragment() { + + private var _binding: FragmentDisplayBinding? = null + private val binding get() = _binding!! + private val displayFragmentArgs by navArgs() + private val viewModel: DisplayViewModel by viewModels { DisplayViewModel.Factory } + private var toast: Toast? = null + + private val url: String by lazy { + return@lazy "${displayFragmentArgs.baseUrl}/${displayFragmentArgs.fileName}" + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentDisplayBinding.inflate(inflater, container, false) + setupMenuProvider() + setupViews() + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.uiState.collect { + binding.emptyTextView.visibility = View.GONE + toast = Utils.showToast( + requireContext(), + toast, + it.toast, + viewModel, + UiEvent.ResetToast + ) + + if (it.stage != Stages.LOADING) { + binding.swipeRefresh.isRefreshing = false + } + + when (it.stage) { + Stages.LOADING -> { + binding.swipeRefresh.isRefreshing = true + } + + Stages.SUCCEEDED -> { + binding.displayTextView.text = it.data + requireActivity().invalidateOptionsMenu() + Handler(Looper.getMainLooper()).postDelayed({ + binding.horizontalScroll.scrollX = viewModel.scrollX + binding.verticalScroll.scrollY = viewModel.scrollY + }, 500) + } + + Stages.FAILED -> { + it.errorMessage?.let { uiMessage -> + showErrorMessage(uiMessage.asString(requireContext())) + } + } + } + } + } + } + + return binding.root + } + + private fun setupViews() { + binding.swipeRefresh.setOnRefreshListener { + viewModel.onEvent(UiEvent.RefreshContent(url)) + } + } + + private fun showErrorMessage(message: String) { + binding.emptyTextView.visibility = View.VISIBLE + binding.emptyTextView.text = message + } + + private fun setupMenuProvider() { + requireActivity().addMenuProvider(object : MenuProvider { + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.menu_display_fragment, menu) + if (viewModel.uiState.value.stage == Stages.SUCCEEDED) { + menu.findItem(R.id.menu_item_copy_display_activity).isEnabled = true + } + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return when (menuItem.itemId) { + R.id.menu_item_refresh -> { + viewModel.onEvent(UiEvent.RefreshContent(url)) + return true + } + + R.id.menu_item_copy_display_activity -> { + + val clipboard = + requireActivity().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clipData = ClipData( + ClipData.newPlainText( + Constants.LABEL_CODE, + viewModel.uiState.value.data + ) + ) + clipboard.setPrimaryClip(clipData) + Toast.makeText( + requireContext(), + getString(R.string.code_copied_to_clipboard), + Toast.LENGTH_SHORT + ).show() + return true + } + + else -> false + } + } + }, viewLifecycleOwner) + } + + override fun onResume() { + super.onResume() + viewModel.onEvent(UiEvent.LoadContent(url)) + } + + override fun onSaveInstanceState(outState: Bundle) { + viewModel.scrollX = binding.horizontalScroll.scrollX + viewModel.scrollY = binding.verticalScroll.scrollY + super.onSaveInstanceState(outState) + } +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/screens/display/DisplayViewModel.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/screens/display/DisplayViewModel.kt new file mode 100644 index 0000000..59ea723 --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/screens/display/DisplayViewModel.kt @@ -0,0 +1,73 @@ +package com.nagpal.shivam.vtucslab.screens.display + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory +import com.nagpal.shivam.vtucslab.VTUCSLabApplication +import com.nagpal.shivam.vtucslab.repositories.VtuCsLabRepository +import com.nagpal.shivam.vtucslab.screens.ContentState +import com.nagpal.shivam.vtucslab.screens.EventEmitter +import com.nagpal.shivam.vtucslab.screens.UiEvent +import com.nagpal.shivam.vtucslab.screens.Utils +import com.nagpal.shivam.vtucslab.utilities.Stages +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +class DisplayViewModel( + application: Application, + private val vtuCsLabRepository: VtuCsLabRepository +) : AndroidViewModel(application), EventEmitter { + var scrollX = 0 + var scrollY = 0 + private val initialState = ContentState(Stages.LOADING) + + private val _uiState = MutableStateFlow(initialState) + val uiState = _uiState.asStateFlow() + private var fetchJob: Job? = null + + override fun onEvent(event: UiEvent) { + when (event) { + is UiEvent.LoadContent -> { + loadContent(event.url) + } + + is UiEvent.RefreshContent -> { + loadContent(event.url, true) + } + + UiEvent.ResetToast -> { + _uiState.update { _uiState.value.copy(toast = null) } + } + } + } + + private fun loadContent(url: String, forceRefresh: Boolean = false) { + fetchJob = Utils.loadContent( + _uiState, + fetchJob, + viewModelScope, + { urlArg, forceRefreshArg -> vtuCsLabRepository.fetchContent(urlArg, forceRefreshArg) }, + { null }, + url, + forceRefresh, + ) + } + + companion object { + val Factory: ViewModelProvider.Factory = viewModelFactory { + initializer { + val vtuCsLabApplication = this[APPLICATION_KEY] as VTUCSLabApplication + DisplayViewModel( + vtuCsLabApplication, + vtuCsLabApplication.vtuCsLabRepository + ) + } + } + } +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/screens/programs/ProgramFragment.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/screens/programs/ProgramFragment.kt new file mode 100644 index 0000000..ae78d69 --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/screens/programs/ProgramFragment.kt @@ -0,0 +1,158 @@ +package com.nagpal.shivam.vtucslab.screens.programs + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.core.view.MenuProvider +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import androidx.recyclerview.widget.LinearLayoutManager +import com.nagpal.shivam.vtucslab.R +import com.nagpal.shivam.vtucslab.adapters.ContentAdapter +import com.nagpal.shivam.vtucslab.databinding.FragmentProgramBinding +import com.nagpal.shivam.vtucslab.models.ContentFile +import com.nagpal.shivam.vtucslab.screens.UiEvent +import com.nagpal.shivam.vtucslab.screens.Utils +import com.nagpal.shivam.vtucslab.screens.Utils.asString +import com.nagpal.shivam.vtucslab.utilities.Stages +import kotlinx.coroutines.launch + + +class ProgramFragment : Fragment() { + private var _binding: FragmentProgramBinding? = null + private val binding get() = _binding!! + + private val viewModel: ProgramViewModel by viewModels { ProgramViewModel.Factory } + private lateinit var contentAdapter: ContentAdapter + + private val programFragmentArgs by navArgs() + private var toast: Toast? = null + + private val url: String by lazy { + return@lazy "${programFragmentArgs.baseUrl}/${programFragmentArgs.fileName}" + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentProgramBinding.inflate(inflater, container, false) + setupMenuProvider() + setupViews() + setupRepositoryAdapter() + + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.uiState.collect { + binding.emptyTextView.visibility = View.GONE + toast = Utils.showToast( + requireContext(), + toast, + it.toast, + viewModel, + UiEvent.ResetToast + ) + + if (it.stage != Stages.LOADING) { + binding.swipeRefresh.isRefreshing = false + } + + when (it.stage) { + Stages.LOADING -> { + binding.swipeRefresh.isRefreshing = true + } + + Stages.SUCCEEDED -> { + if (it.data!!.isValid) { + contentAdapter.clear() + contentAdapter.addAll(it.data.labExperiments) + } else { + // TODO: Handle this logic in Data Layer + showErrorMessage(it.data.invalidationMessage) + } + } + + Stages.FAILED -> { + it.errorMessage?.let { uiMessage -> + showErrorMessage(uiMessage.asString(requireContext())) + } + } + } + } + } + } + + return binding.root + } + + // TODO: Duplicate: Move to a static method + private fun showErrorMessage(message: String?) { + binding.emptyTextView.visibility = View.VISIBLE + binding.emptyTextView.text = message + } + + private fun setupMenuProvider() { + requireActivity().addMenuProvider(object : MenuProvider { + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.menu_program_fragment, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return when (menuItem.itemId) { + R.id.menu_item_refresh -> { + viewModel.onEvent(UiEvent.RefreshContent(url)) + true + } + + else -> false + } + } + }, viewLifecycleOwner) + } + + private fun setupRepositoryAdapter() { + contentAdapter = ContentAdapter(requireContext(), ArrayList()) + contentAdapter.setItemClickHandler(object : ContentAdapter.ItemClickHandler { + override fun onContentFileClick(file: ContentFile) { + val actionProgramFragmentToDisplayFragment = + ProgramFragmentDirections.actionProgramFragmentToDisplayFragment( + viewModel.uiState.value.baseUrl!!, + file.fileName, + file.fileName + ) + findNavController().navigate(actionProgramFragmentToDisplayFragment) + } + + }) + binding.recyclerView.adapter = contentAdapter + } + + private fun setupViews() { + binding.recyclerView.layoutManager = + LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false) + binding.recyclerView.setHasFixedSize(true) + binding.swipeRefresh.setOnRefreshListener { + viewModel.onEvent(UiEvent.RefreshContent(url)) + } + } + + override fun onResume() { + super.onResume() + viewModel.onEvent(UiEvent.LoadContent(url)) + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/screens/programs/ProgramViewModel.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/screens/programs/ProgramViewModel.kt new file mode 100644 index 0000000..e6b32c3 --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/screens/programs/ProgramViewModel.kt @@ -0,0 +1,80 @@ +package com.nagpal.shivam.vtucslab.screens.programs + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory +import com.nagpal.shivam.vtucslab.VTUCSLabApplication +import com.nagpal.shivam.vtucslab.models.LaboratoryExperimentResponse +import com.nagpal.shivam.vtucslab.repositories.VtuCsLabRepository +import com.nagpal.shivam.vtucslab.screens.ContentState +import com.nagpal.shivam.vtucslab.screens.EventEmitter +import com.nagpal.shivam.vtucslab.screens.UiEvent +import com.nagpal.shivam.vtucslab.screens.Utils +import com.nagpal.shivam.vtucslab.utilities.Stages +import com.nagpal.shivam.vtucslab.utilities.StaticMethods +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +class ProgramViewModel( + application: Application, + private val vtuCsLabRepository: VtuCsLabRepository +) : AndroidViewModel(application), EventEmitter { + private val initialState = + ContentState(Stages.LOADING) + private val _uiState = MutableStateFlow(initialState) + val uiState: StateFlow> = _uiState.asStateFlow() + private var fetchJob: Job? = null + + + override fun onEvent(event: UiEvent) { + when (event) { + is UiEvent.LoadContent -> { + loadContent(event.url) + } + + is UiEvent.RefreshContent -> { + loadContent(event.url, true) + } + + UiEvent.ResetToast -> { + _uiState.update { _uiState.value.copy(toast = null) } + } + } + } + + private fun loadContent(url: String, forceRefresh: Boolean = false) { + fetchJob = Utils.loadContent( + _uiState, + fetchJob, + viewModelScope, + { urlArg, forceRefreshArg -> + vtuCsLabRepository.fetchExperiments( + urlArg, + forceRefreshArg + ) + }, + { StaticMethods.getBaseURL(it) }, + url, + forceRefresh, + ) + } + + companion object { + val Factory: ViewModelProvider.Factory = viewModelFactory { + initializer { + val vtuCsLabApplication = this[APPLICATION_KEY] as VTUCSLabApplication + ProgramViewModel( + vtuCsLabApplication, + vtuCsLabApplication.vtuCsLabRepository + ) + } + } + } +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/screens/repository/RepositoryFragment.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/screens/repository/RepositoryFragment.kt new file mode 100644 index 0000000..9491fdf --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/screens/repository/RepositoryFragment.kt @@ -0,0 +1,162 @@ +package com.nagpal.shivam.vtucslab.screens.repository + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.core.view.MenuProvider +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import com.nagpal.shivam.vtucslab.R +import com.nagpal.shivam.vtucslab.adapters.NavigationAdapter +import com.nagpal.shivam.vtucslab.databinding.FragmentRepositoryBinding +import com.nagpal.shivam.vtucslab.models.Laboratory +import com.nagpal.shivam.vtucslab.screens.UiEvent +import com.nagpal.shivam.vtucslab.screens.Utils +import com.nagpal.shivam.vtucslab.screens.Utils.asString +import com.nagpal.shivam.vtucslab.utilities.Constants +import com.nagpal.shivam.vtucslab.utilities.Stages +import kotlinx.coroutines.launch + +class RepositoryFragment : Fragment() { + private var _binding: FragmentRepositoryBinding? = null + private lateinit var navigationAdapter: NavigationAdapter + + private val binding get() = _binding!! + private val viewModel: RepositoryViewModel by viewModels { RepositoryViewModel.Factory } + private val url = Constants.INDEX_REPOSITORY_URL + + private var toast: Toast? = null + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentRepositoryBinding.inflate(inflater, container, false) + setupMenuProvider() + setupViews() + setupRepositoryAdapter() + + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.uiState.collect { + binding.emptyTextView.visibility = View.GONE + toast = Utils.showToast( + requireContext(), + toast, + it.toast, + viewModel, + UiEvent.ResetToast + ) + + if (it.stage != Stages.LOADING) { + binding.swipeRefresh.isRefreshing = false + } + + when (it.stage) { + Stages.LOADING -> { + binding.swipeRefresh.isRefreshing = true + } + + Stages.SUCCEEDED -> { + if (it.data!!.isValid) { + navigationAdapter.clear() + navigationAdapter.addAll(it.data.laboratories) + } else { + // TODO: Handle this logic in Data Layer + showErrorMessage(it.data.invalidationMessage) + } + } + + Stages.FAILED -> { + it.errorMessage?.let { uiMessage -> + showErrorMessage(uiMessage.asString(requireContext())) + } + } + } + } + } + } + return binding.root + } + + // TODO: Duplicate: Move to a static method + private fun showErrorMessage(message: String?) { + binding.emptyTextView.visibility = View.VISIBLE + binding.emptyTextView.text = message + } + + private fun setupViews() { + binding.repositoryRecyclerView.layoutManager = + LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false) + binding.repositoryRecyclerView.setHasFixedSize(true) + binding.swipeRefresh.setOnRefreshListener { + viewModel.onEvent(UiEvent.RefreshContent(url)) + } + } + + override fun onResume() { + super.onResume() + viewModel.onEvent(UiEvent.LoadContent(url)) + } + + private fun setupRepositoryAdapter() { + navigationAdapter = NavigationAdapter(requireContext(), ArrayList()) + navigationAdapter.setNavigationAdapterItemClickHandler(object : + NavigationAdapter.NavigationAdapterItemClickHandler { + override fun onNavigationAdapterItemClick(laboratory: Laboratory, i: Int) { + val actionRepositoryFragmentToProgramFragment = + RepositoryFragmentDirections.actionRepositoryFragmentToProgramFragment( + viewModel.uiState.value.baseUrl!!, + laboratory.fileName, + laboratory.title.orEmpty(), + ) + findNavController().navigate(actionRepositoryFragmentToProgramFragment) + } + + }) + binding.repositoryRecyclerView.adapter = navigationAdapter + } + + private fun setupMenuProvider() { + requireActivity().addMenuProvider(object : MenuProvider { + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.menu_main_fragment, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return when (menuItem.itemId) { + R.id.menu_item_refresh -> { + viewModel.onEvent(UiEvent.RefreshContent(url)) + true + } + + R.id.main_menu_item_privacy -> { + val intent = Intent(Intent.ACTION_VIEW) + intent.data = Uri.parse(Constants.PRIVACY_POLICY_LINK) + startActivity(intent) + true + } + + else -> false + } + } + }, viewLifecycleOwner) + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/screens/repository/RepositoryViewModel.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/screens/repository/RepositoryViewModel.kt new file mode 100644 index 0000000..675e6e1 --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/screens/repository/RepositoryViewModel.kt @@ -0,0 +1,78 @@ +package com.nagpal.shivam.vtucslab.screens.repository + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory +import com.nagpal.shivam.vtucslab.VTUCSLabApplication +import com.nagpal.shivam.vtucslab.models.LaboratoryResponse +import com.nagpal.shivam.vtucslab.repositories.VtuCsLabRepository +import com.nagpal.shivam.vtucslab.screens.ContentState +import com.nagpal.shivam.vtucslab.screens.EventEmitter +import com.nagpal.shivam.vtucslab.screens.UiEvent +import com.nagpal.shivam.vtucslab.screens.Utils +import com.nagpal.shivam.vtucslab.utilities.Stages +import com.nagpal.shivam.vtucslab.utilities.StaticMethods +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +class RepositoryViewModel( + application: Application, + private val vtuCsLabRepository: VtuCsLabRepository +) : AndroidViewModel(application), EventEmitter { + private val initialState = ContentState(Stages.LOADING) + private val _uiState = MutableStateFlow(initialState) + val uiState: StateFlow> = _uiState.asStateFlow() + private var fetchJob: Job? = null + + override fun onEvent(event: UiEvent) { + when (event) { + is UiEvent.LoadContent -> { + loadContent(event.url) + } + + is UiEvent.RefreshContent -> { + loadContent(event.url, true) + } + + UiEvent.ResetToast -> { + _uiState.update { _uiState.value.copy(toast = null) } + } + } + } + + private fun loadContent(url: String, forceRefresh: Boolean = false) { + fetchJob = Utils.loadContent( + _uiState, + fetchJob, + viewModelScope, + { urlArg, forceRefreshArg -> + vtuCsLabRepository.fetchLaboratories( + urlArg, + forceRefreshArg + ) + }, + { StaticMethods.getBaseURL(it) }, + url, + forceRefresh, + ) + } + + companion object { + val Factory: ViewModelProvider.Factory = viewModelFactory { + initializer { + val vtuCsLabApplication = this[APPLICATION_KEY] as VTUCSLabApplication + RepositoryViewModel( + vtuCsLabApplication, + vtuCsLabApplication.vtuCsLabRepository + ) + } + } + } +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/services/VtuCsLabService.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/services/VtuCsLabService.kt new file mode 100644 index 0000000..8ddd4e3 --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/services/VtuCsLabService.kt @@ -0,0 +1,30 @@ +package com.nagpal.shivam.vtucslab.services + +import com.nagpal.shivam.vtucslab.models.LaboratoryExperimentResponse +import com.nagpal.shivam.vtucslab.models.LaboratoryResponse +import com.nagpal.shivam.vtucslab.retrofit.ApiResult +import com.nagpal.shivam.vtucslab.retrofit.getRetrofitBuilder +import com.nagpal.shivam.vtucslab.utilities.Constants +import retrofit2.http.GET +import retrofit2.http.Url + +interface VtuCsLabService { + + @GET + suspend fun getLaboratoryResponse(@Url url: String): ApiResult + + @GET + suspend fun getLaboratoryExperimentsResponse(@Url url: String): ApiResult + + @GET + suspend fun fetchRawResponse(@Url url: String): ApiResult + + companion object { + val instance: VtuCsLabService by lazy { + val retrofit = getRetrofitBuilder() + .baseUrl(Constants.GITHUB_RAW_BASE_URL) + .build() + return@lazy retrofit.create(VtuCsLabService::class.java) + } + } +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/utilities/Configurations.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/utilities/Configurations.kt new file mode 100644 index 0000000..60f0e7a --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/utilities/Configurations.kt @@ -0,0 +1,5 @@ +package com.nagpal.shivam.vtucslab.utilities + +object Configurations { + const val RESPONSE_FRESHNESS_TIME = 3 * 60 * 60 +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/utilities/Constants.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/utilities/Constants.kt new file mode 100644 index 0000000..378bab7 --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/utilities/Constants.kt @@ -0,0 +1,12 @@ +package com.nagpal.shivam.vtucslab.utilities + +object Constants { + const val GITHUB_RAW_BASE_URL = "https://raw.githubusercontent.com" + const val PRIVACY_POLICY_LINK = + "https://github.com/ShivamNagpal/Privacy_Policies/blob/master/VTU_CS_LAB_MANUAL.md" + const val INDEX_REPOSITORY_URL = + "https://raw.githubusercontent.com/vtucs/Index_v3/master/Index_v3.json" + const val LABEL_CODE = "Code" + const val GITHUB_RAW_CONTENT = "github_raw_content" + const val VTU_CS_LAB = "vtu_cs_lab" +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/utilities/NetworkUtils.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/utilities/NetworkUtils.kt new file mode 100644 index 0000000..3a8f086 --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/utilities/NetworkUtils.kt @@ -0,0 +1,37 @@ +package com.nagpal.shivam.vtucslab.utilities + +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import android.os.Build + +object NetworkUtils { + fun isNetworkConnected(context: Context): Boolean { + val connectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val networkCapabilities = connectivityManager.activeNetwork ?: return false + val activeNetwork = + connectivityManager.getNetworkCapabilities(networkCapabilities) ?: return false + return when { + activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true + activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true + activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true + else -> false + } + } else { //TODO: Remove this code post upgrading the minSdkVersion to 23 (https://stackoverflow.com/a/61152364) + connectivityManager.run { + connectivityManager.activeNetworkInfo?.run { + return when (type) { + ConnectivityManager.TYPE_WIFI -> true + ConnectivityManager.TYPE_MOBILE -> true + ConnectivityManager.TYPE_ETHERNET -> true + else -> false + } + + } + } + } + return false + } +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/utilities/Stages.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/utilities/Stages.kt new file mode 100644 index 0000000..29d2b07 --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/utilities/Stages.kt @@ -0,0 +1,7 @@ +package com.nagpal.shivam.vtucslab.utilities + +object Stages { + const val LOADING = "LOADING" + const val SUCCEEDED = "SUCCEEDED" + const val FAILED = "FAILED" +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/utilities/StaticMethods.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/utilities/StaticMethods.kt new file mode 100644 index 0000000..3c629aa --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/utilities/StaticMethods.kt @@ -0,0 +1,60 @@ +package com.nagpal.shivam.vtucslab.utilities + +import android.util.Log +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.json.JsonMapper +import com.nagpal.shivam.vtucslab.models.LaboratoryExperimentResponse +import com.nagpal.shivam.vtucslab.models.LaboratoryResponse +import java.util.Calendar +import java.util.Date + +object StaticMethods { + + val jsonMapper: JsonMapper by lazy { + com.fasterxml.jackson.module.kotlin.jacksonMapperBuilder() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .build() + } + + fun formatProgramName(programName: String): String { + return programName.replace('_', ' ') + } + + fun getBaseURL(labResponse: LaboratoryResponse): String { + return "${labResponse.githubRawContent}/${labResponse.organization}/${labResponse.repository}/${labResponse.branch}" + } + + fun getBaseURL(laboratoryExperimentResponse: LaboratoryExperimentResponse): String { + return "${laboratoryExperimentResponse.githubRawContent}/${laboratoryExperimentResponse.organization}/${laboratoryExperimentResponse.repository}/${laboratoryExperimentResponse.branch}" + } + + fun logNetworkResultError( + logTag: String, + url: String, + code: Int, + message: String? + ) { + Log.e( + logTag, + "Call to $url resulted in non-success response. Status Code: $code. Message: $message" + ) + } + + fun logNetworkResultException( + logTag: String, + url: String, + throwable: Throwable + ) { + Log.e( + logTag, + "Call to $url failed with exception: ${throwable.javaClass.name}", + throwable + ) + } + + fun getCurrentDateMinusSeconds(seconds: Int): Date { + val calendar = Calendar.getInstance() + calendar.add(Calendar.SECOND, -seconds) + return calendar.time + } +} diff --git a/app/src/main/res/drawable-hdpi/ic_action_arrow_back.png b/app/src/main/res/drawable-hdpi/ic_action_arrow_back.png deleted file mode 100644 index e2ff536..0000000 Binary files a/app/src/main/res/drawable-hdpi/ic_action_arrow_back.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_arrow_back.png b/app/src/main/res/drawable-mdpi/ic_action_arrow_back.png deleted file mode 100644 index 41de0b3..0000000 Binary files a/app/src/main/res/drawable-mdpi/ic_action_arrow_back.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_arrow_back.png b/app/src/main/res/drawable-xhdpi/ic_action_arrow_back.png deleted file mode 100644 index 7b69282..0000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_action_arrow_back.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_arrow_back.png b/app/src/main/res/drawable-xxhdpi/ic_action_arrow_back.png deleted file mode 100644 index 4d205b2..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_action_arrow_back.png and /dev/null differ diff --git a/app/src/main/res/layout/activity_display.xml b/app/src/main/res/layout/activity_display.xml deleted file mode 100644 index f351598..0000000 --- a/app/src/main/res/layout/activity_display.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..4909472 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/app/src/main/res/layout/activity_program.xml b/app/src/main/res/layout/activity_program.xml deleted file mode 100644 index 2d2cce2..0000000 --- a/app/src/main/res/layout/activity_program.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_repository.xml b/app/src/main/res/layout/activity_repository.xml deleted file mode 100644 index 40f7bdf..0000000 --- a/app/src/main/res/layout/activity_repository.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/layout/content_repository.xml b/app/src/main/res/layout/content_repository.xml deleted file mode 100644 index 69b70d3..0000000 --- a/app/src/main/res/layout/content_repository.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/coordinator_repository.xml b/app/src/main/res/layout/coordinator_repository.xml deleted file mode 100644 index c144304..0000000 --- a/app/src/main/res/layout/coordinator_repository.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_display.xml b/app/src/main/res/layout/fragment_display.xml new file mode 100644 index 0000000..b65cc87 --- /dev/null +++ b/app/src/main/res/layout/fragment_display.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_program.xml b/app/src/main/res/layout/fragment_program.xml new file mode 100644 index 0000000..b8d9ce3 --- /dev/null +++ b/app/src/main/res/layout/fragment_program.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_repository.xml b/app/src/main/res/layout/fragment_repository.xml new file mode 100644 index 0000000..6fd4262 --- /dev/null +++ b/app/src/main/res/layout/fragment_repository.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/header_repository.xml b/app/src/main/res/layout/header_repository.xml deleted file mode 100644 index 998d63c..0000000 --- a/app/src/main/res/layout/header_repository.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/layout_card_se_msp.xml b/app/src/main/res/layout/layout_card_se_msp.xml index 4b2c6e6..485935a 100644 --- a/app/src/main/res/layout/layout_card_se_msp.xml +++ b/app/src/main/res/layout/layout_card_se_msp.xml @@ -1,48 +1,45 @@ - + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content"> - + android:layout_height="wrap_content" + android:layout_marginStart="12dp" + android:layout_marginTop="6dp" + android:layout_marginEnd="12dp" + android:layout_marginBottom="6dp" + android:foreground="?attr/selectableItemBackground" + app:cardCornerRadius="4dp" + app:cardElevation="2dp"> - + android:padding="8dp"> - - - + android:background="@drawable/shape_background_number" + android:gravity="center" + android:textColor="@android:color/white" + android:textSize="24sp" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="1" /> - + - - - - \ No newline at end of file + + + diff --git a/app/src/main/res/layout/layout_card_se_ssp_mf.xml b/app/src/main/res/layout/layout_card_se_ssp_mf.xml index b0aea2d..688d99b 100644 --- a/app/src/main/res/layout/layout_card_se_ssp_mf.xml +++ b/app/src/main/res/layout/layout_card_se_ssp_mf.xml @@ -1,48 +1,45 @@ - + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content"> - + android:layout_height="wrap_content" + android:layout_marginStart="12dp" + android:layout_marginTop="6dp" + android:layout_marginEnd="12dp" + android:layout_marginBottom="6dp" + android:foreground="?attr/selectableItemBackground" + app:cardCornerRadius="4dp" + app:cardElevation="2dp"> - + android:padding="8dp"> - - - + android:background="@drawable/shape_background_number" + android:gravity="center" + android:textColor="@android:color/white" + android:textSize="24sp" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="1" /> - + - - - - \ No newline at end of file + + + diff --git a/app/src/main/res/layout/layout_card_se_ssp_sf.xml b/app/src/main/res/layout/layout_card_se_ssp_sf.xml index 3623cbf..ea09748 100644 --- a/app/src/main/res/layout/layout_card_se_ssp_sf.xml +++ b/app/src/main/res/layout/layout_card_se_ssp_sf.xml @@ -1,57 +1,54 @@ - + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content"> - + android:layout_height="wrap_content" + android:layout_marginStart="12dp" + android:layout_marginTop="6dp" + android:layout_marginEnd="12dp" + android:layout_marginBottom="6dp" + android:foreground="?attr/selectableItemBackground" + app:cardCornerRadius="4dp" + app:cardElevation="2dp"> - + android:padding="8dp"> - + android:background="@drawable/shape_background_number" + android:gravity="center" + android:textColor="@android:color/white" + android:textSize="24sp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="1" /> - - - + - - - - \ No newline at end of file + + + diff --git a/app/src/main/res/layout/layout_card_single_files_without_sub_parts.xml b/app/src/main/res/layout/layout_card_single_files_without_sub_parts.xml index 6242aad..6860d9a 100644 --- a/app/src/main/res/layout/layout_card_single_files_without_sub_parts.xml +++ b/app/src/main/res/layout/layout_card_single_files_without_sub_parts.xml @@ -1,46 +1,40 @@ - + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:foreground="?attr/selectableItemBackground"> - - - - + + + + - - - - - - - - - - \ No newline at end of file + android:layout_marginStart="16dp" + android:ellipsize="end" + android:singleLine="true" + android:textColor="@android:color/black" + android:textSize="18sp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toEndOf="@+id/bullet" + app:layout_constraintTop_toTopOf="parent" + tools:text="TextView" /> + + + diff --git a/app/src/main/res/layout/layout_card_single_sub_parts_with_files.xml b/app/src/main/res/layout/layout_card_single_sub_parts_with_files.xml index 44103ad..e695204 100644 --- a/app/src/main/res/layout/layout_card_single_sub_parts_with_files.xml +++ b/app/src/main/res/layout/layout_card_single_sub_parts_with_files.xml @@ -1,50 +1,47 @@ - - - + + + android:layout_height="wrap_content" + android:padding="8dp"> - - - + android:background="@drawable/shape_background_sub_part" + android:gravity="center" + android:textColor="@color/colorPrimary" + android:textSize="18sp" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="1" /> - + - + - - - \ No newline at end of file + + diff --git a/app/src/main/res/layout/layout_card_single_sub_parts_without_files.xml b/app/src/main/res/layout/layout_card_single_sub_parts_without_files.xml index 7f90368..2f747f9 100644 --- a/app/src/main/res/layout/layout_card_single_sub_parts_without_files.xml +++ b/app/src/main/res/layout/layout_card_single_sub_parts_without_files.xml @@ -1,45 +1,42 @@ - + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:foreground="?attr/selectableItemBackground"> - + android:padding="8dp"> - + android:background="@drawable/shape_background_sub_part" + android:gravity="center" + android:textColor="@color/colorPrimary" + android:textSize="18sp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="1" /> - - - + - - - \ No newline at end of file + + diff --git a/app/src/main/res/layout/layout_content_main_activity.xml b/app/src/main/res/layout/layout_content_main_activity.xml deleted file mode 100644 index b8f8617..0000000 --- a/app/src/main/res/layout/layout_content_main_activity.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/layout/layout_coordinator_main_activity.xml b/app/src/main/res/layout/layout_coordinator_main_activity.xml deleted file mode 100644 index d08577a..0000000 --- a/app/src/main/res/layout/layout_coordinator_main_activity.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/menu_display_activity.xml b/app/src/main/res/menu/menu_display_fragment.xml similarity index 83% rename from app/src/main/res/menu/menu_display_activity.xml rename to app/src/main/res/menu/menu_display_fragment.xml index 9d63dbd..3e7a6cf 100644 --- a/app/src/main/res/menu/menu_display_activity.xml +++ b/app/src/main/res/menu/menu_display_fragment.xml @@ -1,11 +1,11 @@ - \ No newline at end of file + diff --git a/app/src/main/res/menu/menu_main_activity.xml b/app/src/main/res/menu/menu_main_fragment.xml similarity index 69% rename from app/src/main/res/menu/menu_main_activity.xml rename to app/src/main/res/menu/menu_main_fragment.xml index d07c96a..ccaf8ce 100644 --- a/app/src/main/res/menu/menu_main_activity.xml +++ b/app/src/main/res/menu/menu_main_fragment.xml @@ -1,5 +1,9 @@ + + diff --git a/app/src/main/res/menu/menu_navigation_drawer.xml b/app/src/main/res/menu/menu_navigation_drawer.xml index 21ff825..9e58fd0 100644 --- a/app/src/main/res/menu/menu_navigation_drawer.xml +++ b/app/src/main/res/menu/menu_navigation_drawer.xml @@ -4,11 +4,10 @@ tools:showIn="navigation_view"> - - \ No newline at end of file + diff --git a/app/src/main/res/menu/menu_program_fragment.xml b/app/src/main/res/menu/menu_program_fragment.xml new file mode 100644 index 0000000..493ad07 --- /dev/null +++ b/app/src/main/res/menu/menu_program_fragment.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/app/src/main/res/navigation/main_navigation_graph.xml b/app/src/main/res/navigation/main_navigation_graph.xml new file mode 100644 index 0000000..4b7afe2 --- /dev/null +++ b/app/src/main/res/navigation/main_navigation_graph.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-v21/styles.xml b/app/src/main/res/values-v21/styles.xml new file mode 100644 index 0000000..de7fde9 --- /dev/null +++ b/app/src/main/res/values-v21/styles.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 93f666d..992cea1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,5 +1,7 @@ VTU CS Lab + VTU CS Lab New Version + No Internet Connection.\n\nThis content is not available offline yet, Kindly connect to the internet to sync this content locally. No Internet Connection. Some Error Occurred. Copy @@ -13,4 +15,5 @@ Repository Exit Privacy Policy + Code copied to Clipboard. diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 58b3344..1ca91d6 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -14,6 +14,16 @@ @color/colorAccent + +