diff --git a/app/build.gradle b/app/build.gradle index bd68e1b..755a347 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -32,7 +32,6 @@ dependencies { androidTestImplementation('androidx.test.espresso:espresso-core:3.1.0-beta02', { exclude group: 'com.android.support', module: 'support-annotations' }) - implementation 'androidx.appcompat:appcompat:1.0.0' implementation 'androidx.preference:preference:1.0.0' implementation 'com.google.android.material:material:1.0.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha2' @@ -42,7 +41,7 @@ dependencies { implementation 'androidx.legacy:legacy-support-v4:1.0.0' testImplementation 'junit:junit:4.12' implementation fileTree(include: ['*.jar'], dir: 'libs') - implementation 'androidx.appcompat:appcompat:1.0.0' + implementation 'androidx.appcompat:appcompat:1.0.2' implementation 'org.jbundle.util.osgi.wrapped:org.jbundle.util.osgi.wrapped.org.apache.http.client:4.1.2' implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'org.jetbrains:annotations-java5:15.0' diff --git a/app/src/main/java/de/sebse/fuplanner/MainActivity.java b/app/src/main/java/de/sebse/fuplanner/MainActivity.java index a4929d3..c0bf213 100644 --- a/app/src/main/java/de/sebse/fuplanner/MainActivity.java +++ b/app/src/main/java/de/sebse/fuplanner/MainActivity.java @@ -8,6 +8,7 @@ import android.view.View; import android.widget.TextView; import android.widget.Toast; +import com.android.volley.NetworkResponse; import com.google.android.material.navigation.NavigationView; import java.util.HashMap; @@ -33,18 +34,23 @@ import de.sebse.fuplanner.fragments.canteen.DaySwitcherFragment; import de.sebse.fuplanner.fragments.moddetails.ModDetailFragment; import de.sebse.fuplanner.services.Canteen.CanteenBrowser; import de.sebse.fuplanner.services.Canteen.types.Canteen; +import de.sebse.fuplanner.services.GoogleAuth.Credentials; import de.sebse.fuplanner.services.GoogleAuth.GoogleAuth; import de.sebse.fuplanner.services.KVV.KVV; +import de.sebse.fuplanner.services.KVV.KVVListener; import de.sebse.fuplanner.services.KVV.types.LoginToken; import de.sebse.fuplanner.services.KVV.types.Modules; import de.sebse.fuplanner.tools.MainActivityListener; +import de.sebse.fuplanner.tools.NewAsyncQueue; import de.sebse.fuplanner.tools.RequestPermissionsResultListener; import de.sebse.fuplanner.tools.logging.Logger; +import de.sebse.fuplanner.tools.network.NetworkCallback; +import de.sebse.fuplanner.tools.network.NetworkError; +import de.sebse.fuplanner.tools.network.NetworkErrorCallback; public class MainActivity extends AppCompatActivity - implements MainActivityListener, + implements MainActivityListener, KVVListener, NavigationView.OnNavigationItemSelectedListener, - LoginFragment.OnLoginFragmentInteractionListener, ModulesFragment.OnModulesFragmentInteractionListener, CanteensFragment.OnCanteensFragmentInteractionListener { @@ -68,21 +74,20 @@ public class MainActivity extends AppCompatActivity private NavigationView mNavigationView; private int fragmentPage = FRAGMENT_NONE; - private int currentPage = FRAGMENT_NONE; private String fragmentData = ""; - private String currentData = ""; private CanteenBrowser mCanteenBrowser; - private boolean mOfflineMode = false; private HashMap permissionListeners = new HashMap<>(); + private boolean mOfflineBanner; + private NewAsyncQueue mQueue = new NewAsyncQueue(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - int newFragmentPage = FRAGMENT_NONE; - String newFragmentData = ""; + int desiredPage = getDefaultFragmentAfterLogin(); + String desiredData = ""; if (savedInstanceState != null) { - newFragmentPage = savedInstanceState.getInt(ARG_FRAGMENT_PAGE, fragmentPage); - newFragmentData = savedInstanceState.getString(ARG_FRAGMENT_STATUS, fragmentData); + desiredPage = savedInstanceState.getInt(ARG_FRAGMENT_PAGE, desiredPage); + desiredData = savedInstanceState.getString(ARG_FRAGMENT_STATUS, desiredData); } setContentView(R.layout.activity_main); @@ -99,15 +104,12 @@ public class MainActivity extends AppCompatActivity mNavigationView.setNavigationItemSelectedListener(this); mFragmentManager = getSupportFragmentManager(); - LoginToken loginToken = getKVV().easyLogin(); - if (loginToken == null) { - checkAndDoLogin(); - } else { - if (newFragmentPage != FRAGMENT_LOGIN && newFragmentPage != FRAGMENT_STARTUP && newFragmentPage != FRAGMENT_NONE) - toLoginState(loginToken, newFragmentPage, newFragmentData); - else - toLoginState(loginToken, getDefaultFragmentAfterLogin(), ""); + if (!getKVV().account().restoreOnlineLogin()) { + desiredPage = FRAGMENT_LOGIN; + desiredData = ""; } + updateNavigation(); + changeFragment(desiredPage, desiredData); } @Override @@ -123,7 +125,7 @@ public class MainActivity extends AppCompatActivity @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. - if (currentPage == FRAGMENT_SCHEDULE) { + if (fragmentPage == FRAGMENT_SCHEDULE) { getMenuInflater().inflate(R.menu.options_schedule, menu); return true; } @@ -180,12 +182,12 @@ public class MainActivity extends AppCompatActivity startActivity(sendIntent); break; case R.id.nav_logout: - this.getKVV().logout(); + getKVV().account().logout(true); + getKVV().modules().list().delete(); this.getGoogleAuth().getLoginState(credentials -> { if (credentials != null) { this.getGoogleAuth().deleteLoginState(credentials.getUsername(), credentials.getPassword()); } - this.toLogoutState(); }); break; @@ -210,13 +212,14 @@ public class MainActivity extends AppCompatActivity @Override protected void onSaveInstanceState(Bundle savedInstanceState) { - Fragment fragment = mFragmentManager.findFragmentByTag(String.valueOf(fragmentPage)); - savedInstanceState.putInt(ARG_FRAGMENT_PAGE, fragmentPage); - savedInstanceState.putString(ARG_FRAGMENT_STATUS, fragmentData); - if (fragment instanceof ModDetailFragment) { - savedInstanceState.putString(ARG_FRAGMENT_STATUS, ((ModDetailFragment) fragment).getData()); - } else { - savedInstanceState.putString(ARG_FRAGMENT_STATUS, fragmentData); + if (fragmentPage != FRAGMENT_STARTUP && fragmentPage != FRAGMENT_NONE && fragmentPage != FRAGMENT_LOGIN) { + Fragment fragment = mFragmentManager.findFragmentByTag(String.valueOf(fragmentPage)); + savedInstanceState.putInt(ARG_FRAGMENT_PAGE, fragmentPage); + if (fragment instanceof ModDetailFragment) { + savedInstanceState.putString(ARG_FRAGMENT_STATUS, ((ModDetailFragment) fragment).getData()); + } else { + savedInstanceState.putString(ARG_FRAGMENT_STATUS, fragmentData); + } } super.onSaveInstanceState(savedInstanceState); } @@ -239,9 +242,17 @@ public class MainActivity extends AppCompatActivity return this.mGoogleAuth; } + /*@Deprecated + public de.sebse.fuplanner.services.KVV.KVV getKVV() { + if (this.mKVV == null) { + this.mKVV = new de.sebse.fuplanner.services.KVV.KVV(this); + } + return this.mKVV; + }*/ + public KVV getKVV() { if (this.mKVV == null) { - this.mKVV = new KVV(this); + this.mKVV = new KVV(this, this); } return this.mKVV; } @@ -254,59 +265,25 @@ public class MainActivity extends AppCompatActivity } private int getDefaultFragmentAfterLogin() { - return getDefaultFragmentAfterLogin(new String[1]); - } - - private int getDefaultFragmentAfterLogin(String[] id) { - if (fragmentPage == FRAGMENT_NONE){ - id[0] = ""; - return FRAGMENT_MODULES; - } - else { - id[0] = fragmentData; - return fragmentPage; - } + return FRAGMENT_MODULES; } private void toLogoutState() { - setOfflineBanner(true); + setOfflineBanner(false); setRefreshFailedBanner(false); + updateNavigation(); changeFragment(FRAGMENT_LOGIN); } - private void toLoginState(LoginToken loginToken, int newFragment, String newData) { - if (loginToken == null) { - toLogoutState(); - } else { - toLoginState(loginToken.getFullName(), loginToken.getEmail(), newFragment, newData, true); - } - } - - private void toLoginState(String fullName, String email, int newFragment, String newData, boolean onlineMode) { - setOfflineBanner(onlineMode); - changeFragment(newFragment, newData); + private void toLoginState(String fullName, String email, int newFragment, boolean onlineMode) { + setOfflineBanner(!onlineMode); + updateNavigation(); View header = mNavigationView.getHeaderView(0); ((TextView) header.findViewById(R.id.login_name)).setText(fullName); ((TextView) header.findViewById(R.id.login_mail)).setText(email); - } - private void checkAndDoLogin() { - changeFragment(FRAGMENT_STARTUP); - getGoogleAuth().getLoginState(credentials -> { - if (credentials == null || credentials.getUsername() == null || credentials.getPassword() == null) { - toLogoutState(); - return; - } - String[] id = {""}; - - int fragment = getDefaultFragmentAfterLogin(id); - this.getKVV().login(credentials.getUsername(), credentials.getPassword(), success -> toLoginState(success, fragment , id[0]), - error -> { - log.e(error); - toLogoutState(); - }); - }); + changeFragment(newFragment); } private void changeFragment(int newFragment) { @@ -355,56 +332,36 @@ public class MainActivity extends AppCompatActivity fragmentTransaction.commit(); if (newFragment == FRAGMENT_STARTUP) { - findViewById(R.id.app_bar_layout).setVisibility(View.GONE); + findViewById(R.id.app_bar_include).setVisibility(View.GONE); } else { - findViewById(R.id.app_bar_layout).setVisibility(View.VISIBLE); + findViewById(R.id.app_bar_include).setVisibility(View.VISIBLE); } - boolean isChangeLoginState = - ( - (newFragment == FRAGMENT_STARTUP || newFragment == FRAGMENT_LOGIN) && - (currentPage != FRAGMENT_STARTUP && currentPage != FRAGMENT_LOGIN) - ) || ( - (currentPage == FRAGMENT_STARTUP || currentPage == FRAGMENT_LOGIN || currentPage == FRAGMENT_NONE) && - (newFragment != FRAGMENT_STARTUP && newFragment != FRAGMENT_LOGIN && (getKVV().isLoggedIn() || mOfflineMode)) - ); + this.fragmentPage = newFragment; + this.fragmentData = newData; - if (newFragment != FRAGMENT_STARTUP && newFragment != FRAGMENT_NONE && newFragment != FRAGMENT_LOGIN) { - this.fragmentPage = newFragment; - this.fragmentData = newData; - } - this.currentPage = newFragment; - this.currentData = newData; invalidateOptionsMenu(); - if (isChangeLoginState) - refreshNavigation(); - else - setNavigationSelection(currentPage, currentData); } - private void setOfflineBanner(boolean onlineMode) { + private void setOfflineBanner(boolean visible) { View offline_header = findViewById(R.id.offline_msg); - if (onlineMode) - offline_header.setVisibility(View.GONE); - else - offline_header.setVisibility(View.VISIBLE); - mOfflineMode = !onlineMode; + offline_header.setVisibility(visible ? View.VISIBLE : View.GONE); + mOfflineBanner = visible; } private void setRefreshFailedBanner(boolean refreshFailed) { View viewNoConnection = findViewById(R.id.no_connection_msg); - if (!mOfflineMode && refreshFailed) + if (!mOfflineBanner && refreshFailed) viewNoConnection.setVisibility(View.VISIBLE); else viewNoConnection.setVisibility(View.GONE); - } - private void setNavigationSelection(int fragment, String data) { + private void setNavigationSelection() { MenuItem item; - switch (fragment) { + switch (fragmentPage) { case FRAGMENT_MODULES_DETAILS: - getKVV().getModule(data, success -> { + getKVV().modules().list().find(fragmentData, success -> { int size = mNavigationView.getMenu().size(); //noinspection ConstantConditions String title = success == null ? null : success.title; @@ -426,7 +383,7 @@ public class MainActivity extends AppCompatActivity case FRAGMENT_CANTEENS_DETAILS: getCanteenBrowser().getCanteens(success -> { int size = mNavigationView.getMenu().size(); - Canteen canteen = success.getCanteen(Integer.parseInt(data)); + Canteen canteen = success.getCanteen(Integer.parseInt(fragmentData)); //noinspection ConstantConditions String title = canteen == null ? null : canteen.getName(); for (int k = 0; k < size; k++) { @@ -464,9 +421,11 @@ public class MainActivity extends AppCompatActivity }); } - private void afterAnyMenuInflate(boolean isLoggedIn) { + private void afterAnyMenuInflate(boolean isLoggedIn, Runnable done) { + int MAX_COUNT = isLoggedIn ? 2 : 1; + final int[] count = {0}; if (isLoggedIn) { - getKVV().getModuleList(success -> { + getKVV().modules().list().recv(success -> { int i = 0; for (Iterator it = success.latestSemesterIterator(); it.hasNext(); ) { Modules.Module module = it.next(); @@ -477,7 +436,11 @@ public class MainActivity extends AppCompatActivity }); i++; } - }, log::e); + if (++count[0] == MAX_COUNT) done.run(); + }, msg -> { + if (++count[0] == MAX_COUNT) done.run(); + log.e(msg); + }); } getCanteenBrowser().getCanteens(success -> { int i = 0; @@ -489,7 +452,27 @@ public class MainActivity extends AppCompatActivity }); i++; } - }, log::e); + if (++count[0] == MAX_COUNT) done.run(); + }, msg -> { + if (++count[0] == MAX_COUNT) done.run(); + log.e(msg); + }); + } + + private void updateNavigation() { + mQueue.add(() -> { + boolean isLoggedIn = getKVV().account().isLoggedIn(); + setNavigationHeader(isLoggedIn); + mNavigationView.getMenu().clear(); + if (isLoggedIn) + mNavigationView.inflateMenu(R.menu.activity_main_drawer_login); + else + mNavigationView.inflateMenu(R.menu.activity_main_drawer); + afterAnyMenuInflate(isLoggedIn, () -> { + setNavigationSelection(); + mQueue.next(); + }); + }); } @@ -498,11 +481,7 @@ public class MainActivity extends AppCompatActivity - public void onLoginFragmentInteraction(LoginToken loginToken, boolean onlineMode) { - String[] id = {""}; - int fragment = getDefaultFragmentAfterLogin(id); - toLoginState(loginToken.getFullName(), loginToken.getEmail(), fragment, id[0], onlineMode); - } + @Override public void onModulesFragmentInteraction(final String itemID) { @@ -524,21 +503,6 @@ public class MainActivity extends AppCompatActivity setTitle(titleId); } - @Override - public void loginTokenInvalid(boolean doLoginCheck) { - if (doLoginCheck) { - getKVV().testLogin(isSuccess -> { - if (!isSuccess) { - getKVV().invalidate(); - checkAndDoLogin(); - } - }); - } else { - getKVV().invalidate(); - checkAndDoLogin(); - } - } - @Override public void onRefreshCompleted(boolean isFailed) { setRefreshFailedBanner(isFailed); @@ -564,16 +528,43 @@ public class MainActivity extends AppCompatActivity Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); } + + + + + + + + + @Override - public void refreshNavigation() { - boolean isLoggedIn = getKVV().isLoggedIn() || mOfflineMode; - setNavigationHeader(isLoggedIn); - mNavigationView.getMenu().clear(); - if (isLoggedIn) - mNavigationView.inflateMenu(R.menu.activity_main_drawer_login); - else - mNavigationView.inflateMenu(R.menu.activity_main_drawer); - afterAnyMenuInflate(isLoggedIn); - setNavigationSelection(currentPage, currentData); + public void getCredentials(NetworkCallback callback, NetworkErrorCallback error) { + getGoogleAuth().getLoginState(credentials -> { + if (credentials == null || credentials.getUsername() == null || credentials.getPassword() == null) { + error.onError(new NetworkError(200100, 403, "No Google Login available!")); + } else { + callback.onResponse(credentials); + } + }); + } + + @Override + public void onLogin(LoginToken token, boolean enteringOnlineMode) { + toLoginState(token.getFullName(), token.getEmail(), getDefaultFragmentAfterLogin(), enteringOnlineMode); + } + + @Override + public void onLogout() { + toLogoutState(); + } + + @Override + public void onModuleListChange() { + updateNavigation(); + } + + @Override + public void onKVVNetworkResponse(NetworkResponse error) { + setRefreshFailedBanner(error != null); } } diff --git a/app/src/main/java/de/sebse/fuplanner/fragments/CanteensAdapter.java b/app/src/main/java/de/sebse/fuplanner/fragments/CanteensAdapter.java index 41c6ba0..d9e1217 100644 --- a/app/src/main/java/de/sebse/fuplanner/fragments/CanteensAdapter.java +++ b/app/src/main/java/de/sebse/fuplanner/fragments/CanteensAdapter.java @@ -10,7 +10,6 @@ import de.sebse.fuplanner.R; import de.sebse.fuplanner.fragments.CanteensFragment.OnCanteensFragmentInteractionListener; import de.sebse.fuplanner.services.Canteen.types.Canteen; import de.sebse.fuplanner.services.Canteen.types.Canteens; -import de.sebse.fuplanner.services.KVV.types.Modules; import de.sebse.fuplanner.tools.ui.ItemViewHolder; /** diff --git a/app/src/main/java/de/sebse/fuplanner/fragments/CanteensFragment.java b/app/src/main/java/de/sebse/fuplanner/fragments/CanteensFragment.java index c7d206d..9eee7a8 100644 --- a/app/src/main/java/de/sebse/fuplanner/fragments/CanteensFragment.java +++ b/app/src/main/java/de/sebse/fuplanner/fragments/CanteensFragment.java @@ -70,8 +70,8 @@ public class CanteensFragment extends Fragment { CanteenBrowser browser = ((MainActivity) getActivity()).getCanteenBrowser(); browser.getCanteens(success -> { adapter.setCanteens(success); - if (mMainActivityListener != null) - mMainActivityListener.refreshNavigation(); + //if (mMainActivityListener != null) + // mMainActivityListener.refreshNavigation(); swipeLayout.setRefreshing(false); }, error -> { log.e(error.toString()); diff --git a/app/src/main/java/de/sebse/fuplanner/fragments/LoginFragment.java b/app/src/main/java/de/sebse/fuplanner/fragments/LoginFragment.java index f063748..a481e4f 100644 --- a/app/src/main/java/de/sebse/fuplanner/fragments/LoginFragment.java +++ b/app/src/main/java/de/sebse/fuplanner/fragments/LoginFragment.java @@ -9,31 +9,21 @@ import android.view.ViewGroup; import android.widget.Button; import android.widget.EditText; -import java.io.IOException; - import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; -import de.sebse.fuplanner.MainActivity; import de.sebse.fuplanner.R; -import de.sebse.fuplanner.services.GoogleAuth.GoogleAuth; -import de.sebse.fuplanner.services.KVV.KVV; -import de.sebse.fuplanner.services.KVV.types.LoginToken; -import de.sebse.fuplanner.services.KVV.types.Modules; import de.sebse.fuplanner.tools.MainActivityListener; import de.sebse.fuplanner.tools.logging.Logger; /** * A simple {@link Fragment} subclass. - * Activities that contain this fragment must implement the - * {@link LoginFragment.OnLoginFragmentInteractionListener} interface - * to handle interaction events. * Use the {@link LoginFragment#newInstance} factory method to * create an instance of this fragment. */ public class LoginFragment extends Fragment { - private OnLoginFragmentInteractionListener mListener; private final Logger log = new Logger(this); - private MainActivityListener mActivityListener; + @Nullable private MainActivityListener mActivityListener; public LoginFragment() { // Required empty public constructor @@ -57,21 +47,11 @@ public class LoginFragment extends Fragment { Bundle savedInstanceState) { // Inflate the layout for this fragment View v = inflater.inflate(R.layout.fragment_login, container, false); - try { - Context context = getContext(); - if (context != null) { - Modules modules = Modules.load(context); - if (modules != null) { - Button offline_btn = v.findViewById(R.id.btn_offline); - offline_btn.setVisibility(View.VISIBLE); - offline_btn.setText(v.getResources().getString(R.string.enter_offline_mode, modules.getToken().getUsername())); - offline_btn.setOnClickListener(v1 -> mListener.onLoginFragmentInteraction(modules.getToken(), false)); - } - } - } catch (IOException e) { - e.printStackTrace(); - } catch (ClassNotFoundException e) { - e.printStackTrace(); + if (mActivityListener != null && mActivityListener.getKVV().account().isOfflineStoredAvailable()) { + Button offline_btn = v.findViewById(R.id.btn_offline); + offline_btn.setVisibility(View.VISIBLE); + offline_btn.setText(v.getResources().getString(R.string.enter_offline_mode, mActivityListener.getKVV().modules().list().getUsername())); + offline_btn.setOnClickListener(v1 -> mActivityListener.getKVV().account().doOfflineLogin()); } View btn_login = v.findViewById(R.id.btn_login); @@ -84,40 +64,29 @@ public class LoginFragment extends Fragment { EditText input_usr = ((View) view.getParent()).findViewById(R.id.input_username); EditText input_pwd = ((View) view.getParent()).findViewById(R.id.input_password); - if (input_usr != null) { - if (input_pwd != null) { - if (LoginFragment.this.getActivity() == null) { - log.e("Login fragment has no activity!"); - return; + + String username = input_usr.getText().toString(); + String password = input_pwd.getText().toString(); + + mActivityListener.getKVV().account().doOnlineLogin(username, password, success -> { + progressDialog.dismiss(); + mActivityListener.getGoogleAuth().setLoginState(username, password); + input_usr.setError(null); + input_pwd.setError(null); + }, error -> { + progressDialog.dismiss(); + // Invalid password + if (mActivityListener != null) { + if (error.getCode() == 100131) { + mActivityListener.showToast(R.string.invalid_credentials); + input_usr.setError(input_usr.getResources().getString(R.string.invalid_credentials)); + input_pwd.setError(input_pwd.getResources().getString(R.string.invalid_credentials)); + } else { + mActivityListener.showToast(v.getResources().getString(R.string.error_occurred_code, error.getCode())); } - String username = input_usr.getText().toString(); - String password = input_pwd.getText().toString(); - KVV kvv = ((MainActivity) getActivity()).getKVV(); - GoogleAuth gauth = ((MainActivity) getActivity()).getGoogleAuth(); - kvv.login(username, password, success -> { - progressDialog.dismiss(); - gauth.setLoginState(username, password); - if (mListener != null) { - input_usr.setError(null); - input_pwd.setError(null); - mListener.onLoginFragmentInteraction(success, true); - } - }, error -> { - progressDialog.dismiss(); - // Invalid password - if (mActivityListener != null) { - if (error.getCode() == 100131) { - mActivityListener.showToast(R.string.invalid_credentials); - input_usr.setError(input_usr.getResources().getString(R.string.invalid_credentials)); - input_pwd.setError(input_pwd.getResources().getString(R.string.invalid_credentials)); - } else { - mActivityListener.showToast(v.getResources().getString(R.string.error_occurred_code, error.getCode())); - } - } - log.e("Error on KVV login!", error); - }); } - } + log.e("Error on KVV login!", error); + }); }); return v; @@ -128,12 +97,6 @@ public class LoginFragment extends Fragment { @Override public void onAttach(Context context) { super.onAttach(context); - if (context instanceof OnLoginFragmentInteractionListener) { - mListener = (OnLoginFragmentInteractionListener) context; - } else { - throw new RuntimeException(context.toString() - + " must implement OnLoginFragmentInteractionListener"); - } if (context instanceof MainActivityListener) { mActivityListener = (MainActivityListener) context; mActivityListener.onTitleTextChange(R.string.log_in); @@ -144,20 +107,6 @@ public class LoginFragment extends Fragment { @Override public void onDetach() { super.onDetach(); - mListener = null; - } - - /** - * This interface must be implemented by activities that contain this - * fragment to allow an interaction in this fragment to be communicated - * to the activity and potentially other fragments contained in that - * activity. - *

- * See the Android Training lesson Communicating with Other Fragments for more information. - */ - public interface OnLoginFragmentInteractionListener { - void onLoginFragmentInteraction(LoginToken loginToken, boolean onlineMode); + mActivityListener = null; } } diff --git a/app/src/main/java/de/sebse/fuplanner/fragments/ModulesAdapter.java b/app/src/main/java/de/sebse/fuplanner/fragments/ModulesAdapter.java index 4b85503..3d770ea 100644 --- a/app/src/main/java/de/sebse/fuplanner/fragments/ModulesAdapter.java +++ b/app/src/main/java/de/sebse/fuplanner/fragments/ModulesAdapter.java @@ -1,66 +1,115 @@ package de.sebse.fuplanner.fragments; +import android.util.Pair; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import java.util.ArrayList; + import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import de.sebse.fuplanner.R; import de.sebse.fuplanner.fragments.ModulesFragment.OnModulesFragmentInteractionListener; +import de.sebse.fuplanner.services.KVV.types.Lecturer; import de.sebse.fuplanner.services.KVV.types.Modules; +import de.sebse.fuplanner.services.KVV.types.Semester; +import de.sebse.fuplanner.tools.ui.CustomViewHolder; import de.sebse.fuplanner.tools.ui.ItemViewHolder; +import de.sebse.fuplanner.tools.ui.StringViewHolder; /** * {@link RecyclerView.Adapter} that can display a {@link Modules.Module} and makes a call to the * specified {@link OnModulesFragmentInteractionListener}. */ -class ModulesAdapter extends RecyclerView.Adapter { +class ModulesAdapter extends RecyclerView.Adapter { + + private static final int TYPE_HEADER = 0; + private static final int TYPE_ITEM = 2; private Modules mValues; private final OnModulesFragmentInteractionListener mListener; + private final ArrayList> mPositionalData; ModulesAdapter(OnModulesFragmentInteractionListener listener) { mValues = null; mListener = listener; + mPositionalData = new ArrayList<>(); } public void setModules(Modules modules) { mValues = modules; + mPositionalData.clear(); + Semester lastSemester = null; + for (Modules.Module module : mValues) { + Semester semester = module.semester; + if (semester == null) + continue; + if (!semester.equals(lastSemester)) { + mPositionalData.add(new Pair<>(TYPE_HEADER, semester)); + lastSemester = semester; + } + mPositionalData.add(new Pair<>(TYPE_ITEM, module)); + } this.notifyDataSetChanged(); } + @Override + public int getItemViewType(int position) { + return mPositionalData.get(position).first; + } + @NonNull @Override - public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.list_all_items, parent, false); - return new ItemViewHolder(view); + public CustomViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + if (viewType == TYPE_HEADER) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.list_all_caption, parent, false); + return new StringViewHolder(view); + } else { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.list_all_items, parent, false); + return new ItemViewHolder(view); + } } @Override - public void onBindViewHolder(@NonNull ItemViewHolder holder, int position) { - if (mValues == null) - return; - Modules.Module module = mValues.getByIndex(holder.getAdapterPosition()); - holder.mTitle.setText(module.title); - holder.mSubLeft.setText(module.semester); - holder.mSubRight.setText(module.type); - - holder.mView.setOnClickListener(v -> { - if (null != mListener) { - // Notify the active callbacks interface (the activity, if the - // fragment is attached to one) that an item has been selected. - mListener.onModulesFragmentInteraction(module.getID()); + public void onBindViewHolder(@NonNull CustomViewHolder holder, int position) { + Pair pair = mPositionalData.get(holder.getAdapterPosition()); + if (pair.first == TYPE_HEADER) { + StringViewHolder sHolder = (StringViewHolder) holder; + String localizedSemester; + Semester semester = (Semester) pair.second; + if (semester.getType() == Semester.SEM_WS) + localizedSemester = holder.mView.getResources().getString(R.string.winter_semester, semester.getYear(), semester.getYear()+1); + else + localizedSemester = holder.mView.getResources().getString(R.string.summer_semester, semester.getYear()); + sHolder.mString.setText(localizedSemester); + } else if (pair.first == TYPE_ITEM) { + ItemViewHolder iHolder = (ItemViewHolder) holder; + Modules.Module module = ((Modules.Module) pair.second); + iHolder.mTitle.setText(module.title); + StringBuilder lecturers = new StringBuilder(); + for (Lecturer lecturer: module.lecturer) { + if (!lecturer.isResponsible()) + continue; + if (lecturers.length() > 0) + lecturers.append(", "); + lecturers.append(lecturer.getNameShort()); } - }); + iHolder.mSubLeft.setText(lecturers); + iHolder.mSubRight.setText(module.type); + + iHolder.mView.setOnClickListener(v -> { + if (mListener != null) { + mListener.onModulesFragmentInteraction(module.getID()); + } + }); + } } @Override public int getItemCount() { - if (mValues != null) { - return mValues.size(); - } - return 0; + return mPositionalData.size(); } } diff --git a/app/src/main/java/de/sebse/fuplanner/fragments/ModulesFragment.java b/app/src/main/java/de/sebse/fuplanner/fragments/ModulesFragment.java index 6ebcf64..809ab8a 100644 --- a/app/src/main/java/de/sebse/fuplanner/fragments/ModulesFragment.java +++ b/app/src/main/java/de/sebse/fuplanner/fragments/ModulesFragment.java @@ -7,13 +7,12 @@ import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; -import de.sebse.fuplanner.MainActivity; import de.sebse.fuplanner.R; -import de.sebse.fuplanner.services.KVV.KVV; import de.sebse.fuplanner.tools.MainActivityListener; import de.sebse.fuplanner.tools.logging.Logger; @@ -28,7 +27,7 @@ public class ModulesFragment extends Fragment { private final Logger log = new Logger(this); private ModulesAdapter adapter; private SwipeRefreshLayout swipeLayout; - private MainActivityListener mMainActivityListener; + @Nullable private MainActivityListener mMainActivityListener; /** * Mandatory empty constructor for the fragment manager to instantiate the @@ -66,12 +65,11 @@ public class ModulesFragment extends Fragment { } private void refresh(boolean forceRefresh) { - if (getActivity() != null) { - KVV kvv = ((MainActivity) getActivity()).getKVV(); - kvv.getModuleList(success -> { + if (mMainActivityListener != null) { + mMainActivityListener.getKVV().modules().list().recv(success -> { adapter.setModules(success); - if (mMainActivityListener != null) - mMainActivityListener.refreshNavigation(); + //if (mMainActivityListener != null) + // mMainActivityListener.refreshNavigation(); swipeLayout.setRefreshing(false); }, error -> { log.e(error.toString()); diff --git a/app/src/main/java/de/sebse/fuplanner/fragments/ScheduleFragment.java b/app/src/main/java/de/sebse/fuplanner/fragments/ScheduleFragment.java index eaafe4a..2dce81b 100644 --- a/app/src/main/java/de/sebse/fuplanner/fragments/ScheduleFragment.java +++ b/app/src/main/java/de/sebse/fuplanner/fragments/ScheduleFragment.java @@ -16,7 +16,6 @@ import java.util.List; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import de.sebse.fuplanner.R; -import de.sebse.fuplanner.services.KVV.KVV; import de.sebse.fuplanner.services.KVV.types.Event; import de.sebse.fuplanner.services.KVV.types.Modules; import de.sebse.fuplanner.tools.UtilsDate; @@ -59,24 +58,22 @@ public class ScheduleFragment extends Fragment implements MonthLoader.MonthChang } public void invalidate(boolean forceRefresh) { - if (mListener != null) { - KVV kvv = mListener.getKVV(); - kvv.getModuleList((Modules success) -> { - mModules = success; - final int[] i = {0}; - for (Modules.Module module: mModules) { - kvv.getModuleEvents(module, success1 -> { - i[0]++; - if (i[0] >= mModules.size()) { - if (mWeekView != null) { - mWeekView.invalidate(); - mWeekView.notifyDatasetChanged(); - } + if (mListener == null) return; + mListener.getKVV().modules().list().recv(modules -> { + mModules = modules; + final int[] i = {0}; + for (Modules.Module module: mModules) { + mListener.getKVV().modules().events().recv(module, success1 -> { + i[0]++; + if (i[0] >= mModules.size()) { + if (mWeekView != null) { + mWeekView.invalidate(); + mWeekView.notifyDatasetChanged(); } - }, log::e, forceRefresh); - } - }, log::e, forceRefresh); - } + } + }, log::e, forceRefresh); + } + }, log::e, forceRefresh); } @Override @@ -183,24 +180,21 @@ public class ScheduleFragment extends Fragment implements MonthLoader.MonthChang String moduleId = idParts[0]; AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getContext()); - if (mListener != null) { - KVV kvv = mListener.getKVV(); - kvv.getModuleList((Modules success) -> { - Modules.Module module = success.get(moduleId); - String moduleName = module.title; - alertDialogBuilder - .setTitle(event.getName()) - .setMessage( - getResources().getString(R.string.module_name, moduleName) + "\n" + - getResources().getString(R.string.location_name, event.getLocation()) + "\n" + - getResources().getString(R.string.date_scale, UtilsDate.getModifiedTime(getContext(), event.getStartTime().getTimeInMillis()), UtilsDate.getModifiedTime(getContext(), event.getEndTime().getTimeInMillis()+1)) - ) - .setCancelable(true) - .setNeutralButton(R.string.close, (dialog, id) -> dialog.cancel()); + if (mListener == null) return; + mListener.getKVV().modules().list().find(moduleId, module -> { + String moduleName = module.title; + alertDialogBuilder + .setTitle(event.getName()) + .setMessage( + getResources().getString(R.string.module_name, moduleName) + "\n" + + getResources().getString(R.string.location_name, event.getLocation()) + "\n" + + getResources().getString(R.string.date_scale, UtilsDate.getModifiedTime(getContext(), event.getStartTime().getTimeInMillis()), UtilsDate.getModifiedTime(getContext(), event.getEndTime().getTimeInMillis()+1)) + ) + .setCancelable(true) + .setNeutralButton(R.string.close, (dialog, id) -> dialog.cancel()); - AlertDialog alertDialog = alertDialogBuilder.create(); - alertDialog.show(); - }, log::e); - } + AlertDialog alertDialog = alertDialogBuilder.create(); + alertDialog.show(); + }, log::e); } } diff --git a/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailAnnounceAdapter.java b/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailAnnounceAdapter.java index d35f26f..b5a37f0 100644 --- a/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailAnnounceAdapter.java +++ b/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailAnnounceAdapter.java @@ -16,7 +16,7 @@ import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.recyclerview.widget.RecyclerView; import de.sebse.fuplanner.R; -import de.sebse.fuplanner.services.KVV.Download; +import de.sebse.fuplanner.services.KVV.ui.Download; import de.sebse.fuplanner.services.KVV.types.Announcement; import de.sebse.fuplanner.services.KVV.types.Modules; import de.sebse.fuplanner.tools.Regex; @@ -76,12 +76,10 @@ class ModDetailAnnounceAdapter extends RecyclerView.Adapter { tag.id = i; tag.layoutColor = ContextCompat.getColor(holder.mView.getContext(), R.color.colorFUBlue); holder.mTagGroup.addTag(tag); - log.d(notes.get(i)); } holder.mTagGroup.setOnTagClickListener((tag, i) -> { String s = notes.get(i); if (s != null) { - log.d("Download", s); String name = urlToName(s, i, holder.mView.getResources()); requestInterface.request(name, s); } diff --git a/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailAnnounceFragment.java b/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailAnnounceFragment.java index 59df627..9a203b8 100644 --- a/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailAnnounceFragment.java +++ b/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailAnnounceFragment.java @@ -6,16 +6,15 @@ import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.ExpandableListView; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import de.sebse.fuplanner.MainActivity; import de.sebse.fuplanner.R; -import de.sebse.fuplanner.services.KVV.Download; -import de.sebse.fuplanner.services.KVV.KVV; +import de.sebse.fuplanner.services.KVV.ui.Download; import de.sebse.fuplanner.services.KVV.types.Modules; import de.sebse.fuplanner.tools.MainActivityListener; import de.sebse.fuplanner.tools.logging.Logger; @@ -33,7 +32,7 @@ public class ModDetailAnnounceFragment extends Fragment implements Download.OnDo private ModDetailAnnounceAdapter adapter; private SwipeRefreshLayout swipeLayout; private Download download; - private MainActivityListener context; + @Nullable private MainActivityListener mListener; public ModDetailAnnounceFragment() { @@ -83,41 +82,34 @@ public class ModDetailAnnounceFragment extends Fragment implements Download.OnDo } private void refresh(boolean forceRefresh) { - if (getActivity() != null) { - KVV kvv = ((MainActivity) getActivity()).getKVV(); - kvv.getModule(mItemPos, (Modules.Module module) -> { - adapter.setModule(module); - kvv.getModuleAnnouncements(module, success1 -> { - adapter.setModule(); - swipeLayout.setRefreshing(false); - }, error -> { - swipeLayout.setRefreshing(false); - log.e(error); - }, forceRefresh); - }, error -> { - swipeLayout.setRefreshing(false); - log.e(error); - }, forceRefresh); - } + if (mListener == null) + return; + mListener.getKVV().modules().announcements().recv(mItemPos, success -> { + adapter.setModule(success); + swipeLayout.setRefreshing(false); + }, error -> { + swipeLayout.setRefreshing(false); + log.e(error); + }, forceRefresh); } @Override public void request(String title, String url) { - context.getKVV().getModule(mItemPos, (Modules.Module module) -> { - if (module == null) - return; + if (mListener == null) + return; + mListener.getKVV().modules().list().find(mItemPos, (Modules.Module module) -> { String folderName = "FU-"+module.title.replaceAll("[:*<>|/\"\\\\]", "-"); folderName += "/Assignment"; getDownload().openDownloadDialog(title, url, folderName); - }, log::e, false); + }, log::e); } @Override public void onAttach(Context context) { super.onAttach(context); if (context instanceof MainActivityListener) { - this.context = ((MainActivityListener) context); - this.context.addRequestPermissionsResultListener(getDownload().getRequestPermissionsResultListener(), "ModDetailAnnounceFragment"); + this.mListener = ((MainActivityListener) context); + this.mListener.addRequestPermissionsResultListener(getDownload().getRequestPermissionsResultListener(), "ModDetailAnnounceFragment"); } else throw new RuntimeException(context.toString() + " must implement MainActivityListener"); } @@ -125,7 +117,10 @@ public class ModDetailAnnounceFragment extends Fragment implements Download.OnDo @Override public void onDetach() { super.onDetach(); - this.context.removeRequestPermissionsResultListener("ModDetailAnnounceFragment"); + if (this.mListener != null) { + this.mListener.removeRequestPermissionsResultListener("ModDetailAnnounceFragment"); + this.mListener = null; + } } Download getDownload() { diff --git a/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailAssignmentAdapter.java b/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailAssignmentAdapter.java index e695666..34c49fd 100644 --- a/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailAssignmentAdapter.java +++ b/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailAssignmentAdapter.java @@ -16,7 +16,7 @@ import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.recyclerview.widget.RecyclerView; import de.sebse.fuplanner.R; -import de.sebse.fuplanner.services.KVV.Download; +import de.sebse.fuplanner.services.KVV.ui.Download; import de.sebse.fuplanner.services.KVV.types.Assignment; import de.sebse.fuplanner.services.KVV.types.Modules; import de.sebse.fuplanner.tools.Regex; @@ -76,12 +76,10 @@ class ModDetailAssignmentAdapter extends RecyclerView.Adapter tag.id = i; tag.layoutColor = ContextCompat.getColor(holder.mView.getContext(), R.color.colorFUBlue); holder.mTagGroup.addTag(tag); - log.d(notes.get(i)); } holder.mTagGroup.setOnTagClickListener((tag, i) -> { String s = notes.get(i); if (s != null) { - log.d("Download", s); String name = urlToName(s, i, holder.mView.getResources()); requestInterface.request(name, s); } diff --git a/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailAssignmentFragment.java b/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailAssignmentFragment.java index a3225d9..8706804 100644 --- a/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailAssignmentFragment.java +++ b/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailAssignmentFragment.java @@ -6,16 +6,15 @@ import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.ExpandableListView; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import de.sebse.fuplanner.MainActivity; import de.sebse.fuplanner.R; -import de.sebse.fuplanner.services.KVV.Download; -import de.sebse.fuplanner.services.KVV.KVV; +import de.sebse.fuplanner.services.KVV.ui.Download; import de.sebse.fuplanner.services.KVV.types.Modules; import de.sebse.fuplanner.tools.MainActivityListener; import de.sebse.fuplanner.tools.logging.Logger; @@ -33,7 +32,7 @@ public class ModDetailAssignmentFragment extends Fragment implements Download.On private ModDetailAssignmentAdapter adapter; private SwipeRefreshLayout swipeLayout; private Download download; - private MainActivityListener context; + @Nullable private MainActivityListener mListener; public ModDetailAssignmentFragment() { @@ -83,41 +82,35 @@ public class ModDetailAssignmentFragment extends Fragment implements Download.On } private void refresh(boolean forceRefresh) { - if (getActivity() != null) { - KVV kvv = ((MainActivity) getActivity()).getKVV(); - kvv.getModule(mItemPos, (Modules.Module module) -> { - adapter.setModule(module); - kvv.getModuleAssignments(module, success1 -> { - adapter.setModule(); - swipeLayout.setRefreshing(false); - }, error -> { - swipeLayout.setRefreshing(false); - log.e(error); - }, forceRefresh); - }, error -> { - swipeLayout.setRefreshing(false); - log.e(error); - }, forceRefresh); - } + if (mListener == null) + return; + mListener.getKVV().modules().assignments().recv(mItemPos, success -> { + adapter.setModule(success); + swipeLayout.setRefreshing(false); + }, error -> { + swipeLayout.setRefreshing(false); + log.e(error); + }, forceRefresh); } @Override public void request(String title, String url) { - context.getKVV().getModule(mItemPos, (Modules.Module module) -> { - if (module == null) - return; + log.d(title, url, mListener); + if (mListener == null) + return; + mListener.getKVV().modules().list().find(mItemPos, (Modules.Module module) -> { String folderName = "FU-"+module.title.replaceAll("[:*<>|/\"\\\\]", "-"); folderName += "/Assignment"; getDownload().openDownloadDialog(title, url, folderName); - }, log::e, false); + }, log::e); } @Override public void onAttach(Context context) { super.onAttach(context); if (context instanceof MainActivityListener) { - this.context = ((MainActivityListener) context); - this.context.addRequestPermissionsResultListener(getDownload().getRequestPermissionsResultListener(), "ModDetailAssignmentFragment"); + this.mListener = ((MainActivityListener) context); + this.mListener.addRequestPermissionsResultListener(getDownload().getRequestPermissionsResultListener(), "ModDetailAssignmentFragment"); } else throw new RuntimeException(context.toString() + " must implement MainActivityListener"); } @@ -125,7 +118,10 @@ public class ModDetailAssignmentFragment extends Fragment implements Download.On @Override public void onDetach() { super.onDetach(); - this.context.removeRequestPermissionsResultListener("ModDetailAssignmentFragment"); + if (this.mListener != null) { + this.mListener.removeRequestPermissionsResultListener("ModDetailAssignmentFragment"); + this.mListener = null; + } } Download getDownload() { diff --git a/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailEventAdapter.java b/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailEventAdapter.java index a3ef881..fd80dee 100644 --- a/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailEventAdapter.java +++ b/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailEventAdapter.java @@ -1,7 +1,6 @@ package de.sebse.fuplanner.fragments.moddetails; import android.content.res.Resources; -import android.util.Pair; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -11,7 +10,6 @@ import java.util.LinkedHashMap; import java.util.Map; import androidx.annotation.NonNull; -import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import de.sebse.fuplanner.R; import de.sebse.fuplanner.services.KVV.types.Event; @@ -19,12 +17,9 @@ import de.sebse.fuplanner.services.KVV.types.EventList; import de.sebse.fuplanner.services.KVV.types.GroupedEvents; import de.sebse.fuplanner.services.KVV.types.Modules; import de.sebse.fuplanner.tools.Triplet; -import de.sebse.fuplanner.tools.UtilsDate; import de.sebse.fuplanner.tools.logging.Logger; import de.sebse.fuplanner.tools.ui.CustomViewHolder; -import de.sebse.fuplanner.tools.ui.ItemViewHolder; import de.sebse.fuplanner.tools.ui.ListViewHolder; -import de.sebse.fuplanner.tools.ui.StringViewHolder; class ModDetailEventAdapter extends RecyclerView.Adapter { private static final String VALUE_LECTURE = "Class section - Lecture"; diff --git a/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailEventAdapterInner.java b/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailEventAdapterInner.java index b4e5551..2ff1e72 100644 --- a/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailEventAdapterInner.java +++ b/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailEventAdapterInner.java @@ -13,11 +13,8 @@ import de.sebse.fuplanner.R; import de.sebse.fuplanner.services.KVV.types.Event; import de.sebse.fuplanner.services.KVV.types.EventList; import de.sebse.fuplanner.services.KVV.types.GroupedEvents; -import de.sebse.fuplanner.tools.Triplet; import de.sebse.fuplanner.tools.UtilsDate; -import de.sebse.fuplanner.tools.ui.CustomViewHolder; import de.sebse.fuplanner.tools.ui.ItemViewHolder; -import de.sebse.fuplanner.tools.ui.ListViewHolder; class ModDetailEventAdapterInner extends RecyclerView.Adapter { private final ArrayList mPositionalData; diff --git a/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailEventFragment.java b/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailEventFragment.java index 04fb784..c5346c5 100644 --- a/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailEventFragment.java +++ b/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailEventFragment.java @@ -8,14 +8,13 @@ import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; -import de.sebse.fuplanner.MainActivity; import de.sebse.fuplanner.R; -import de.sebse.fuplanner.services.KVV.KVV; -import de.sebse.fuplanner.services.KVV.types.Modules; +import de.sebse.fuplanner.tools.MainActivityListener; import de.sebse.fuplanner.tools.logging.Logger; /** @@ -30,6 +29,7 @@ public class ModDetailEventFragment extends Fragment { private final Logger log = new Logger(this); private ModDetailEventAdapter adapter; private SwipeRefreshLayout swipeLayout; + @Nullable private MainActivityListener mListener; public ModDetailEventFragment() { @@ -81,22 +81,31 @@ public class ModDetailEventFragment extends Fragment { } private void refresh(boolean forceRefresh) { - if (getActivity() != null) { - KVV kvv = ((MainActivity) getActivity()).getKVV(); - kvv.getModule(mItemPos, (Modules.Module module) -> { - adapter.setModule(module); - kvv.getModuleEvents(module, success1 -> { - adapter.setModule(); - swipeLayout.setRefreshing(false); - }, error -> { - swipeLayout.setRefreshing(false); - log.e(error); - }, forceRefresh); - }, error -> { - swipeLayout.setRefreshing(false); - log.e(error); - }, forceRefresh); - } + if (mListener == null) + return; + mListener.getKVV().modules().events().recv(mItemPos, success -> { + adapter.setModule(success); + swipeLayout.setRefreshing(false); + }, error -> { + swipeLayout.setRefreshing(false); + log.e(error); + }, forceRefresh); } + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (context instanceof MainActivityListener) { + this.mListener = ((MainActivityListener) context); + } else + throw new RuntimeException(context.toString() + " must implement MainActivityListener"); + } + + @Override + public void onDetach() { + super.onDetach(); + if (this.mListener != null) { + this.mListener = null; + } + } } diff --git a/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailFragment.java b/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailFragment.java index d1c2591..b903da7 100644 --- a/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailFragment.java +++ b/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailFragment.java @@ -72,7 +72,7 @@ public class ModDetailFragment extends Fragment implements ModDetailListener { } if (mListener != null) { mListener.onTitleTextChange(R.string.courses); - mListener.getKVV().getModuleList(success -> { + mListener.getKVV().modules().list().recv(success -> { Modules.Module module = success.get(mItemPos); if (mListener != null && module != null) mListener.onTitleTextChange(module.title); diff --git a/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailGradebookAdapter.java b/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailGradebookAdapter.java index 62a779d..a5ef15b 100644 --- a/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailGradebookAdapter.java +++ b/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailGradebookAdapter.java @@ -12,7 +12,7 @@ import java.util.ArrayList; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import de.sebse.fuplanner.R; -import de.sebse.fuplanner.services.KVV.types.Gradebook; +import de.sebse.fuplanner.services.KVV.types.Grade; import de.sebse.fuplanner.services.KVV.types.Modules; import de.sebse.fuplanner.tools.ui.StringViewHolder; @@ -62,7 +62,7 @@ class ModDetailGradebookAdapter extends RecyclerView.Adapter { - adapter.setModule(module); - kvv.getModuleGradebook(module, success1 -> { - adapter.setModule(); - swipeLayout.setRefreshing(false); - }, error -> { - swipeLayout.setRefreshing(false); - log.e(error); - }, forceRefresh); - }, error -> { - swipeLayout.setRefreshing(false); - log.e(error); - }, forceRefresh); - } + if (mListener == null) + return; + mListener.getKVV().modules().gradebook().recv(mItemPos, success -> { + adapter.setModule(success); + swipeLayout.setRefreshing(false); + }, error -> { + swipeLayout.setRefreshing(false); + log.e(error); + }, forceRefresh); } + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (context instanceof MainActivityListener) { + this.mListener = ((MainActivityListener) context); + } else + throw new RuntimeException(context.toString() + " must implement MainActivityListener"); + } + + @Override + public void onDetach() { + super.onDetach(); + if (this.mListener != null) { + this.mListener = null; + } + } } diff --git a/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailOverviewAdapter.java b/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailOverviewAdapter.java index aca5eb7..8b14a70 100644 --- a/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailOverviewAdapter.java +++ b/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailOverviewAdapter.java @@ -1,5 +1,7 @@ package de.sebse.fuplanner.fragments.moddetails; +import android.content.Intent; +import android.net.Uri; import android.util.Pair; import android.view.LayoutInflater; import android.view.View; @@ -16,11 +18,13 @@ import de.sebse.fuplanner.R; import de.sebse.fuplanner.services.KVV.types.Announcement; import de.sebse.fuplanner.services.KVV.types.Assignment; import de.sebse.fuplanner.services.KVV.types.Event; +import de.sebse.fuplanner.services.KVV.types.Lecturer; import de.sebse.fuplanner.services.KVV.types.Modules; import de.sebse.fuplanner.tools.UtilsDate; import de.sebse.fuplanner.tools.logging.Logger; import de.sebse.fuplanner.tools.ui.CustomViewHolder; import de.sebse.fuplanner.tools.ui.ItemViewHolder; +import de.sebse.fuplanner.tools.ui.MailViewHolder; import de.sebse.fuplanner.tools.ui.StringViewHolder; class ModDetailOverviewAdapter extends RecyclerView.Adapter { @@ -30,6 +34,7 @@ class ModDetailOverviewAdapter extends RecyclerView.Adapter(TYPE_HEADER, ModulePart.DESCRIPTION)); mPositionalData.add(new Pair<>(TYPE_DESCRIPTION, null)); + mPositionalData.add(new Pair<>(TYPE_HEADER, ModulePart.LECTURERS)); + for (int i = 0; i < mValue.lecturer.size(); i++) { + mPositionalData.add(new Pair<>(TYPE_MAIL, ModulePart.LECTURERS+1024*i)); + } mPositionalData.add(new Pair<>(TYPE_HEADER, ModulePart.ANNOUNCEMENT)); addPositionalListData(getAnnounceCount(), ModulePart.ANNOUNCEMENT); mPositionalData.add(new Pair<>(TYPE_HEADER, ModulePart.ASSIGNMENT)); @@ -90,6 +99,10 @@ class ModDetailOverviewAdapter extends RecyclerView.Adapter { + String defaultText = m.mView.getResources().getString(R.string.mail_default_text, + lecturer.getName()); + Intent emailIntent = new Intent(Intent.ACTION_SENDTO); + emailIntent.setData(Uri.parse("mailto:"+lecturer.getMail())); + emailIntent.putExtra(Intent.EXTRA_SUBJECT, mValue.title); + emailIntent.putExtra(Intent.EXTRA_TEXT, defaultText); + m.mView.getContext().startActivity(Intent.createChooser(emailIntent, mValue.title)); + }); + break; + } case TYPE_SHOW_MORE: CustomViewHolder c = (CustomViewHolder) holder; c.mView.setOnClickListener(view -> { @@ -231,6 +267,7 @@ class ModDetailOverviewAdapter extends RecyclerView.Adapter { - adapter.setModule(module); - kvv.getModuleDetails(module, pair -> { - adapter.setModule(); - if (pair.second) - swipeLayout.setRefreshing(false); - }, error -> { + if (mListener != null) { + mListener.getKVV().modules().details().recv(mItemPos, pair -> { + adapter.setModule(pair.first); + if (pair.second) swipeLayout.setRefreshing(false); - log.e(error); - }, forceRefresh); }, error -> { swipeLayout.setRefreshing(false); log.e(error); @@ -104,15 +99,27 @@ public class ModDetailOverviewFragment extends Fragment { @Override public void onAttach(Context context) { + super.onAttach(context); + if (context instanceof MainActivityListener) { + this.mListener = ((MainActivityListener) context); + } else + throw new RuntimeException(context.toString() + " must implement MainActivityListener"); + + super.onAttach(context); Fragment parentFragment = getParentFragment(); if (parentFragment != null) { if (parentFragment instanceof ModDetailListener) { - mListener = (ModDetailListener) parentFragment; + mDetailListener = (ModDetailListener) parentFragment; } else { - throw new RuntimeException(context.toString() - + " must implement ModDetailListener"); + throw new RuntimeException(context.toString() + " must implement ModDetailListener"); } } else log.w("No parent fragment!"); } + + @Override + public void onDetach() { + super.onDetach(); + this.mListener = null; + } } diff --git a/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailResourceFragment.java b/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailResourceFragment.java index 665ba28..ed73214 100644 --- a/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailResourceFragment.java +++ b/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModDetailResourceFragment.java @@ -11,14 +11,14 @@ import android.widget.ImageView; import java.util.Arrays; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import de.sebse.fuplanner.MainActivity; import de.sebse.fuplanner.R; -import de.sebse.fuplanner.services.KVV.Download; -import de.sebse.fuplanner.services.KVV.KVV; +import de.sebse.fuplanner.services.KVV.ui.Download; import de.sebse.fuplanner.services.KVV.types.Modules; import de.sebse.fuplanner.services.KVV.types.Resource; import de.sebse.fuplanner.tools.MainActivityListener; @@ -40,8 +40,8 @@ public class ModDetailResourceFragment extends Fragment { private final Logger log = new Logger(this); private ModDetailResourceAdapter adapter; private SwipeRefreshLayout swipeLayout; - private MainActivityListener context; private Download download; + @Nullable private MainActivityListener mListener; public ModDetailResourceFragment() { @@ -88,10 +88,8 @@ public class ModDetailResourceFragment extends Fragment { if (!node.isLeaf()) { // Update and toggle the node. onToggle(!node.isExpand(), holder); - } else if(node.getContent() instanceof Resource.File) { // if leaf is file - KVV kvv = ModDetailResourceFragment.this.context.getKVV(); - kvv.getModule(mItemPos, (Modules.Module module) -> { - + } else if (node.getContent() instanceof Resource.File && ModDetailResourceFragment.this.mListener != null) { // if leaf is file + ModDetailResourceFragment.this.mListener.getKVV().modules().resources().recv(mItemPos, (Modules.Module module) -> { String folderName = "FU-"+module.title.replaceAll("[:*<>|/\"\\\\]", "-"); Resource.File file = (Resource.File) node.getContent(); getDownload().openDownloadDialog(file, folderName); @@ -120,14 +118,12 @@ public class ModDetailResourceFragment extends Fragment { return view; } - - @Override public void onAttach(Context context) { super.onAttach(context); if (context instanceof MainActivityListener) { - this.context = ((MainActivityListener) context); - this.context.addRequestPermissionsResultListener(getDownload().getRequestPermissionsResultListener(), "ModDetailResourceFragment"); + this.mListener = ((MainActivityListener) context); + this.mListener.addRequestPermissionsResultListener(getDownload().getRequestPermissionsResultListener(), "ModDetailResourceFragment"); } else throw new RuntimeException(context.toString() + " must implement MainActivityListener"); } @@ -135,26 +131,22 @@ public class ModDetailResourceFragment extends Fragment { @Override public void onDetach() { super.onDetach(); - this.context.removeRequestPermissionsResultListener("ModDetailResourceFragment"); + if (this.mListener != null) { + this.mListener.removeRequestPermissionsResultListener("ModDetailResourceFragment"); + this.mListener = null; + } } private void refresh(boolean forceRefresh) { - if (getActivity() != null) { - KVV kvv = ((MainActivity) getActivity()).getKVV(); - kvv.getModule(mItemPos, (Modules.Module module) -> { - adapter.setModule(module); - kvv.getModuleResources(module, success1 -> { - adapter.setModule(); - swipeLayout.setRefreshing(false); - }, error -> { - swipeLayout.setRefreshing(false); - log.e(error); - }, forceRefresh); - }, error -> { - swipeLayout.setRefreshing(false); - log.e(error); - }, forceRefresh); - } + if (mListener == null) + return; + mListener.getKVV().modules().assignments().recv(mItemPos, success -> { + adapter.setModule(success); + swipeLayout.setRefreshing(false); + }, error -> { + swipeLayout.setRefreshing(false); + log.e(error); + }, forceRefresh); } Download getDownload() { diff --git a/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModulePart.java b/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModulePart.java index 1b6c693..bb25f62 100644 --- a/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModulePart.java +++ b/app/src/main/java/de/sebse/fuplanner/fragments/moddetails/ModulePart.java @@ -8,6 +8,7 @@ class ModulePart { static final int EVENT = 4; static final int GRADEBOOK = 5; static final int RESOURCES = 6; + static final int LECTURERS = 7; private static final int[] pages = new int[]{OVERVIEW, ANNOUNCEMENT, ASSIGNMENT, GRADEBOOK, RESOURCES, EVENT}; static int getPageCount() { diff --git a/app/src/main/java/de/sebse/fuplanner/services/GoogleAuth/GoogleAuth.java b/app/src/main/java/de/sebse/fuplanner/services/GoogleAuth/GoogleAuth.java index ead95a8..98074e8 100644 --- a/app/src/main/java/de/sebse/fuplanner/services/GoogleAuth/GoogleAuth.java +++ b/app/src/main/java/de/sebse/fuplanner/services/GoogleAuth/GoogleAuth.java @@ -29,6 +29,7 @@ public class GoogleAuth { private static final String TAG = "GoogleAuth"; private final FragmentActivity activity; + private static final String FU_PLANNER_PROVIDER = "FUPlanner"; private CredentialsClient mCredentialsClient; private boolean mIsResolving; @Nullable @@ -61,6 +62,7 @@ public class GoogleAuth { connect(); CredentialRequest request = new CredentialRequest.Builder() .setPasswordLoginSupported(true) + .setAccountTypes(FU_PLANNER_PROVIDER) .build(); diff --git a/app/src/main/java/de/sebse/fuplanner/services/KVV/KVV.java b/app/src/main/java/de/sebse/fuplanner/services/KVV/KVV.java index 4420c01..076947f 100644 --- a/app/src/main/java/de/sebse/fuplanner/services/KVV/KVV.java +++ b/app/src/main/java/de/sebse/fuplanner/services/KVV/KVV.java @@ -1,219 +1,47 @@ package de.sebse.fuplanner.services.KVV; import android.content.Context; -import android.util.Pair; -import java.io.IOException; -import java.util.ArrayList; +import org.jetbrains.annotations.NotNull; + import java.util.HashMap; -import androidx.annotation.NonNull; -import de.sebse.fuplanner.services.KVV.types.LoginToken; -import de.sebse.fuplanner.services.KVV.types.Modules; -import de.sebse.fuplanner.tools.MainActivityListener; -import de.sebse.fuplanner.tools.logging.Logger; -import de.sebse.fuplanner.tools.network.NetworkCallback; -import de.sebse.fuplanner.tools.network.NetworkErrorCallback; - -/** - * Created by sebastian on 29.10.17. - */ - public class KVV { - private final Context context; - private LoginToken lastToken; - private boolean isLoginPending = true; - private final ArrayList updatingList; private final HashMap addons = new HashMap<>(); - private final MainActivityListener mListener; - private Logger log = new Logger(this); + private final KVVListener mListener; + private final Context mContext; - public KVV(Context context) { - mListener = (MainActivityListener) context; - this.context = context; - this.updatingList = new ArrayList<>(); + public KVV(KVVListener listener, Context context) { + this.mListener = listener; + this.mContext = context; } - public boolean isLoggedIn() { - return this.lastToken != null; + @NotNull + public Login account() { + return (Login) addAndGet("account", () -> new Login(mListener, mContext)); } - public LoginToken easyLogin() { - KVVLogin login = new KVVLogin(this.context); - lastToken = login.easyLogin(); - this.endUpdate(); - return lastToken; + @NotNull + public Modules modules() { + return (Modules) addAndGet("module", () -> new Modules(account(), mListener, mContext)); } - public void login(@NonNull String username, @NonNull String password, final NetworkCallback callback, NetworkErrorCallback error) { - KVVLogin login = new KVVLogin(this.context); - login.login(username, password, success -> { - lastToken = success; - this.endUpdate(); - try { - login.saveOffline(); - } catch (IOException e) { - e.printStackTrace(); - } - callback.onResponse(success); - }, error1 -> { - this.endUpdate(); - error.onError(error1); - }); - } - public void logout() { - KVVModuleList modules = (KVVModuleList) addons.get("modules"); - if (modules != null) { - modules.deleteModulesOffline(this.context); + + + + + @NotNull + private Object addAndGet(@NotNull String addon, @NotNull ModuleCreatorInterface creatorInterface) { + Object o = addons.get(addon); + if (o == null) { + o = creatorInterface.create(); + addons.put(addon, o); } - invalidate(); + return o; } - public void invalidate() { - if (lastToken != null) { - new KVVLogin(context).deleteOffline(); - lastToken = null; - } - addons.clear(); - this.isLoginPending = true; - } - - public void getModule(String id, final NetworkCallback callback, final NetworkErrorCallback error) { - getModule(id, callback, error, false); - } - - public void getModule(String id, final NetworkCallback callback, final NetworkErrorCallback error, boolean forceRefresh) { - getModulePart(modules -> modules.getModule(id, saveOnCallback(modules, callback, forceRefresh), errorOnCallback(error), forceRefresh)); - } - - public void getModuleList(final NetworkCallback callback, final NetworkErrorCallback error) { - getModuleList(callback, error, false); - } - - public void getModuleList(final NetworkCallback callback, final NetworkErrorCallback error, boolean forceRefresh) { - getModulePart(modules -> modules.getModuleList(saveOnCallback(modules, callback, forceRefresh), errorOnCallback(error), forceRefresh)); - } - - public void getModuleDetails(Modules.Module module, final NetworkCallback> callback, final NetworkErrorCallback error) { - getModuleDetails(module, callback, error, false); - } - - public void getModuleDetails(Modules.Module module, final NetworkCallback> callback, final NetworkErrorCallback error, boolean forceRefresh) { - getModulePart(modules -> modules.getModuleDetails(module, saveOnCallback(modules, callback, forceRefresh), errorOnCallback(error), forceRefresh)); - } - - public void getModuleAnnouncements(Modules.Module module, final NetworkCallback callback, final NetworkErrorCallback error) { - getModuleAnnouncements(module, callback, error, false); - } - - public void getModuleAnnouncements(Modules.Module module, final NetworkCallback callback, final NetworkErrorCallback error, boolean forceRefresh) { - getModulePart(modules -> modules.getAnnouncements(module, saveOnCallback(modules, callback, forceRefresh), errorOnCallback(error), forceRefresh)); - } - - public void getModuleAssignments(Modules.Module module, final NetworkCallback callback, final NetworkErrorCallback error) { - getModuleAssignments(module, callback, error, false); - } - - public void getModuleAssignments(Modules.Module module, final NetworkCallback callback, final NetworkErrorCallback error, boolean forceRefresh) { - getModulePart(modules -> modules.getAssignments(module, saveOnCallback(modules, callback, forceRefresh), errorOnCallback(error), forceRefresh)); - } - - public void getModuleEvents(Modules.Module module, final NetworkCallback callback, final NetworkErrorCallback error) { - getModuleEvents(module, callback, error, false); - } - - public void getModuleEvents(Modules.Module module, final NetworkCallback callback, final NetworkErrorCallback error, boolean forceRefresh) { - getModulePart(modules -> modules.getEvents(module, saveOnCallback(modules, callback, forceRefresh), errorOnCallback(error), forceRefresh)); - } - - public void getModuleGradebook(Modules.Module module, final NetworkCallback callback, final NetworkErrorCallback error) { - getModuleGradebook(module, callback, error, false); - } - - public void getModuleGradebook(Modules.Module module, final NetworkCallback callback, final NetworkErrorCallback error, boolean forceRefresh) { - getModulePart(modules -> modules.getGradebook(module, saveOnCallback(modules, callback, forceRefresh), errorOnCallback(error), forceRefresh)); - } - - public void getModuleResources(Modules.Module module, final NetworkCallback callback, final NetworkErrorCallback error) { - getModuleResources(module, callback, error, false); - } - - public void getModuleResources(Modules.Module module, final NetworkCallback callback, final NetworkErrorCallback error, boolean forceRefresh) { - getModulePart(modules -> modules.getResources(module, saveOnCallback(modules, callback, forceRefresh), errorOnCallback(error), forceRefresh)); - } - - public void getResourceFile(final NetworkCallback callback, final NetworkErrorCallback error, String filename, String url, String moduleName, boolean downloadNew){ - getModulePart(modules -> modules.getResourceFile(callback, errorOnCallback(error),filename, url, moduleName, downloadNew)); - } - - public void testLogin(BooleanFunction callback) { - getLastToken(lastToken -> { - TestLogin.testLogin(context, lastToken, success -> callback.apply(true), error -> callback.apply(false)); - }); - } - - - - - - private void getModulePart(ModListFunction func) { - this.getLastToken(token -> { - KVVModuleList modules = (KVVModuleList) addons.get("modules"); - if (modules == null) { - modules = new KVVModuleList(this.context, token); - addons.put("modules", modules); - } - func.apply(modules); - }); - } - - private NetworkCallback saveOnCallback(KVVModuleList modules, NetworkCallback callback, boolean forceRefresh){ - return (success -> { - try { - modules.saveModulesOffline(this.context); - } catch (IOException e) { - e.printStackTrace(); - } - if (forceRefresh) - mListener.onRefreshCompleted(false); - callback.onResponse(success); - }); - } - - private NetworkErrorCallback errorOnCallback(NetworkErrorCallback errorCallback){ - return (error -> { - if (error.getHttpStatus() == 401 || error.getHttpStatus() == 403) - mListener.loginTokenInvalid(true); - else - mListener.onRefreshCompleted(true); - log.e(error); - errorCallback.onError(error); - }); - } - - @FunctionalInterface - interface ModListFunction { - void apply(KVVModuleList mod); - } - - @FunctionalInterface - public interface BooleanFunction { - void apply(boolean isSuccess); - } - - private void getLastToken(LastTokenCallback lastTokenCallback) { - if (this.isLoginPending) { - this.updatingList.add(lastTokenCallback); - } else { - lastTokenCallback.onReceived(this.lastToken); - } - } - - private void endUpdate() { - this.isLoginPending = false; - for (LastTokenCallback s: this.updatingList) { - s.onReceived(this.lastToken); - } + private interface ModuleCreatorInterface { + @NotNull Object create(); } } diff --git a/app/src/main/java/de/sebse/fuplanner/services/KVV/KVVListener.java b/app/src/main/java/de/sebse/fuplanner/services/KVV/KVVListener.java new file mode 100644 index 0000000..402899f --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner/services/KVV/KVVListener.java @@ -0,0 +1,20 @@ +package de.sebse.fuplanner.services.KVV; + +import com.android.volley.NetworkResponse; + +import de.sebse.fuplanner.services.GoogleAuth.Credentials; +import de.sebse.fuplanner.services.KVV.types.LoginToken; +import de.sebse.fuplanner.tools.network.NetworkCallback; +import de.sebse.fuplanner.tools.network.NetworkErrorCallback; + +public interface KVVListener { + void getCredentials(NetworkCallback callback, NetworkErrorCallback error); + + void onLogin(LoginToken token, boolean enteringOnlineMode); + + void onLogout(); + + void onModuleListChange(); + + void onKVVNetworkResponse(NetworkResponse error); +} diff --git a/app/src/main/java/de/sebse/fuplanner/services/KVV/KVVModuleList.java b/app/src/main/java/de/sebse/fuplanner/services/KVV/KVVModuleList.java deleted file mode 100644 index 6196088..0000000 --- a/app/src/main/java/de/sebse/fuplanner/services/KVV/KVVModuleList.java +++ /dev/null @@ -1,643 +0,0 @@ -package de.sebse.fuplanner.services.KVV; - -import android.content.Context; -import android.os.Build; -import android.os.Environment; -import android.text.Html; -import android.text.Spanned; -import android.util.Pair; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; -import java.util.regex.MatchResult; - -import de.sebse.fuplanner.services.KVV.types.Announcement; -import de.sebse.fuplanner.services.KVV.types.Assignment; -import de.sebse.fuplanner.services.KVV.types.AssignmentList; -import de.sebse.fuplanner.services.KVV.types.Event; -import de.sebse.fuplanner.services.KVV.types.EventList; -import de.sebse.fuplanner.services.KVV.types.Gradebook; -import de.sebse.fuplanner.services.KVV.types.Lecturer; -import de.sebse.fuplanner.services.KVV.types.LoginToken; -import de.sebse.fuplanner.services.KVV.types.Modules; -import de.sebse.fuplanner.services.KVV.types.Resource; -import de.sebse.fuplanner.tools.AsyncQueue; -import de.sebse.fuplanner.tools.Regex; -import de.sebse.fuplanner.tools.network.HTTPService; -import de.sebse.fuplanner.tools.network.NetworkCallback; -import de.sebse.fuplanner.tools.network.NetworkError; -import de.sebse.fuplanner.tools.network.NetworkErrorCallback; -import de.sebse.fuplanner.tools.network.Result; - -import static de.sebse.fuplanner.services.KVV.TestLogin.testLogin; - -/** - * Created by sebastian on 29.10.17. - */ - -class KVVModuleList extends HTTPService { - private final LoginToken token; - private Modules moduleList; - private final AsyncQueue queueModuleDetails = new AsyncQueue(); - - KVVModuleList(Context context, LoginToken token) { - super(context); - this.token = token; - try { - Modules modules = Modules.load(context); - if (token == null || token.isSameUser(modules.getToken())) - this.moduleList = modules; - } catch (IOException e) { - e.printStackTrace(); - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } - } - - - - - void getModuleList(final NetworkCallback callback, final NetworkErrorCallback errorCallback, boolean forceRefresh) { - queueModuleDetails.add("list", () -> { - if (this.moduleList != null && !forceRefresh) { - callback.onResponse(this.moduleList); - queueModuleDetails.next("list"); - return; - } - this.getModuleListUpgrade(success -> { - if (this.moduleList == null) - this.moduleList = success; - else - this.moduleList.updateList(success); - callback.onResponse(this.moduleList); - queueModuleDetails.next("list"); - }, queueModuleDetails.check("list", errorCallback)); - }); - } - - private void getModuleListUpgrade(final NetworkCallback callback, final NetworkErrorCallback errorCallback) { - if (token == null) { - errorCallback.onError(new NetworkError(101105, 500, "Currently running in offline mode!")); - return; - } - get("https://kvv.imp.fu-berlin.de/direct/site.json", token.getCookies(), response -> { - String body = response.getParsed(); - if (body == null) { - errorCallback.onError(new NetworkError(101101, 403, "No module list retrieved!")); - return; - } - Modules modules = new Modules(token); - JSONArray sites; - try { - JSONObject json = new JSONObject(body); - sites = json.getJSONArray("site_collection"); - } catch (JSONException e) { - e.printStackTrace(); - errorCallback.onError(new NetworkError(101102, 403, "Cannot parse module list!")); - return; - } - for (int i = 0; i < sites.length(); i++) { - try { - JSONObject site = sites.getJSONObject(i); - String semester = site.getJSONObject("props").getString("term_eid"); - HashSet lvNumbers = new HashSet<>(); - for (MatchResult matchResult : Regex.allMatches("[0-9]+", site.getJSONObject("props").getString("kvv_lvnumbers"))) { - lvNumbers.add(matchResult.group()); - } - String title = site.getString("entityTitle"); - HashSet lecturers = new HashSet<>(); - for (String lecturer : site.getJSONObject("props").getString("kvv_lecturers").split("#")) { - if (lecturer.length() > 2) - lecturers.add(new Lecturer(lecturer)); - } - String type = site.getJSONObject("props").getString("kvv_coursetype"); - String description = site.optString("description", ""); - description = String.valueOf(fromHtml(description)); - String id = site.getString("id"); - modules.addModule(semester, lvNumbers, title, lecturers, type, description, id); - } catch (JSONException e) { - log.e("Cannot parse module! ID:", i, sites); - e.printStackTrace(); - } catch (NoSuchFieldException e) { - log.e("Cannot parse module! ID:", i, sites); - e.printStackTrace(); - } - } - // Empty module *may be* because token is invalid -> check - if (modules.size() == 0) - testLogin(getContext(), token, token -> callback.onResponse(modules), errorCallback); - else - callback.onResponse(modules); - }, error -> errorCallback.onError(new NetworkError(101104, error.networkResponse.statusCode, "Cannot get module list!"))); - } - - void deleteModulesOffline(Context context) { - if (this.moduleList != null) - this.moduleList.delete(context); - } - - void saveModulesOffline(Context context) throws IOException { - if (this.moduleList != null) - this.moduleList.save(context); - } - - void getModule(String id, final NetworkCallback callback, final NetworkErrorCallback errorCallback, boolean forceRefresh) { - this.getModuleList(success -> { - callback.onResponse(success.get(id)); - }, errorCallback, forceRefresh); - } - - - - - - - void getModuleDetails(Modules.Module module, final NetworkCallback> callback, final NetworkErrorCallback errorCallback, boolean forceRefresh) { - AtomicInteger returns = new AtomicInteger(0); - AtomicReference lastError = new AtomicReference<>(null); - final AtomicInteger items = new AtomicInteger(0); - NetworkCallback successCb = success -> { - returns.getAndIncrement(); - callback.onResponse(Pair.create(module, false)); - if (returns.get() == items.get()) { - callback.onResponse(Pair.create(module, true)); - if (lastError.get() != null) - errorCallback.onError(lastError.get()); - } - }; - NetworkErrorCallback errorCb = error -> { - lastError.set(error); - returns.getAndIncrement(); - if (returns.get() == items.get()) { - callback.onResponse(Pair.create(module, true)); - if (lastError.get() != null) - errorCallback.onError(lastError.get()); - } - }; - Runnable[] methods = { - () -> this.getAssignments(module, successCb, errorCb, forceRefresh), - () -> this.getEvents(module, successCb, errorCb, forceRefresh), - () -> this.getAnnouncements(module, successCb, errorCb, forceRefresh), - () -> this.getGradebook(module, successCb, errorCb, forceRefresh), - () -> this.getResources(module, successCb, errorCb, forceRefresh) - }; - items.set(methods.length); - for (Runnable method: methods) { - method.run(); - } - } - - - - - - - void getAnnouncements(Modules.Module module, final NetworkCallback callback, final NetworkErrorCallback errorCallback, boolean forceRefresh) { - queueModuleDetails.add(module.getID(), () -> { - if (module.announcements != null && !forceRefresh) { - callback.onResponse(module); - queueModuleDetails.next(module.getID()); - return; - } - getAnnouncementsUpgrade(module.getID(), success -> { - module.announcements = success; - callback.onResponse(module); - queueModuleDetails.next(module.getID()); - }, queueModuleDetails.check(module.getID(), errorCallback)); - }); - } - - private void getAnnouncementsUpgrade(String ID, final NetworkCallback> callback, final NetworkErrorCallback errorCallback) { - if (token == null) { - errorCallback.onError(new NetworkError(101204, 500, "Currently running in offline mode!")); - return; - } - get(String.format("https://kvv.imp.fu-berlin.de/direct/announcement/site/%s.json?n=999999&d=999999999", ID), token.getCookies(), response -> { - String body = response.getParsed(); - if (body == null) { - errorCallback.onError(new NetworkError(101201, 403, "No announcements retrieved!")); - return; - } - ArrayList announcements = new ArrayList<>(); - try { - JSONObject json = new JSONObject(body); - JSONArray sites = json.getJSONArray("announcement_collection"); - - for (int i = 0; i < sites.length(); i++) { - JSONObject site = sites.getJSONObject(i); - String id = site.getString("announcementId"); - String title = site.getString("title"); - String text = site.getString("body"); - text = String.valueOf(fromHtml(text)); - String createdBy = site.getString("createdByDisplayName"); - long createdOn = site.getLong("createdOn"); - - - - // Extract attachment links - JSONArray attachments = site.getJSONArray("attachments"); - ArrayList urls = new ArrayList<>(); - for (int j =0; j check - if (announcements.size() == 0) - testLogin(getContext(), token, token -> callback.onResponse(announcements), errorCallback); - else - callback.onResponse(announcements); - }, error -> errorCallback.onError(new NetworkError(101203, error.networkResponse.statusCode, "Cannot get announcements!"))); - } - - - - - - void getAssignments(Modules.Module module, final NetworkCallback callback, final NetworkErrorCallback errorCallback, boolean forceRefresh) { - queueModuleDetails.add(module.getID(), () -> { - if (module.assignments != null && !forceRefresh) { - callback.onResponse(module); - queueModuleDetails.next(module.getID()); - return; - } - getAssignmentsUpgrade(module.getID(), success -> { - module.assignments = success; - callback.onResponse(module); - queueModuleDetails.next(module.getID()); - }, queueModuleDetails.check(module.getID(), errorCallback)); - }); - } - - private void getAssignmentsUpgrade(String ID, final NetworkCallback callback, final NetworkErrorCallback errorCallback) { - if (token == null) { - errorCallback.onError(new NetworkError(101304, 500, "Currently running in offline mode!")); - return; - } - get(String.format("https://kvv.imp.fu-berlin.de/direct/assignment/site/%s.json", ID), token.getCookies(), response ->{ - String body = response.getParsed(); - if (body == null) { - errorCallback.onError(new NetworkError(101301, 403, "No assignments retrieved!")); - return; - } - AssignmentList assignments = new AssignmentList(); - try { - JSONObject json = new JSONObject(body); - JSONArray sites = json.getJSONArray("assignment_collection"); - - for (int i = 0; i < sites.length(); i++) { - JSONObject site = sites.getJSONObject(i); - String id = site.getString("id"); - String title = site.getString("title"); - String instructions = site.getString("instructions"); - instructions = String.valueOf(fromHtml(instructions)); - long dueTime = site.getJSONObject("dueTime").getLong("time"); - String gradebookItemName = site.optString("gradebookItemName", null); - String gradeScale = site.getString("gradeScale"); - JSONArray attachments = site.getJSONArray("attachments"); - ArrayList urls = new ArrayList<>(); - for (int j = 0; j check - if (assignments.size() == 0) - testLogin(getContext(), token, token -> callback.onResponse(assignments), errorCallback); - else - callback.onResponse(assignments); - }, error -> errorCallback.onError(new NetworkError(101303, error.networkResponse.statusCode, "Cannot get assignments!"))); - - - } - - - - - - - void getEvents(Modules.Module module, final NetworkCallback callback, final NetworkErrorCallback errorCallback, boolean forceRefresh) { - queueModuleDetails.add(module.getID(), () -> { - if (module.events != null && !forceRefresh) { - callback.onResponse(module); - queueModuleDetails.next(module.getID()); - return; - } - getEventsUpgrade(module.getID(), success -> { - module.events = success; - callback.onResponse(module); - queueModuleDetails.next(module.getID()); - }, queueModuleDetails.check(module.getID(), errorCallback)); - }); - } - - private void getEventsUpgrade(String ID, final NetworkCallback callback, final NetworkErrorCallback errorCallback) { - if (token == null) { - errorCallback.onError(new NetworkError(101404, 500, "Currently running in offline mode!")); - return; - } - get(String.format("https://kvv.imp.fu-berlin.de/direct/calendar/site/%s.json?detailed=true", ID), token.getCookies(), response -> { - String body = response.getParsed(); - if (body == null) { - errorCallback.onError(new NetworkError(101401, 403, "No calendar retrieved!")); - return; - } - EventList events = new EventList(); - try { - JSONObject json = new JSONObject(body); - JSONArray sites = json.getJSONArray("calendar_collection"); - - for (int i = 0; i < sites.length(); i++) { - JSONObject site = sites.getJSONObject(i); - String id = site.getString("eventId"); - String type = site.getString("type"); - String title = site.getString("title"); - String siteId = site.getString("siteId"); - long duration = site.getLong("duration"); - long firstTime = site.getJSONObject("firstTime").getLong("time"); - String location = site.getString("location"); - events.add(new Event(id, type, title, duration, firstTime, siteId, location)); - } - } catch (JSONException e) { - e.printStackTrace(); - errorCallback.onError(new NetworkError(101402, 403, "Cannot parse calendar entries!")); - return; - } - events.sort(); - // Empty events *may be* because token is invalid -> check - if (events.size() == 0) - testLogin(getContext(), token, token -> callback.onResponse(events), errorCallback); - else - callback.onResponse(events); - }, error -> errorCallback.onError(new NetworkError(101403, error.networkResponse.statusCode, "Cannot get calendar entries!"))); - } - - - - - - void getGradebook(Modules.Module module, final NetworkCallback callback, final NetworkErrorCallback errorCallback, boolean forceRefresh) { - queueModuleDetails.add(module.getID(), () -> { - if (module.gradebook != null && !forceRefresh) { - callback.onResponse(module); - queueModuleDetails.next(module.getID()); - return; - } - getGradebookUpgrade(module.getID(), success -> { - module.gradebook = success; - callback.onResponse(module); - queueModuleDetails.next(module.getID()); - }, queueModuleDetails.check(module.getID(), errorCallback)); - }); - } - - private void getGradebookUpgrade(String ID, final NetworkCallback> callback, final NetworkErrorCallback errorCallback) { - if (token == null) { - errorCallback.onError(new NetworkError(101504, 500, "Currently running in offline mode!")); - return; - } - get(String.format("https://kvv.imp.fu-berlin.de/direct/gradebook/site/%s.json", ID ), token.getCookies(), response ->{ - String body = response.getParsed(); - if (body == null) { - errorCallback.onError(new NetworkError(101501, 403, "No assignments retrieved!")); - return; - } - ArrayList gradebook = new ArrayList<>(); - try { - JSONObject json = new JSONObject(body); - JSONArray sites = json.getJSONArray("assignments"); - - for (int i = 0; i < sites.length(); i++) { - JSONObject site = sites.getJSONObject(i); - double grade = site.optDouble("grade", 0); - String itemName = site.optString("itemName", null); - double maxPoints = site.optDouble("points", -1); - - gradebook.add(0, new Gradebook(itemName, grade, maxPoints)); - } - - }catch (JSONException e) { - e.printStackTrace(); - errorCallback.onError(new NetworkError(101502, 403, "Cannot parse gradebook for announcements!")); - return; - } - callback.onResponse(gradebook); - }, error -> errorCallback.onError(new NetworkError(101503, error.networkResponse.statusCode, "Cannot get gradebook for assignments!"))); - - } - - public void getResources(Modules.Module module, final NetworkCallback callback, final NetworkErrorCallback errorCallback, boolean forceRefresh) { - queueModuleDetails.add(module.getID(), () -> { - if (module.resources != null && !forceRefresh) { - callback.onResponse(module); - queueModuleDetails.next(module.getID()); - return; - } - getResourcesUpgrade(module.getID(), success -> { - module.resources = success; - callback.onResponse(module); - queueModuleDetails.next(module.getID()); - }, queueModuleDetails.check(module.getID(), errorCallback)); - }); - } - - private void getResourcesUpgrade(String ID, final NetworkCallback> callback, final NetworkErrorCallback errorCallback) { - if (token == null) { - errorCallback.onError(new NetworkError(101604, 500, "Currently running in offline mode!")); - return; - } - get(String.format("https://kvv.imp.fu-berlin.de/direct/content/site/%s.json", ID ), token.getCookies(), response ->{ - String body = response.getParsed(); - if (body == null) { - errorCallback.onError(new NetworkError(101601, 403, "No resources retrieved!")); - return; - } - ArrayList resources = new ArrayList<>(); - try { - JSONObject json = new JSONObject(body); - JSONArray sites = json.getJSONArray("content_collection"); - - for (int i = 0; i < sites.length(); i++) { - JSONObject site = sites.getJSONObject(i); - String author = site.getString("author"); - String title = site.getString("title"); - long modifiedDate = site.getLong("modifiedDate"); - String url = site.getString("url"); - boolean visible = site.getBoolean("visible"); - String type = site.getString("type"); - String container = site.getString("container"); - if (type.equals("collection")){ - resources.add(new Resource.Folder(author, title, modifiedDate, url, visible, container)); - } - else { - resources.add(new Resource.File(author, title, modifiedDate, url, visible, container, type)); - } - - } - - } catch (JSONException e) { - e.printStackTrace(); - errorCallback.onError(new NetworkError(101602, 403, "Cannot parse resources!")); - return; - } - - ArrayList root = new ArrayList<>(); - // Generate folder structure - for (Resource res: resources) { - if (!res.getContainer().equals("/content/group/")) { - if (res.getContainer().equals("/content/group/"+ID+"/")){ - // if file in root folder - root.add(res); - } else { - // in sub folder - for (Resource res2: resources) { - if (res2.getUrl().endsWith(res.getContainer()) && res2 instanceof Resource.Folder) { - // Append File/Folder to list - ((Resource.Folder) res2).add(res); - } - } - } - } - } - - // Empty resources *may be* because token is invalid -> check - if (root.size() == 0) - testLogin(getContext(), token, token -> callback.onResponse(root), errorCallback); - else - callback.onResponse(root); - - }, error -> errorCallback.onError(new NetworkError(101603, error.networkResponse.statusCode, "Cannot get resources!"))); - - } - - void getResourceFile(final NetworkCallback callback, final NetworkErrorCallback errorCallback, String Filename, String url, String moduleName, boolean downloadNew) { - if (isExternalStorageReadable()){ - File f = new File(Environment.getExternalStoragePublicDirectory( - Environment.DIRECTORY_DOWNLOADS)+"/"+moduleName+"/"+Filename); - // check if file already downloaded -> do not download again - if (f.exists() && !downloadNew) { - callback.onResponse(f.getPath()); - return; - } - } - getResourceFileUpgrade(Filename, url , moduleName, callback, errorCallback); - } - - private void getResourceFileUpgrade(String filename, String url , String moduleName, final NetworkCallback callback, final NetworkErrorCallback errorCallback) { - if (token == null) { - errorCallback.onError(new NetworkError(101701, 500, "Currently running in offline mode!")); - return; - } - - get(url, token.getCookies(), response ->{ - - - if (Regex.has("\\.[Uu][Rr][Ll]$", url)){ - // Return redirected URL - String path = response.getHeaders().get("Location"); - if (path == null){ - path = ""; - } - callback.onResponse(path); - } else if (response.getBytes()==null){ - testLogin(getContext(), token, token -> { - if (isExternalStorageWritable()) { - // try to download file again - get(url, token.getCookies(), response2 -> { - String path = saveFileInDownloads(filename, response2, moduleName); - callback.onResponse(path); - }, error -> errorCallback.onError(new NetworkError(101705, error.networkResponse.statusCode, "Cannot get file!"))); - } else { - errorCallback.onError(new NetworkError(101703, 403, "External storage not writable!")); - } - }, errorCallback); - } else if (isExternalStorageWritable()) { - String path = saveFileInDownloads(filename, response, moduleName); - callback.onResponse(path); - } else { - errorCallback.onError(new NetworkError(101704, 403, "External storage not writable!")); - } - }, error -> errorCallback.onError(new NetworkError(101702, error.networkResponse.statusCode, "Cannot get file!"))); - - - } - - - - - - - - /* Checks if external storage is available for read and write */ - private boolean isExternalStorageWritable() { - String state = Environment.getExternalStorageState(); - if (Environment.MEDIA_MOUNTED.equals(state)) { - return true; - } - log.w("File system: Writing not possible!"); - return false; - } - - /* Checks if external storage is available to at least read */ - private boolean isExternalStorageReadable() { - String state = Environment.getExternalStorageState(); - if (Environment.MEDIA_MOUNTED.equals(state) || - Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) { - return true; - } - log.w("File system: Reading not possible!"); - return false; - } - private String saveFileInDownloads(String filename, Result fileResult, String moduleName) { - // Saves file in folder: DOWNLOADS/moduleName - File folder = new File(Environment.getExternalStoragePublicDirectory( - Environment.DIRECTORY_DOWNLOADS), moduleName); - if (!folder.mkdirs()) { - log.w( "Directory not created", folder.toString()); - } - String path = ""; - try { - // TODO check if enough storage space is available - FileOutputStream out = new FileOutputStream(folder.getPath()+"/"+filename); - out.write(fileResult.getBytes()); - out.close(); - path = folder.getPath()+"/"+filename; - } catch (Exception e) { - log.w("File not saved!"); - e.printStackTrace(); - } - return path; - } - - @SuppressWarnings("deprecation") - public static Spanned fromHtml(String html){ - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - return Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY); - } else { - return Html.fromHtml(html); - } - } - - -} diff --git a/app/src/main/java/de/sebse/fuplanner/services/KVV/LastTokenCallback.java b/app/src/main/java/de/sebse/fuplanner/services/KVV/LastTokenCallback.java deleted file mode 100644 index 9740b1e..0000000 --- a/app/src/main/java/de/sebse/fuplanner/services/KVV/LastTokenCallback.java +++ /dev/null @@ -1,11 +0,0 @@ -package de.sebse.fuplanner.services.KVV; - -import de.sebse.fuplanner.services.KVV.types.LoginToken; - -/** - * Created by sebastian on 31.01.18. - */ - -interface LastTokenCallback { - void onReceived(LoginToken token); -} diff --git a/app/src/main/java/de/sebse/fuplanner/services/KVV/KVVLogin.java b/app/src/main/java/de/sebse/fuplanner/services/KVV/Login.java similarity index 72% rename from app/src/main/java/de/sebse/fuplanner/services/KVV/KVVLogin.java rename to app/src/main/java/de/sebse/fuplanner/services/KVV/Login.java index 03604a7..5f6d21d 100644 --- a/app/src/main/java/de/sebse/fuplanner/services/KVV/KVVLogin.java +++ b/app/src/main/java/de/sebse/fuplanner/services/KVV/Login.java @@ -2,69 +2,191 @@ package de.sebse.fuplanner.services.KVV; import android.content.Context; + +import org.jetbrains.annotations.NotNull; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.FileNotFoundException; import java.io.IOException; import java.util.HashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; +import androidx.annotation.Nullable; import de.sebse.fuplanner.services.KVV.types.LoginToken; import de.sebse.fuplanner.tools.network.HTTPService; import de.sebse.fuplanner.tools.network.NetworkCallback; import de.sebse.fuplanner.tools.network.NetworkError; import de.sebse.fuplanner.tools.network.NetworkErrorCallback; -import static de.sebse.fuplanner.services.KVV.TestLogin.testLogin; +public class Login extends HTTPService { + private KVVListener mListener; + @Nullable private LoginToken mToken; + private boolean mLoginPending = false; + private boolean mOnlineMode = false; -/** - * Created by sebastian on 24.10.17. - */ - -class KVVLogin extends HTTPService { - private LoginToken loginToken; - - KVVLogin(Context context) { + Login(KVVListener listener, Context context) { super(context); + this.mListener = listener; + } + + public void doOnlineLogin(@NotNull String username, @NotNull String password, NetworkCallback callback, NetworkErrorCallback errorCallback) { + if (mLoginPending) { + errorCallback.onError(new NetworkError(100160, -1, "Login already pending!")); + } + mLoginPending = true; + doLogin(username, password, token -> { + testLoginToken(token, token2 -> { + setToken(token2, true); + mLoginPending = false; + callback.onResponse(token2); + }, error -> { + mLoginPending = false; + errorCallback.onError(error); + }); + }, error -> { + mLoginPending = false; + errorCallback.onError(error); + }); + } + + public boolean restoreOnlineLogin() { + return restoreLogin(true); + } + + public boolean doOfflineLogin() { + return restoreLogin(false); + } + + private boolean restoreLogin(boolean enteringOnlineMode) { + if (mLoginPending || mToken != null) + return false; + mLoginPending = true; + boolean result = false; try { - this.loginToken = LoginToken.load(context); + result = setToken(LoginToken.load(getContext()), enteringOnlineMode); + } catch (FileNotFoundException ignored) { } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } + mLoginPending = false; + return result; } - LoginToken easyLogin() { - return this.loginToken; + public boolean isOfflineStoredAvailable() { + try { + LoginToken load = LoginToken.load(getContext()); + return load != null; + } catch (FileNotFoundException ignored) { + } catch (IOException e) { + e.printStackTrace(); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + return false; } - void login(String username, String password, NetworkCallback callback, NetworkErrorCallback errorCallback) { - if (this.loginToken != null) { - if (this.loginToken.getUsername().equals(username)) { - testLogin(getContext(), this.loginToken, success -> callback.onResponse(this.loginToken), error -> { - this.loginToken = null; - login(username, password, callback, errorCallback); - }); - } else { - this.loginToken = null; - login(username, password, callback, errorCallback); + public boolean logout(boolean delete) { + if (mLoginPending) + return false; + if (mToken == null) + return true; + if (delete) + mToken.delete(getContext()); + mToken = null; + return handleCallbacks(); + } + + public boolean isLoginPending() { + return mLoginPending; + } + + public boolean isLoggedIn() { + return mToken != null; + } + + public boolean isInOfflineMode() { + return isLoggedIn() && !mOnlineMode; + } + + public boolean isInOnlineMode() { + return isLoggedIn() && mOnlineMode; + } + + void testLoginToken(@NotNull NetworkCallback callback, @NotNull NetworkErrorCallback errorCallback) { + if (mToken == null) { + errorCallback.onError(new NetworkError(100173, -1, "Not logged in!")); + return; + } + testLoginToken(mToken, callback, errorCallback); + } + + private void testLoginToken(@NotNull LoginToken token, @NotNull NetworkCallback callback, @NotNull NetworkErrorCallback errorCallback) { + get(String.format("https://kvv.imp.fu-berlin.de/direct/profile/%s.json", token.getUsername()), token.getCookies(), response -> { + String body = response.getParsed(); + if (body == null) { + errorCallback.onError(new NetworkError(100172, 403, "Testing login failed!")); + return; } + try { + JSONObject json = new JSONObject(body); + String displayName = json.getString("displayName"); + String email = json.getString("email"); + token.setAdditionals(displayName, email); + callback.onResponse(token); + } catch (JSONException e) { + errorCallback.onError(new NetworkError(100171, 403, "Cannot parse profile!")); + } + }, error -> errorCallback.onError(new NetworkError(100170, error.networkResponse.statusCode, "Testing login failed!"))); + } + + @Nullable public LoginToken getLoginToken() { + return mToken; + } + + void refreshLogin(NetworkCallback success, NetworkErrorCallback error) { + mListener.getCredentials(credentials -> { + doOnlineLogin(credentials.getUsername(), credentials.getPassword(), success, error); + }, e -> { + logout(false); + error.onError(e); + }); + } + + + + private boolean handleCallbacks() { + if (mToken != null) { + mListener.onLogin(mToken, mOnlineMode); + return true; } else { - doLogin(username, password, token -> { - this.loginToken = token; - testLogin(getContext(), this.loginToken, success -> callback.onResponse(this.loginToken), errorCallback); - }, errorCallback); + mListener.onLogout(); + return false; } } - void deleteOffline() { - if (this.loginToken != null) - this.loginToken.delete(getContext()); + private boolean setToken(@Nullable LoginToken token, boolean enteringOnlineMode) { + if (token == null) + return false; + boolean isOnlyRefresh = mToken != null; + mToken = token; + if (enteringOnlineMode) { + try { + mToken.save(getContext()); + } catch (IOException e) { + e.printStackTrace(); + } + } + mOnlineMode = enteringOnlineMode; + return isOnlyRefresh || handleCallbacks(); } - void saveOffline() throws IOException { - if (this.loginToken != null) - this.loginToken.save(getContext()); - } + + + @@ -220,7 +342,7 @@ class KVVLogin extends HTTPService { errorCallback.onError(new NetworkError(100143, -1, "Error on getting SAML response!")); return; } - + HashMap object = new HashMap<>(); Pattern pattern = Pattern.compile("ss:mem:([0-9a-f]+)"); @@ -320,5 +442,3 @@ class KVVLogin extends HTTPService { return result; } } - - diff --git a/app/src/main/java/de/sebse/fuplanner/services/KVV/Modules.java b/app/src/main/java/de/sebse/fuplanner/services/KVV/Modules.java new file mode 100644 index 0000000..8c8b54a --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner/services/KVV/Modules.java @@ -0,0 +1,81 @@ +package de.sebse.fuplanner.services.KVV; + +import android.content.Context; + +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; + +public class Modules { + private final HashMap mAddons = new HashMap<>(); + private ModulesList mList = null; + private final Login mLogin; + private KVVListener mListener; + private final Context context; + + Modules(Login login, KVVListener listener, Context context) { + this.mLogin = login; + this.mListener = listener; + this.context = context; + } + + @NotNull + public ModulesDetails details() { + return (ModulesDetails) addAndGet("details", () -> { + PartModules[] parts = {announcements(), assignments(), events(), gradebook(), resources()}; + return new ModulesDetails(mLogin, list(), context, parts); + }); + } + + @NotNull + public ModulesAnnouncements announcements() { + return (ModulesAnnouncements) addAndGet("announcements", () -> new ModulesAnnouncements(mLogin, list(), context)); + } + + @NotNull + public ModulesAssignments assignments() { + return (ModulesAssignments) addAndGet("assignments", () -> new ModulesAssignments(mLogin, list(), context)); + } + + @NotNull + public ModulesEvents events() { + return (ModulesEvents) addAndGet("events", () -> new ModulesEvents(mLogin, list(), context)); + } + + @NotNull + public ModulesGradebook gradebook() { + return (ModulesGradebook) addAndGet("gradebook", () -> new ModulesGradebook(mLogin, list(), context)); + } + + @NotNull + public ModulesResources resources() { + return (ModulesResources) addAndGet("resources", () -> new ModulesResources(mLogin, list(), context)); + } + + @NotNull + public ModulesList list() { + if (mList == null) { + mList = new ModulesList(mLogin, mListener, context); + mList.addErrorListener("Modules", error -> mListener.onKVVNetworkResponse(error.networkResponse)); + mList.addSuccessListener("Modules", success -> mListener.onKVVNetworkResponse(null)); + } + return mList; + } + + + @NotNull + private Part addAndGet(@NotNull String addon, @NotNull ModuleCreatorInterface creatorInterface) { + Part o = mAddons.get(addon); + if (o == null) { + o = creatorInterface.create(); + o.addErrorListener("Modules", error -> mListener.onKVVNetworkResponse(error.networkResponse)); + o.addSuccessListener("Modules", success -> mListener.onKVVNetworkResponse(null)); + mAddons.put(addon, o); + } + return o; + } + + private interface ModuleCreatorInterface { + @NotNull Part create(); + } +} diff --git a/app/src/main/java/de/sebse/fuplanner/services/KVV/ModulesAnnouncements.java b/app/src/main/java/de/sebse/fuplanner/services/KVV/ModulesAnnouncements.java new file mode 100644 index 0000000..b13df13 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner/services/KVV/ModulesAnnouncements.java @@ -0,0 +1,91 @@ +package de.sebse.fuplanner.services.KVV; + +import android.content.Context; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; + +import de.sebse.fuplanner.services.KVV.types.Announcement; +import de.sebse.fuplanner.services.KVV.types.Modules; +import de.sebse.fuplanner.tools.network.NetworkCallback; +import de.sebse.fuplanner.tools.network.NetworkError; +import de.sebse.fuplanner.tools.network.NetworkErrorCallback; + +public class ModulesAnnouncements extends PartModules> { + + ModulesAnnouncements(Login login, ModulesList list, Context context) { + super(login, list, context); + } + + @Override + protected ArrayList getPart(Modules.Module module) { + return module.announcements; + } + + @Override + protected boolean setPart(Modules.Module module, ArrayList part) { + boolean changed = module.announcements == null || module.announcements.hashCode() != part.hashCode(); + module.announcements = part; + return changed; + } + + @Override + protected void upgrade(final String ID, final NetworkCallback> callback, final NetworkErrorCallback errorCallback) { + if (!mLogin.isInOnlineMode() || mLogin.getLoginToken() == null) { + errorCallback.onError(new NetworkError(101204, 500, "Currently running in offline mode!")); + return; + } + super.get(String.format("https://kvv.imp.fu-berlin.de/direct/announcement/site/%s.json?n=999999&d=999999999", ID), mLogin.getLoginToken().getCookies(), response -> { + String body = response.getParsed(); + if (body == null) { + errorCallback.onError(new NetworkError(101201, 403, "No announcements retrieved!")); + return; + } + ArrayList announcements = new ArrayList<>(); + JSONArray sites; + try { + JSONObject json = new JSONObject(body); + sites = json.getJSONArray("announcement_collection"); + } catch (JSONException e) { + e.printStackTrace(); + errorCallback.onError(new NetworkError(101202, 403, "Cannot parse announcements!")); + return; + } + + for (int i = 0; i < sites.length(); i++) { + try { + JSONObject site = sites.getJSONObject(i); + String id = site.getString("announcementId"); + String title = site.getString("title"); + String text = site.getString("body"); + text = String.valueOf(fromHtml(text)); + String createdBy = site.getString("createdByDisplayName"); + long createdOn = site.getLong("createdOn"); + + // Extract attachment links + JSONArray attachments = site.getJSONArray("attachments"); + ArrayList urls = new ArrayList<>(); + for (int j = 0; j < attachments.length(); j++) { + urls.add(attachments.getJSONObject(j).optString("url", null)); + } + + announcements.add(new Announcement(id, title, text, createdBy, createdOn, urls)); + } catch (JSONException e) { + log.e(new NetworkError(101205, 403, "Cannot parse announcements!")); + log.e("ID:", i, "JSON:", sites); + e.printStackTrace(); + return; + } + } + + // Empty announcements *may be* because token is invalid -> check + if (announcements.size() == 0) + mLogin.testLoginToken(token -> callback.onResponse(announcements), errorCallback); + else + callback.onResponse(announcements); + }, error -> errorCallback.onError(new NetworkError(101203, error.networkResponse.statusCode, "Cannot get announcements!"))); + } +} diff --git a/app/src/main/java/de/sebse/fuplanner/services/KVV/ModulesAssignments.java b/app/src/main/java/de/sebse/fuplanner/services/KVV/ModulesAssignments.java new file mode 100644 index 0000000..357be2c --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner/services/KVV/ModulesAssignments.java @@ -0,0 +1,90 @@ +package de.sebse.fuplanner.services.KVV; + +import android.content.Context; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; + +import de.sebse.fuplanner.services.KVV.types.Assignment; +import de.sebse.fuplanner.services.KVV.types.AssignmentList; +import de.sebse.fuplanner.services.KVV.types.Modules; +import de.sebse.fuplanner.tools.network.NetworkCallback; +import de.sebse.fuplanner.tools.network.NetworkError; +import de.sebse.fuplanner.tools.network.NetworkErrorCallback; + +public class ModulesAssignments extends PartModules { + + ModulesAssignments(Login login, ModulesList list, Context context) { + super(login, list, context); + } + + @Override + protected AssignmentList getPart(Modules.Module module) { + return module.assignments; + } + + @Override + protected boolean setPart(Modules.Module module, AssignmentList part) { + boolean changed = module.assignments == null || module.assignments.hashCode() != part.hashCode(); + module.assignments = part; + return changed; + } + + @Override + protected void upgrade(final String ID, final NetworkCallback callback, final NetworkErrorCallback errorCallback) { + if (!mLogin.isInOnlineMode() || mLogin.getLoginToken() == null) { + errorCallback.onError(new NetworkError(101304, 500, "Currently running in offline mode!")); + return; + } + get(String.format("https://kvv.imp.fu-berlin.de/direct/assignment/site/%s.json", ID), mLogin.getLoginToken().getCookies(), response -> { + String body = response.getParsed(); + if (body == null) { + errorCallback.onError(new NetworkError(101301, 403, "No assignments retrieved!")); + return; + } + AssignmentList assignments = new AssignmentList(); + JSONArray sites; + try { + JSONObject json = new JSONObject(body); + sites = json.getJSONArray("assignment_collection"); + } catch (JSONException e) { + e.printStackTrace(); + errorCallback.onError(new NetworkError(101302, 403, "Cannot parse assignments!")); + return; + } + + for (int i = 0; i < sites.length(); i++) { + try { + JSONObject site = sites.getJSONObject(i); + String id = site.getString("id"); + String title = site.getString("title"); + String instructions = site.getString("instructions"); + instructions = String.valueOf(fromHtml(instructions)); + long dueTime = site.getJSONObject("dueTime").getLong("time"); + String gradebookItemName = site.optString("gradebookItemName", null); + String gradeScale = site.getString("gradeScale"); + JSONArray attachments = site.getJSONArray("attachments"); + ArrayList urls = new ArrayList<>(); + for (int j = 0; j < attachments.length(); j++) { + urls.add(attachments.getJSONObject(j).getString("url")); + } + assignments.add(0, new Assignment(id, title, dueTime, gradebookItemName, gradeScale, urls, instructions)); + } catch (JSONException e) { + log.e(new NetworkError(101305, 403, "Cannot parse assignments!")); + e.printStackTrace(); + log.e("ID:", i, "JSON:", sites); + return; + } + } + + // Empty assignments *may be* because token is invalid -> check + if (assignments.size() == 0) + mLogin.testLoginToken(token -> callback.onResponse(assignments), errorCallback); + else + callback.onResponse(assignments); + }, error -> errorCallback.onError(new NetworkError(101303, error.networkResponse.statusCode, "Cannot get assignments!"))); + } +} diff --git a/app/src/main/java/de/sebse/fuplanner/services/KVV/ModulesDetails.java b/app/src/main/java/de/sebse/fuplanner/services/KVV/ModulesDetails.java new file mode 100644 index 0000000..a08fda4 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner/services/KVV/ModulesDetails.java @@ -0,0 +1,47 @@ +package de.sebse.fuplanner.services.KVV; + +import android.content.Context; +import android.util.Pair; + +import java.util.concurrent.atomic.AtomicReference; + +import de.sebse.fuplanner.services.KVV.types.Modules; +import de.sebse.fuplanner.tools.network.NetworkCallback; +import de.sebse.fuplanner.tools.network.NetworkError; +import de.sebse.fuplanner.tools.network.NetworkErrorCallback; + +final public class ModulesDetails extends Part> { + private final PartModules[] parts; + + ModulesDetails(Login login, ModulesList list, Context context, PartModules[] parts) { + super(login, list, context); + this.parts = parts; + } + + @Override + protected void recv(final Modules.Module module, final NetworkCallback> callback, final NetworkErrorCallback errorCallback, final boolean forceRefresh, final int retries) { + final int[] returned = {0}; + AtomicReference lastError = new AtomicReference<>(null); + NetworkCallback successCb = success -> { + returned[0] += 1; + callback.onResponse(Pair.create(module, false)); + if (returned[0] == parts.length) { + callback.onResponse(Pair.create(module, true)); + if (lastError.get() != null) + errorCallback.onError(lastError.get()); + } + }; + NetworkErrorCallback errorCb = error -> { + lastError.set(error); + returned[0] += 1; + if (returned[0] == parts.length) { + callback.onResponse(Pair.create(module, true)); + if (lastError.get() != null) + errorCallback.onError(lastError.get()); + } + }; + for (PartModules part: parts) { + part.recv(module, successCb, errorCb, forceRefresh, RETRY_COUNT); + } + } +} diff --git a/app/src/main/java/de/sebse/fuplanner/services/KVV/ModulesEvents.java b/app/src/main/java/de/sebse/fuplanner/services/KVV/ModulesEvents.java new file mode 100644 index 0000000..66ba5c1 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner/services/KVV/ModulesEvents.java @@ -0,0 +1,83 @@ +package de.sebse.fuplanner.services.KVV; + +import android.content.Context; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import de.sebse.fuplanner.services.KVV.types.Event; +import de.sebse.fuplanner.services.KVV.types.EventList; +import de.sebse.fuplanner.services.KVV.types.Modules; +import de.sebse.fuplanner.tools.network.NetworkCallback; +import de.sebse.fuplanner.tools.network.NetworkError; +import de.sebse.fuplanner.tools.network.NetworkErrorCallback; + +public class ModulesEvents extends PartModules { + + ModulesEvents(Login login, ModulesList list, Context context) { + super(login, list, context); + } + + @Override + protected EventList getPart(Modules.Module module) { + return module.events; + } + + @Override + protected boolean setPart(Modules.Module module, EventList part) { + boolean changed = module.events == null || module.events.hashCode() != part.hashCode(); + module.events = part; + return changed; + } + + @Override + protected void upgrade(final String ID, final NetworkCallback callback, final NetworkErrorCallback errorCallback) { + if (!mLogin.isInOnlineMode() || mLogin.getLoginToken() == null) { + errorCallback.onError(new NetworkError(101404, 500, "Currently running in offline mode!")); + return; + } + get(String.format("https://kvv.imp.fu-berlin.de/direct/calendar/site/%s.json?detailed=true", ID), mLogin.getLoginToken().getCookies(), response -> { + String body = response.getParsed(); + if (body == null) { + errorCallback.onError(new NetworkError(101401, 403, "No events retrieved!")); + return; + } + EventList events = new EventList(); + JSONArray sites; + try { + JSONObject json = new JSONObject(body); + sites = json.getJSONArray("calendar_collection"); + } catch (JSONException e) { + e.printStackTrace(); + errorCallback.onError(new NetworkError(101402, 403, "Cannot parse events!")); + return; + } + + for (int i = 0; i < sites.length(); i++) { + try { + JSONObject site = sites.getJSONObject(i); + String id = site.getString("eventId"); + String type = site.getString("type"); + String title = site.getString("title"); + String siteId = site.getString("siteId"); + long duration = site.getLong("duration"); + long firstTime = site.getJSONObject("firstTime").getLong("time"); + String location = site.getString("location"); + events.add(new Event(id, type, title, duration, firstTime, siteId, location)); + } catch (JSONException e) { + log.e(new NetworkError(101405, 403, "Cannot parse events!")); + e.printStackTrace(); + log.e("ID:", i, "JSON:", sites); + return; + } + } + + // Empty events *may be* because token is invalid -> check + if (events.size() == 0) + mLogin.testLoginToken(token -> callback.onResponse(events), errorCallback); + else + callback.onResponse(events); + }, error -> errorCallback.onError(new NetworkError(101403, error.networkResponse.statusCode, "Cannot get events!"))); + } +} diff --git a/app/src/main/java/de/sebse/fuplanner/services/KVV/ModulesGradebook.java b/app/src/main/java/de/sebse/fuplanner/services/KVV/ModulesGradebook.java new file mode 100644 index 0000000..d346289 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner/services/KVV/ModulesGradebook.java @@ -0,0 +1,76 @@ +package de.sebse.fuplanner.services.KVV; + +import android.content.Context; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; + +import de.sebse.fuplanner.services.KVV.types.Grade; +import de.sebse.fuplanner.services.KVV.types.Modules; +import de.sebse.fuplanner.tools.network.NetworkCallback; +import de.sebse.fuplanner.tools.network.NetworkError; +import de.sebse.fuplanner.tools.network.NetworkErrorCallback; + +public class ModulesGradebook extends PartModules> { + + ModulesGradebook(Login login, ModulesList list, Context context) { + super(login, list, context); + } + + @Override + protected ArrayList getPart(Modules.Module module) { + return module.gradebook; + } + + @Override + protected boolean setPart(Modules.Module module, ArrayList part) { + boolean changed = module.gradebook == null || module.gradebook.hashCode() != part.hashCode(); + module.gradebook = part; + return changed; + } + + @Override + protected void upgrade(final String ID, final NetworkCallback> callback, final NetworkErrorCallback errorCallback) { + if (!mLogin.isInOnlineMode() || mLogin.getLoginToken() == null) { + errorCallback.onError(new NetworkError(101504, 500, "Currently running in offline mode!")); + return; + } + super.get(String.format("https://kvv.imp.fu-berlin.de/direct/gradebook/site/%s.json", ID), mLogin.getLoginToken().getCookies(), response -> { + String body = response.getParsed(); + if (body == null) { + errorCallback.onError(new NetworkError(101501, 403, "No gradebook retrieved!")); + return; + } + ArrayList gradebook = new ArrayList<>(); + JSONArray sites; + try { + JSONObject json = new JSONObject(body); + sites = json.getJSONArray("assignments"); + } catch (JSONException e) { + e.printStackTrace(); + errorCallback.onError(new NetworkError(101502, 403, "Cannot parse gradebook!")); + return; + } + + for (int i = 0; i < sites.length(); i++) { + try { + JSONObject site = sites.getJSONObject(i); + double grade = site.optDouble("grade", 0); + String itemName = site.optString("itemName", null); + double maxPoints = site.optDouble("points", -1); + + gradebook.add(0, new Grade(itemName, grade, maxPoints)); + } catch (JSONException e) { + log.e(new NetworkError(101505, 403, "Cannot parse gradebook!")); + log.e("ID:", i, "JSON:", sites); + e.printStackTrace(); + return; + } + } + callback.onResponse(gradebook); + }, error -> errorCallback.onError(new NetworkError(101503, error.networkResponse.statusCode, "Cannot get gradebook!"))); + } +} diff --git a/app/src/main/java/de/sebse/fuplanner/services/KVV/ModulesList.java b/app/src/main/java/de/sebse/fuplanner/services/KVV/ModulesList.java new file mode 100644 index 0000000..cc5b8f9 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner/services/KVV/ModulesList.java @@ -0,0 +1,201 @@ +package de.sebse.fuplanner.services.KVV; + +import android.content.Context; + +import org.jetbrains.annotations.Nullable; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.regex.MatchResult; + +import de.sebse.fuplanner.services.KVV.types.Lecturer; +import de.sebse.fuplanner.services.KVV.types.Modules; +import de.sebse.fuplanner.services.KVV.types.Semester; +import de.sebse.fuplanner.tools.NewAsyncQueue; +import de.sebse.fuplanner.tools.Regex; +import de.sebse.fuplanner.tools.network.HTTPService; +import de.sebse.fuplanner.tools.network.NetworkCallback; +import de.sebse.fuplanner.tools.network.NetworkError; +import de.sebse.fuplanner.tools.network.NetworkErrorCallback; + +import static de.sebse.fuplanner.services.KVV.PartModules.RETRY_COUNT; + +public class ModulesList extends HTTPService { + private final Login mLogin; + private final KVVListener mListener; + @Nullable private Modules mModules; + private NewAsyncQueue mQueue = new NewAsyncQueue(); + + ModulesList(Login login, KVVListener listener, Context context) { + super(context); + this.mLogin = login; + this.mListener = listener; + restore(); + } + + @Nullable + public String getUsername() { + if (mModules != null) { + return mModules.getUsername(); + } + return null; + } + + public void find(String moduleID, NetworkCallback moduleNetworkCallback, NetworkErrorCallback errorCallback) { + find(moduleID, moduleNetworkCallback, errorCallback, RETRY_COUNT); + } + + private void find(String moduleID, NetworkCallback moduleNetworkCallback, NetworkErrorCallback errorCallback, int retries) { + if (mModules != null && mLogin.getLoginToken() != null && !mLogin.getLoginToken().isSameUser(mModules.getUsername())) + delete(); + if (retries < 0) { + errorCallback.onError(new NetworkError(101107, -1, "Too many retries!")); + return; + } + if (this.mModules != null) { + Modules.Module module = this.mModules.get(moduleID); + if (module != null) { + moduleNetworkCallback.onResponse(module); + return; + } + } + recv(success -> find(moduleID, moduleNetworkCallback, errorCallback, retries - 1), errorCallback, true, RETRY_COUNT); + } + + void store() { + if (this.mModules != null) { + try { + this.mModules.save(getContext()); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + private void restore() { + try { + this.mModules = Modules.load(getContext()); + } catch (FileNotFoundException ignored) { + } catch (IOException e) { + e.printStackTrace(); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + } + + public void delete() { + if (this.mModules != null) { + this.mModules.delete(getContext()); + this.mModules = null; + } + } + + public void recv(final NetworkCallback callback, final NetworkErrorCallback errorCallback) { + recv(callback, errorCallback, false); + } + + public void recv(final NetworkCallback callback, final NetworkErrorCallback errorCallback, boolean forceRefresh) { + recv(callback, errorCallback, forceRefresh, RETRY_COUNT); + } + + private void recv(final NetworkCallback callback, final NetworkErrorCallback errorCallback, boolean forceRefresh, final int retries) { + if (mModules != null && mLogin.getLoginToken() != null && !mLogin.getLoginToken().isSameUser(mModules.getUsername())) + delete(); + mQueue.add(() -> { + if (this.mModules != null && !forceRefresh) { + callback.onResponse(this.mModules); + mQueue.next(); + return; + } + this.upgrade(success -> { + if (this.mModules == null) + this.mModules = success; + else + this.mModules.updateList(success); + mListener.onModuleListChange(); + store(); + callback.onResponse(this.mModules); + mQueue.next(); + }, error -> { + if (retries > 0 && (error.getHttpStatus() == 401 || error.getHttpStatus() == 403)) { + mLogin.refreshLogin(success -> { + recv(callback, errorCallback, forceRefresh, retries-1); + mQueue.next(); + }, error1 -> { + errorCallback.onError(error1); + mQueue.next(); + }); + return; + } + errorCallback.onError(error); + mQueue.next(); + }); + }); + } + + private void upgrade(final NetworkCallback callback, final NetworkErrorCallback errorCallback) { + if (!mLogin.isInOnlineMode() || mLogin.getLoginToken() == null) { + errorCallback.onError(new NetworkError(101105, 500, "Currently running in offline mode!")); + return; + } + get("https://kvv.imp.fu-berlin.de/direct/site.json", mLogin.getLoginToken().getCookies(), response -> { + String body = response.getParsed(); + if (body == null) { + errorCallback.onError(new NetworkError(101101, 403, "No module list retrieved!")); + return; + } + Modules modules = new Modules(mLogin.getLoginToken().getUsername()); + JSONArray sites; + try { + JSONObject json = new JSONObject(body); + sites = json.getJSONArray("site_collection"); + } catch (JSONException e) { + e.printStackTrace(); + errorCallback.onError(new NetworkError(101102, 403, "Cannot parse module list!")); + return; + } + for (int i = 0; i < sites.length(); i++) { + try { + JSONObject site = sites.getJSONObject(i); + String semester_string = site.getJSONObject("props").optString("term_eid", null); + Semester semester = new Semester(semester_string); + HashSet lvNumbers = new HashSet<>(); + String kvv_lvnumbers = site.getJSONObject("props").optString("kvv_lvnumbers", null); + if (kvv_lvnumbers != null) for (MatchResult matchResult : Regex.allMatches("[0-9]+", kvv_lvnumbers)) { + lvNumbers.add(matchResult.group()); + } + String title = site.getString("entityTitle"); + LinkedHashSet lecturers = new LinkedHashSet<>(); + String kvv_lecturers = site.getJSONObject("props").optString("kvv_lecturers", null); + if (kvv_lecturers != null) for (String lecturer : kvv_lecturers.split("#")) { + if (lecturer.length() > 2) + lecturers.add(new Lecturer(lecturer)); + } + String type = site.getJSONObject("props").optString("kvv_coursetype", null); + String description = site.optString("description", ""); + description = String.valueOf(PartModules.fromHtml(description)); + String id = site.getString("id"); + modules.addModule(semester, lvNumbers, title, lecturers, type, description, id); + } catch (JSONException e) { + log.e(new NetworkError(101103, 403, "Cannot parse module list!")); + log.e("ID:", i, "JSON:", sites); + e.printStackTrace(); + } catch (NoSuchFieldException e) { + log.e(new NetworkError(101106, 403, "Cannot parse module list!")); + log.e("ID:", i, "JSON:", sites); + e.printStackTrace(); + } + } + // Empty module *may be* because token is invalid -> check + if (modules.size() == 0) + mLogin.testLoginToken(token -> callback.onResponse(modules), errorCallback); + else + callback.onResponse(modules); + }, error -> errorCallback.onError(new NetworkError(101104, error.networkResponse.statusCode, "Cannot get module list!"))); + } +} diff --git a/app/src/main/java/de/sebse/fuplanner/services/KVV/ModulesResources.java b/app/src/main/java/de/sebse/fuplanner/services/KVV/ModulesResources.java new file mode 100644 index 0000000..f26869c --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner/services/KVV/ModulesResources.java @@ -0,0 +1,220 @@ +package de.sebse.fuplanner.services.KVV; + +import android.content.Context; +import android.os.Environment; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.FileOutputStream; +import java.util.ArrayList; + +import de.sebse.fuplanner.services.KVV.types.Modules; +import de.sebse.fuplanner.services.KVV.types.Resource; +import de.sebse.fuplanner.tools.Regex; +import de.sebse.fuplanner.tools.network.NetworkCallback; +import de.sebse.fuplanner.tools.network.NetworkError; +import de.sebse.fuplanner.tools.network.NetworkErrorCallback; + +public class ModulesResources extends PartModules> { + + ModulesResources(Login login, ModulesList list, Context context) { + super(login, list, context); + } + + @Override + protected ArrayList getPart(Modules.Module module) { + return module.resources; + } + + @Override + protected boolean setPart(Modules.Module module, ArrayList part) { + boolean changed = module.resources == null || module.resources.hashCode() != part.hashCode(); + module.resources = part; + return changed; + } + + @Override + protected void upgrade(final String ID, final NetworkCallback> callback, final NetworkErrorCallback errorCallback) { + if (!mLogin.isInOnlineMode() || mLogin.getLoginToken() == null) { + errorCallback.onError(new NetworkError(101604, 500, "Currently running in offline mode!")); + return; + } + get(String.format("https://kvv.imp.fu-berlin.de/direct/content/site/%s.json", ID), mLogin.getLoginToken().getCookies(), response -> { + String body = response.getParsed(); + if (body == null) { + errorCallback.onError(new NetworkError(101601, 403, "No resources retrieved!")); + return; + } + ArrayList resources = new ArrayList<>(); + JSONArray sites; + try { + JSONObject json = new JSONObject(body); + sites = json.getJSONArray("content_collection"); + } catch (JSONException e) { + e.printStackTrace(); + errorCallback.onError(new NetworkError(101602, 403, "Cannot parse resources!")); + return; + } + + for (int i = 0; i < sites.length(); i++) { + try { + JSONObject site = sites.getJSONObject(i); + String author = site.getString("author"); + String title = site.getString("title"); + long modifiedDate = site.getLong("modifiedDate"); + String url = site.getString("url"); + boolean visible = site.getBoolean("visible"); + String type = site.getString("type"); + String container = site.getString("container"); + if (type.equals("collection")){ + resources.add(new Resource.Folder(author, title, modifiedDate, url, visible, container)); + } + else { + resources.add(new Resource.File(author, title, modifiedDate, url, visible, container, type)); + } + } catch (JSONException e) { + log.e(new NetworkError(101605, 403, "Cannot parse resources!")); + e.printStackTrace(); + log.e("ID:", i, "JSON:", sites); + return; + } + } + + ArrayList root = new ArrayList<>(); + // Generate folder structure + for (Resource res: resources) { + if (!res.getContainer().equals("/content/group/")) { + if (res.getContainer().equals("/content/group/"+ID+"/")){ + // if file in root folder + root.add(res); + } else { + // in sub folder + for (Resource res2: resources) { + if (res2.getUrl().endsWith(res.getContainer()) && res2 instanceof Resource.Folder) { + // Append File/Folder to list + ((Resource.Folder) res2).add(res); + } + } + } + } + } + + // Empty resources *may be* because token is invalid -> check + if (resources.size() == 0) + mLogin.testLoginToken(token -> callback.onResponse(root), errorCallback); + else + callback.onResponse(root); + }, error -> errorCallback.onError(new NetworkError(101603, error.networkResponse.statusCode, "Cannot get resources!"))); + } + + + + + + + + + + + + public void file(final String filename, final String url, final String modulename, final NetworkCallback callback, final NetworkErrorCallback errorCallback, boolean forceRefresh) { + file(filename, url, modulename, callback, errorCallback, forceRefresh, RETRY_COUNT); + } + + public void file(final String filename, final String url, final String modulename, final NetworkCallback callback, final NetworkErrorCallback errorCallback, boolean forceRefresh, int retries) { + if (isExternalStorageReadable()){ + File f = new File(Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_DOWNLOADS)+"/"+modulename+"/"+filename); + // check if file already downloaded -> do not download again + if (f.exists() && !forceRefresh) { + callback.onResponse(f.getPath()); + return; + } + } + fileUpgrade(filename, url , modulename, callback, error -> { + if (retries >= 0 && (error.getHttpStatus() == 401 || error.getHttpStatus() == 403)) { + mLogin.refreshLogin(success -> { + file(filename, url, modulename, callback, errorCallback, forceRefresh, retries-1); + }, errorCallback); + return; + } + errorCallback.onError(error); + }); + } + + private void fileUpgrade(String filename, String url, String modulename, final NetworkCallback callback, final NetworkErrorCallback errorCallback) { + if (!mLogin.isInOnlineMode() || mLogin.getLoginToken() == null) { + errorCallback.onError(new NetworkError(101604, 500, "Currently running in offline mode!")); + return; + } + get(url, mLogin.getLoginToken().getCookies(), response -> { + if (Regex.has("\\.[Uu][Rr][Ll]$", url)){ + // Return redirected URL + String path = response.getHeaders().get("Location"); + if (path == null){ + path = ""; + } + callback.onResponse(path); + } else if (response.getBytes() == null) { + errorCallback.onError(new NetworkError(101705, 403, "Cannot get file!")); + } else if (isExternalStorageWritable()) { + String path = saveFileInDownloads(filename, response.getBytes(), modulename); + callback.onResponse(path); + } else { + errorCallback.onError(new NetworkError(101704, 403, "External storage not writable!")); + } + }, error -> errorCallback.onError(new NetworkError(101702, error.networkResponse.statusCode, "Cannot get file!"))); + + + } + + + + + + + + /* Checks if external storage is available for read and write */ + private boolean isExternalStorageWritable() { + String state = Environment.getExternalStorageState(); + if (Environment.MEDIA_MOUNTED.equals(state)) { + return true; + } + log.w("File system: Writing not possible!"); + return false; + } + + /* Checks if external storage is available to at least read */ + private boolean isExternalStorageReadable() { + String state = Environment.getExternalStorageState(); + if (Environment.MEDIA_MOUNTED.equals(state) || + Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) { + return true; + } + log.w("File system: Reading not possible!"); + return false; + } + private String saveFileInDownloads(String filename, byte[] data, String moduleName) { + // Saves file in folder: DOWNLOADS/moduleName + File folder = new File(Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_DOWNLOADS), moduleName); + if (!folder.mkdir()) { + log.w( "Directory not created"); + } + String path = ""; + try { + // TODO check if enough storage space is available + FileOutputStream out = new FileOutputStream(folder.getPath()+"/"+filename); + out.write(data); + out.close(); + path = folder.getPath()+"/"+filename; + } catch (Exception e) { + log.w("File not saved!"); + e.printStackTrace(); + } + return path; + } +} diff --git a/app/src/main/java/de/sebse/fuplanner/services/KVV/Part.java b/app/src/main/java/de/sebse/fuplanner/services/KVV/Part.java new file mode 100644 index 0000000..686a484 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner/services/KVV/Part.java @@ -0,0 +1,34 @@ +package de.sebse.fuplanner.services.KVV; + +import android.content.Context; + +import de.sebse.fuplanner.services.KVV.types.Modules; +import de.sebse.fuplanner.tools.network.HTTPService; +import de.sebse.fuplanner.tools.network.NetworkCallback; +import de.sebse.fuplanner.tools.network.NetworkErrorCallback; + +public abstract class Part extends HTTPService { + static final int RETRY_COUNT = 1; + protected final Login mLogin; + protected final ModulesList mList; + + Part(Login login, ModulesList list, Context context) { + super(context); + this.mLogin = login; + this.mList = list; + } + + public void recv(final String moduleID, final NetworkCallback callback, final NetworkErrorCallback errorCallback) { + recv(moduleID, callback, errorCallback, false); + } + + public void recv(final String moduleID, final NetworkCallback callback, final NetworkErrorCallback errorCallback, final boolean forceRefresh) { + mList.find(moduleID, success -> recv(success, callback, errorCallback, forceRefresh), errorCallback); + } + + public void recv(final Modules.Module module, final NetworkCallback callback, final NetworkErrorCallback errorCallback, final boolean forceRefresh) { + recv(module, callback, errorCallback, forceRefresh, RETRY_COUNT); + } + + abstract protected void recv(final Modules.Module module, final NetworkCallback callback, final NetworkErrorCallback errorCallback, final boolean forceRefresh, final int retries); +} diff --git a/app/src/main/java/de/sebse/fuplanner/services/KVV/PartModules.java b/app/src/main/java/de/sebse/fuplanner/services/KVV/PartModules.java new file mode 100644 index 0000000..0301135 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner/services/KVV/PartModules.java @@ -0,0 +1,65 @@ +package de.sebse.fuplanner.services.KVV; + +import android.content.Context; +import android.os.Build; +import android.text.Html; +import android.text.Spanned; + +import de.sebse.fuplanner.services.KVV.types.Modules; +import de.sebse.fuplanner.tools.NewAsyncQueue; +import de.sebse.fuplanner.tools.network.NetworkCallback; +import de.sebse.fuplanner.tools.network.NetworkErrorCallback; + +abstract class PartModules extends Part { + private NewAsyncQueue mQueue = new NewAsyncQueue(); + + PartModules(Login login, ModulesList list, Context context) { + super(login, list, context); + } + + @Override + protected void recv(final Modules.Module module, final NetworkCallback callback, final NetworkErrorCallback errorCallback, final boolean forceRefresh, final int retries) { + mQueue.add(() -> { + if (getPart(module) != null && !forceRefresh) { + callback.onResponse(module); + mQueue.next(); + return; + } + upgrade(module.getID(), success -> { + if (setPart(module, success)) { + this.mList.store(); + } + callback.onResponse(module); + mQueue.next(); + }, error -> { + if (retries >= 0 && (error.getHttpStatus() == 401 || error.getHttpStatus() == 403)) { + mLogin.refreshLogin(success -> { + recv(module, callback, errorCallback, forceRefresh, retries-1); + mQueue.next(); + }, error1 -> { + errorCallback.onError(error1); + mQueue.next(); + }); + return; + } + errorCallback.onError(error); + mQueue.next(); + }); + }); + } + + @SuppressWarnings("deprecation") + static Spanned fromHtml(String html){ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY); + } else { + return Html.fromHtml(html); + } + } + + protected abstract T getPart(Modules.Module module); + + protected abstract boolean setPart(Modules.Module module, T part); + + protected abstract void upgrade(final String ID, final NetworkCallback callback, final NetworkErrorCallback errorCallback); +} diff --git a/app/src/main/java/de/sebse/fuplanner/services/KVV/TestLogin.java b/app/src/main/java/de/sebse/fuplanner/services/KVV/TestLogin.java deleted file mode 100644 index 48c9449..0000000 --- a/app/src/main/java/de/sebse/fuplanner/services/KVV/TestLogin.java +++ /dev/null @@ -1,38 +0,0 @@ -package de.sebse.fuplanner.services.KVV; - -import android.content.Context; - -import org.json.JSONException; -import org.json.JSONObject; - -import de.sebse.fuplanner.services.KVV.types.LoginToken; -import de.sebse.fuplanner.tools.network.HTTPService; -import de.sebse.fuplanner.tools.network.NetworkCallback; -import de.sebse.fuplanner.tools.network.NetworkError; -import de.sebse.fuplanner.tools.network.NetworkErrorCallback; - -final class TestLogin extends HTTPService { - - private TestLogin(Context context) { - super(context); - } - - static void testLogin(Context context, LoginToken loginToken, NetworkCallback callback, NetworkErrorCallback errorCallback) { - new TestLogin(context).get(String.format("https://kvv.imp.fu-berlin.de/direct/profile/%s.json", loginToken.getUsername()), loginToken.getCookies(), response -> { - String body = response.getParsed(); - if (body == null) { - errorCallback.onError(new NetworkError(100202, 403, "Testing login failed!")); - return; - } - try { - JSONObject json = new JSONObject(body); - String displayName = json.getString("displayName"); - String email = json.getString("email"); - loginToken.setAdditionals(displayName, email); - callback.onResponse(loginToken); - } catch (JSONException e) { - errorCallback.onError(new NetworkError(100201, 403, "Cannot parse profile!")); - } - }, error -> errorCallback.onError(new NetworkError(100200, error.networkResponse.statusCode, "Testing login failed!"))); - } -} diff --git a/app/src/main/java/de/sebse/fuplanner/services/KVV/types/Announcement.java b/app/src/main/java/de/sebse/fuplanner/services/KVV/types/Announcement.java index 4fb810e..72941a7 100644 --- a/app/src/main/java/de/sebse/fuplanner/services/KVV/types/Announcement.java +++ b/app/src/main/java/de/sebse/fuplanner/services/KVV/types/Announcement.java @@ -1,5 +1,7 @@ package de.sebse.fuplanner.services.KVV.types; +import com.google.android.gms.common.internal.Objects; + import java.io.Serializable; import java.util.ArrayList; @@ -54,4 +56,9 @@ public class Announcement implements Serializable { "\nCreated on: "+getCreatedOn()+ "\nURLs: "+getUrls().toString(); } + + @Override + public int hashCode() { + return Objects.hashCode(getId(), getBody(), getCreatedBy(), getCreatedOn(), getTitle(), getUrls()); + } } diff --git a/app/src/main/java/de/sebse/fuplanner/services/KVV/types/Assignment.java b/app/src/main/java/de/sebse/fuplanner/services/KVV/types/Assignment.java index 6605b73..e24f57e 100644 --- a/app/src/main/java/de/sebse/fuplanner/services/KVV/types/Assignment.java +++ b/app/src/main/java/de/sebse/fuplanner/services/KVV/types/Assignment.java @@ -1,5 +1,7 @@ package de.sebse.fuplanner.services.KVV.types; +import com.google.android.gms.common.internal.Objects; + import java.io.Serializable; import java.util.ArrayList; @@ -15,7 +17,6 @@ public class Assignment implements Serializable { this.title = title; this.dueTime = dueTime; this.urls = urls; - //this.grade = grade; this.instructions = instructions; } @@ -50,4 +51,9 @@ public class Assignment implements Serializable { "\nDue date: "+getDueDate()+ "\nInstructions: "+getInstructions().substring(0, Math.min(getInstructions().length(), 100)); } + + @Override + public int hashCode() { + return Objects.hashCode(getId(), getDueDate(), getInstructions(), getTitle(), getUrls()); + } } diff --git a/app/src/main/java/de/sebse/fuplanner/services/KVV/types/Event.java b/app/src/main/java/de/sebse/fuplanner/services/KVV/types/Event.java index 5b070b0..a88835d 100644 --- a/app/src/main/java/de/sebse/fuplanner/services/KVV/types/Event.java +++ b/app/src/main/java/de/sebse/fuplanner/services/KVV/types/Event.java @@ -1,5 +1,7 @@ package de.sebse.fuplanner.services.KVV.types; +import com.google.android.gms.common.internal.Objects; + import java.io.Serializable; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; @@ -111,4 +113,9 @@ public class Event implements Serializable { "\nStart Date: "+getStartDate()+ "\nEnd date: "+getEndDate(); } + + @Override + public int hashCode() { + return Objects.hashCode(getId(), getType(), getStartDate(), getEndDate(), getTitle(), getLocation()); + } } diff --git a/app/src/main/java/de/sebse/fuplanner/services/KVV/types/Gradebook.java b/app/src/main/java/de/sebse/fuplanner/services/KVV/types/Grade.java similarity index 69% rename from app/src/main/java/de/sebse/fuplanner/services/KVV/types/Gradebook.java rename to app/src/main/java/de/sebse/fuplanner/services/KVV/types/Grade.java index bf636cd..11ccd81 100644 --- a/app/src/main/java/de/sebse/fuplanner/services/KVV/types/Gradebook.java +++ b/app/src/main/java/de/sebse/fuplanner/services/KVV/types/Grade.java @@ -1,13 +1,15 @@ package de.sebse.fuplanner.services.KVV.types; +import com.google.android.gms.common.internal.Objects; + import java.io.Serializable; -public class Gradebook implements Serializable { +public class Grade implements Serializable { private final String itemName; private final double grade; private final double maxPoints; - public Gradebook(String itemName, double points, double maxPoints) { + public Grade(String itemName, double points, double maxPoints) { this.itemName = itemName; this.grade = points; this.maxPoints = maxPoints; @@ -31,4 +33,9 @@ public class Gradebook implements Serializable { "\nPoints: "+ getPoints()+ "\nMax points: "+getMaxPoints(); } + + @Override + public int hashCode() { + return Objects.hashCode(getItemName(), getPoints(), getMaxPoints()); + } } diff --git a/app/src/main/java/de/sebse/fuplanner/services/KVV/types/Lecturer.java b/app/src/main/java/de/sebse/fuplanner/services/KVV/types/Lecturer.java index bad72f5..6ceb2b6 100644 --- a/app/src/main/java/de/sebse/fuplanner/services/KVV/types/Lecturer.java +++ b/app/src/main/java/de/sebse/fuplanner/services/KVV/types/Lecturer.java @@ -8,9 +8,10 @@ public class Lecturer implements Serializable { private final String firstName; private final String surname; private final String mail; + private final boolean isResponsible; public Lecturer(String parsableString) throws NoSuchFieldException { - Pattern pattern = Pattern.compile("([^|]*)\\|([^|]*)\\|([^|]*)\\|\\|", Pattern.DOTALL); + Pattern pattern = Pattern.compile("([^|]*)\\|([^|]*)\\|([^|]*)\\|\\|([^|]*)", Pattern.DOTALL); Matcher matcher = pattern.matcher(parsableString); if (!matcher.find()) { throw new NoSuchFieldException(); @@ -18,20 +19,33 @@ public class Lecturer implements Serializable { this.firstName = matcher.group(1); this.surname = matcher.group(2); this.mail = matcher.group(3); + this.isResponsible = matcher.group(4).equals("true"); } - private String getFirstName() { + public String getFirstName() { return firstName; } - private String getSurname() { + public String getSurname() { return surname; } - private String getMail() { + public String getMail() { return mail; } + public boolean isResponsible() { + return isResponsible; + } + + public String getName() { + return getFirstName() + " " + getSurname(); + } + + public String getNameShort() { + return getFirstName().substring(0, 1) + ". " + getSurname(); + } + @Override public String toString() { return "First name: "+ getFirstName()+ diff --git a/app/src/main/java/de/sebse/fuplanner/services/KVV/types/LoginToken.java b/app/src/main/java/de/sebse/fuplanner/services/KVV/types/LoginToken.java index 0bd58eb..0ebfcfd 100644 --- a/app/src/main/java/de/sebse/fuplanner/services/KVV/types/LoginToken.java +++ b/app/src/main/java/de/sebse/fuplanner/services/KVV/types/LoginToken.java @@ -3,6 +3,7 @@ package de.sebse.fuplanner.services.KVV.types; import android.content.Context; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; @@ -17,16 +18,14 @@ import androidx.annotation.Nullable; */ public class LoginToken implements Serializable { - private static final long EASY_LOGIN_TIME_MILLIS = 1000 * 60 * 60 * 300; private static final String FILE_NAME = "LoginTokenSaving"; private final String username; private final String shibsessionKey; private final String shibsessionName; private final String JSESSIONID; - private String fullName; - private String email; - private long saveDate = 0; + @Nullable private String fullName; + @Nullable private String email; public LoginToken(String username, String shibsessionKey, String shibsessionName, String JSESSIONID) { this.username = username; @@ -37,17 +36,23 @@ public class LoginToken implements Serializable { @Nullable public static LoginToken load(Context context) throws IOException, ClassNotFoundException { - FileInputStream fis = context.openFileInput(FILE_NAME); + FileInputStream fis; + try { + fis = context.openFileInput(FILE_NAME); + } catch (FileNotFoundException e) { + return null; + } ObjectInputStream is = new ObjectInputStream(fis); - LoginToken loginToken = (LoginToken) is.readObject(); + Object readObject = is.readObject(); + if (!(readObject instanceof LoginToken)) + return null; + LoginToken loginToken = (LoginToken) readObject; is.close(); fis.close(); - loginToken = (loginToken.saveDate > (System.currentTimeMillis() - EASY_LOGIN_TIME_MILLIS)) ? loginToken : null; return loginToken; } public void save(Context context) throws IOException { - saveDate = System.currentTimeMillis(); FileOutputStream fos = context.openFileOutput(FILE_NAME, Context.MODE_PRIVATE); ObjectOutputStream os = new ObjectOutputStream(fos); os.writeObject(this); @@ -96,8 +101,8 @@ public class LoginToken implements Serializable { return cookies; } - public boolean isSameUser(LoginToken token) { - return token != null && this.getUsername().equals(token.getUsername()); + public boolean isSameUser(String username) { + return this.getUsername().equals(username); } @Override diff --git a/app/src/main/java/de/sebse/fuplanner/services/KVV/types/Modules.java b/app/src/main/java/de/sebse/fuplanner/services/KVV/types/Modules.java index 6c0f3d5..9e5b6d7 100644 --- a/app/src/main/java/de/sebse/fuplanner/services/KVV/types/Modules.java +++ b/app/src/main/java/de/sebse/fuplanner/services/KVV/types/Modules.java @@ -2,6 +2,8 @@ package de.sebse.fuplanner.services.KVV.types; import android.content.Context; +import org.jetbrains.annotations.NotNull; + import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; @@ -11,6 +13,7 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedHashSet; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -21,16 +24,16 @@ import androidx.annotation.Nullable; public class Modules implements Iterable, Serializable { private SortedListModule list; - private final LoginToken token; + private final String mUsername; //private transient Logger log = new Logger(this); private static final String FILE_NAME = "ModuleListSaving"; - public Modules(LoginToken loginToken) { - this.token = loginToken; + public Modules(String username) { + this.mUsername = username; this.list = new SortedListModule(); } - public void addModule(String semester, HashSet lvNumber, String title, HashSet lecturer, String type, String description, String ID) { + public void addModule(@Nullable Semester semester, HashSet lvNumber, String title, LinkedHashSet lecturer, String type, String description, String ID) { Module m = new Module(semester, lvNumber, title, lecturer, type, description, ID); this.list.add(m); } @@ -66,7 +69,10 @@ public class Modules implements Iterable, Serializable { public static Modules load(Context context) throws IOException, ClassNotFoundException { FileInputStream fis = context.openFileInput(FILE_NAME); ObjectInputStream is = new ObjectInputStream(fis); - Modules modules = (Modules) is.readObject(); + Object readObject = is.readObject(); + if (!(readObject instanceof Modules)) + return null; + Modules modules = (Modules) readObject; is.close(); fis.close(); return modules; @@ -84,8 +90,8 @@ public class Modules implements Iterable, Serializable { context.deleteFile(FILE_NAME); } - public LoginToken getToken() { - return token; + public String getUsername() { + return mUsername; } public void updateList(Modules modules) { @@ -104,29 +110,25 @@ public class Modules implements Iterable, Serializable { } public class Module implements Serializable { - public final String semester; - final HashSet lvNumber; - public final String title; - final HashSet lecturer; - public final String type; - public final String description; - private final String ID; + @Nullable public final Semester semester; + @NotNull final HashSet lvNumber; + @NotNull public final String title; + @NotNull + public final ArrayList lecturer; + @Nullable public final String type; + @Nullable public final String description; + @NotNull private final String ID; @Nullable public ArrayList announcements; @Nullable public AssignmentList assignments; @Nullable public EventList events; - @Nullable public ArrayList gradebook; + @Nullable public ArrayList gradebook; @Nullable public ArrayList resources; - /*private Module() { - this(null, null, null, null, null); - throw new AssertionError("Do not use this constructor!"); - }*/ - public float getGradebookPercent(){ float maxPoint = 0; float userPoint = 0; if (gradebook != null) { - for (Gradebook g : gradebook){ + for (Grade g : gradebook){ maxPoint += g.getMaxPoints(); userPoint += g.getPoints(); } @@ -136,20 +138,20 @@ public class Modules implements Iterable, Serializable { return userPoint/maxPoint; } - private Module(String semester, HashSet lvNumber, String title, HashSet lecturer, String type, String description, String ID) { - semester = semester.replace("SS", "S"); - semester = semester.replaceAll("[0-9]{2}([0-9]{2})", "$1"); + private Module(@Nullable Semester semester, @NotNull HashSet lvNumber, @NotNull String title, @NotNull LinkedHashSet lecturer, @Nullable String type, @Nullable String description, @NotNull String ID) { + title = title.replaceAll("(.*?) (S[0-9]{2}|W[0-9/]{5})", "$1"); this.semester = semester; this.lvNumber = lvNumber; this.title = title; - this.lecturer = lecturer; + this.lecturer = new ArrayList<>(lecturer); this.type = type; this.description = description; this.ID = ID; } + @NonNull public String getID() { return ID; } diff --git a/app/src/main/java/de/sebse/fuplanner/services/KVV/types/Resource.java b/app/src/main/java/de/sebse/fuplanner/services/KVV/types/Resource.java index 31ccb99..e4b1328 100644 --- a/app/src/main/java/de/sebse/fuplanner/services/KVV/types/Resource.java +++ b/app/src/main/java/de/sebse/fuplanner/services/KVV/types/Resource.java @@ -1,5 +1,7 @@ package de.sebse.fuplanner.services.KVV.types; +import com.google.android.gms.common.internal.Objects; + import java.io.Serializable; import java.util.ArrayList; @@ -54,6 +56,11 @@ public abstract class Resource implements Serializable { public abstract TreeNode getTreeNode(); + @Override + public int hashCode() { + return Objects.hashCode(getAuthor(), getContainer(), getModifiedDate(), getTitle(), getUrl()); + } + public static class File extends Resource implements LayoutItemType { private final String type; diff --git a/app/src/main/java/de/sebse/fuplanner/services/KVV/types/Semester.java b/app/src/main/java/de/sebse/fuplanner/services/KVV/types/Semester.java new file mode 100644 index 0000000..76605aa --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner/services/KVV/types/Semester.java @@ -0,0 +1,55 @@ +package de.sebse.fuplanner.services.KVV.types; + +import java.io.Serializable; + +import androidx.annotation.Nullable; +import de.sebse.fuplanner.tools.Regex; + +public class Semester implements Serializable { + public static final int SEM_WS = 1; + public static final int SEM_SS = 2; + private int type; + private int year; + + public Semester(int type, int year) { + this.type = type; + this.year = year; + } + + public Semester(String semester_string) throws NoSuchFieldException { + /*Semester sem = null; + if (semester != null) { + sem = new Semester(semester) + semester = semester.replace("SS", "S"); + semester = semester.replaceAll("[0-9]{2}([0-9]{2})", "$1"); + }*/ + + + /*String s1type = Regex.regex("^(S|WS) ", a); + int s1year = Integer.parseInt(Regex.regex("^(S|WS) ([0-9]{2})", a, 2)); + String s2type = Regex.regex("^(S|WS) ", b); + int s2year = Integer.parseInt(Regex.regex("^(S|WS) ([0-9]{2})", b, 2));*/ + + String type = Regex.regex("^(SS|WS) ", semester_string); + String year = Regex.regex("^(SS|WS) ([0-9]{2})", semester_string, 2); + this.type = type.equals("SS") ? SEM_SS : SEM_WS; + this.year = Integer.parseInt(year); + } + + public int getType() { + return type; + } + + public int getYear() { + return year; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (obj instanceof Semester) { + Semester other = (Semester) obj; + return other.type == type && other.year == year; + } + return false; + } +} diff --git a/app/src/main/java/de/sebse/fuplanner/services/KVV/types/SortedListModule.java b/app/src/main/java/de/sebse/fuplanner/services/KVV/types/SortedListModule.java index 412a208..5c226f4 100644 --- a/app/src/main/java/de/sebse/fuplanner/services/KVV/types/SortedListModule.java +++ b/app/src/main/java/de/sebse/fuplanner/services/KVV/types/SortedListModule.java @@ -1,9 +1,9 @@ package de.sebse.fuplanner.services.KVV.types; -import de.sebse.fuplanner.tools.Regex; +import androidx.annotation.Nullable; import de.sebse.fuplanner.tools.SortedList; -public class SortedListModule extends SortedList { +public class SortedListModule extends SortedList { private static final int LARGER = 1; private static final int EQUAL = 0; private static final int SMALLER = -1; @@ -27,7 +27,7 @@ public class SortedListModule extends SortedList super.add(e); } - public String getLatestSemester() { + public Semester getLatestSemester() { if (size() > 0) //noinspection ConstantConditions return this.get(0).semester; @@ -35,7 +35,7 @@ public class SortedListModule extends SortedList return null; } - private static int compareSemester(String a, String b) throws NoSuchFieldException { + private static int compareSemester(@Nullable Semester a, @Nullable Semester b) throws NoSuchFieldException { if (a == null && b == null) return EQUAL; if (a == null) @@ -43,17 +43,13 @@ public class SortedListModule extends SortedList if (b == null) return LARGER; - String s1type = Regex.regex("^(S|WS) ", a); - int s1year = Integer.parseInt(Regex.regex("^(S|WS) ([0-9]{2})", a, 2)); - String s2type = Regex.regex("^(S|WS) ", b); - int s2year = Integer.parseInt(Regex.regex("^(S|WS) ([0-9]{2})", b, 2)); - if (s1year == s2year) { - if (s1type.equals(s2type)) + if (a.getYear() == b.getYear()) { + if (a.getType() == b.getType()) return EQUAL; - return s1type.equals("S") ? SMALLER : LARGER; + return a.getType() == Semester.SEM_SS ? SMALLER : LARGER; } - return s1year < s2year ? SMALLER : LARGER; + return a.getYear() < b.getYear() ? SMALLER : LARGER; } @Override @@ -62,7 +58,7 @@ public class SortedListModule extends SortedList } @Override - public boolean hasFilter(Modules.Module o1, String filter) { - return o1.semester.equals(filter); + public boolean hasFilter(Modules.Module o1, Semester filter) { + return o1.semester != null && o1.semester.equals(filter); } } diff --git a/app/src/main/java/de/sebse/fuplanner/services/KVV/Download.java b/app/src/main/java/de/sebse/fuplanner/services/KVV/ui/Download.java similarity index 85% rename from app/src/main/java/de/sebse/fuplanner/services/KVV/Download.java rename to app/src/main/java/de/sebse/fuplanner/services/KVV/ui/Download.java index 4c03d45..8cd8ff8 100644 --- a/app/src/main/java/de/sebse/fuplanner/services/KVV/Download.java +++ b/app/src/main/java/de/sebse/fuplanner/services/KVV/ui/Download.java @@ -1,4 +1,4 @@ -package de.sebse.fuplanner.services.KVV; +package de.sebse.fuplanner.services.KVV.ui; import android.Manifest; import android.app.AlertDialog; @@ -24,10 +24,7 @@ import de.sebse.fuplanner.tools.RequestPermissionsResultListener; import de.sebse.fuplanner.tools.UtilsDate; import de.sebse.fuplanner.tools.logging.Logger; -import static android.content.Intent.normalizeMimeType; -import static androidx.core.app.ActivityCompat.startActivityForResult; import static androidx.core.content.ContextCompat.checkSelfPermission; -import static androidx.core.content.ContextCompat.startActivity; public class Download { @@ -35,7 +32,6 @@ public class Download { private final ActivityInterface activityInterface; private RequestedDownload requestedDownload; private Logger log = new Logger(this); - static final int REQUEST_IMAGE_OPEN = 1; public Download(ContextInterface contextInterface, ActivityInterface activityInterface) { @@ -48,19 +44,20 @@ public class Download { } public void openDownloadDialog(Resource.File file, String folderName) { - if (contextInterface.get() == null) + Context context = contextInterface.get(); + if (context == null) return; - AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(contextInterface.get()); + AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(context); File f = new File(Environment.getExternalStoragePublicDirectory( Environment.DIRECTORY_DOWNLOADS)+"/"+folderName+"/"+file.getTitle()); - Resources resources = contextInterface.get().getResources(); + Resources resources = context.getResources(); String message = ""; if (file.getAuthor() != null && !file.getAuthor().isEmpty()) message += resources.getString(R.string.creator_name, file.getAuthor()); if (file.getModifiedDate() != 0) { if (!message.isEmpty()) message += "\n"; - message += resources.getString(R.string.last_modified_on, UtilsDate.getModifiedDateTime(contextInterface.get(), file.getModifiedDate())); + message += resources.getString(R.string.last_modified_on, UtilsDate.getModifiedDateTime(context, file.getModifiedDate())); } alertDialogBuilder @@ -82,40 +79,46 @@ public class Download { } private void download(Resource.File file, String folderName, boolean downloadNew){ - if (activityInterface.get() == null) { + MainActivity activity = activityInterface.get(); + if (activity == null) { showDownloadError(); return; } - if (checkSelfPermission(activityInterface.get(), android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { + if (checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // Access granted downloadOrOpen(file, folderName, downloadNew); } else { this.requestedDownload = new RequestedDownload(file, folderName, downloadNew); - ActivityCompat.requestPermissions(activityInterface.get(), + ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1); } } private void downloadOrOpen(Resource.File file, String folderName, boolean downloadNew) { - KVV kvv = activityInterface.get().getKVV(); - if(isExternalStorageWritable()){ - kvv.getResourceFile(success1 -> { - // Downloading file failed - if (success1.equals("")){ - showDownloadError(); - }else { - if (Regex.has("^http", success1)){ - Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(success1)); - contextInterface.get().startActivity(intent); - } - else { - fileOpen(new File(success1)); - } - - } - }, log::e, file.getTitle(), file.getUrl(), folderName, downloadNew); + if (!isExternalStorageWritable()) { + return; } + MainActivity activity = activityInterface.get(); + if (activity == null) { + showDownloadError(); + return; + } + activity.getKVV().modules().resources().file(file.getTitle(), file.getUrl(), folderName, success -> { + Context context = contextInterface.get(); + if (success.equals("")) { + showDownloadError(); + } else { + if (Regex.has("^http", success)){ + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(success)); + context.startActivity(intent); + } + else { + fileOpen(new File(success)); + } + + } + }, log::e, downloadNew); } private void showDownloadError() { @@ -186,7 +189,6 @@ public class Download { } private void fileOpen(File url){ - Uri uri = FileProvider.getUriForFile(contextInterface.get(), contextInterface.get().getApplicationContext().getPackageName() + ".my.provider", url); Intent intent; @@ -235,18 +237,12 @@ public class Download { intent.setDataAndType(uri, "video/*"); } else { //if you want you can also define the intent type for any other file + //additionally use else clause below, to manage other unknown extensions //in this case, Android will show all applications installed on the device //so you can choose which application to use intent.setDataAndType(uri, "*/*"); } - - //intent.addCategory(Intent.CATEGORY_OPENABLE); - // Only the system receives the ACTION_OPEN_DOCUMENT, so no need to test. - - - - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); contextInterface.get().startActivity(intent); diff --git a/app/src/main/java/de/sebse/fuplanner/tools/EventListener.java b/app/src/main/java/de/sebse/fuplanner/tools/EventListener.java new file mode 100644 index 0000000..42bbbcf --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner/tools/EventListener.java @@ -0,0 +1,26 @@ +package de.sebse.fuplanner.tools; + +import java.util.HashMap; + +public class EventListener { + private HashMap> list = new HashMap<>(); + + public void add(String id,EventFunction listener) { + list.put(id, listener); + } + + public void remove(String id) { + list.remove(id); + } + + public void emit(T value) { + for (EventFunction listener : list.values()) { + listener.apply(value); + } + } + + @FunctionalInterface + public interface EventFunction { + void apply(T value); + } +} diff --git a/app/src/main/java/de/sebse/fuplanner/tools/MainActivityListener.java b/app/src/main/java/de/sebse/fuplanner/tools/MainActivityListener.java index 63f5306..5f6800c 100644 --- a/app/src/main/java/de/sebse/fuplanner/tools/MainActivityListener.java +++ b/app/src/main/java/de/sebse/fuplanner/tools/MainActivityListener.java @@ -19,15 +19,12 @@ public interface MainActivityListener { GoogleAuth getGoogleAuth(); - void loginTokenInvalid(boolean doLoginCheck); - - void onRefreshCompleted(boolean isFailed); - CanteenBrowser getCanteenBrowser(); + @Deprecated + void onRefreshCompleted(boolean isFailed); + void addRequestPermissionsResultListener(RequestPermissionsResultListener listener, String id); void removeRequestPermissionsResultListener(String id); - - void refreshNavigation(); } diff --git a/app/src/main/java/de/sebse/fuplanner/tools/NewAsyncQueue.java b/app/src/main/java/de/sebse/fuplanner/tools/NewAsyncQueue.java new file mode 100644 index 0000000..8a39c91 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner/tools/NewAsyncQueue.java @@ -0,0 +1,58 @@ +package de.sebse.fuplanner.tools; + +import java.util.LinkedList; + +import de.sebse.fuplanner.tools.network.NetworkCallback; +import de.sebse.fuplanner.tools.network.NetworkErrorCallback; + +public class NewAsyncQueue { + private final LinkedList mQueue = new LinkedList<>(); + private boolean mIsRunning = false; + + public void add(AsyncQueueCallback callback) { + if (isRunning()) + getQueue().addLast(callback); + else { + setRunning(true); + callback.run(); + } + } + + public void next() { + AsyncQueueCallback callback = getQueue().pollFirst(); + if (callback == null) + setRunning(false); + else + callback.run(); + } + + public interface AsyncQueueCallback { + void run(); + } + + public NetworkErrorCallback check(NetworkErrorCallback value) { + return error -> { + value.onError(error); + next(); + }; + } + + public NetworkCallback check(NetworkCallback value) { + return success -> { + value.onResponse(success); + next(); + }; + } + + private boolean isRunning() { + return mIsRunning; + } + + private void setRunning(boolean value) { + mIsRunning = value; + } + + private LinkedList getQueue() { + return mQueue; + } +} diff --git a/app/src/main/java/de/sebse/fuplanner/tools/network/HTTPService.java b/app/src/main/java/de/sebse/fuplanner/tools/network/HTTPService.java index 088247a..ea82635 100644 --- a/app/src/main/java/de/sebse/fuplanner/tools/network/HTTPService.java +++ b/app/src/main/java/de/sebse/fuplanner/tools/network/HTTPService.java @@ -17,6 +17,7 @@ import java.util.HashMap; import java.util.Map; import androidx.annotation.Nullable; +import de.sebse.fuplanner.tools.EventListener; import de.sebse.fuplanner.tools.logging.Logger; /** @@ -27,35 +28,64 @@ public class HTTPService { private final RequestQueue requestQueue; private final Context mContext; protected Logger log = new Logger(this); + private EventListener errorResponseListener = new EventListener<>(); + private EventListener successResponseListener = new EventListener<>(); protected HTTPService(Context context) { this.mContext = context; requestQueue = Volley.newRequestQueue(context, new BetterHurlStack(false)); } + public void addErrorListener(String id, EventListener.EventFunction listener) { + errorResponseListener.add(id, listener); + } + + public void removeErrorListener(String id) { + errorResponseListener.remove(id); + } + + public void addSuccessListener(String id, EventListener.EventFunction listener) { + successResponseListener.add(id, listener); + } + + public void removeSuccessListener(String id) { + successResponseListener.remove(id); + } + protected void get(String url, @Nullable final HashMap cookies, Response.Listener response, Response.ErrorListener error) { HttpRequest request = new HttpRequest(Request.Method.GET, url, response, error) { @Override public void deliverError(VolleyError error) { if (error == null) { - super.deliverError(new VolleyError(new NetworkResponse(500, null, true, 0, null))); + deliver(new VolleyError(new NetworkResponse(500, null, true, 0, null))); } else if (error.networkResponse == null) { int statusCode; if (error instanceof TimeoutError) statusCode = 408; else statusCode = 500; - super.deliverError(new VolleyError(new NetworkResponse(statusCode, null, true, error.getNetworkTimeMs(), null))); + deliver(new VolleyError(new NetworkResponse(statusCode, null, true, error.getNetworkTimeMs(), null))); } else { final int status = error.networkResponse.statusCode; if (status == 302) { - super.deliverResponse(new Result(null, error.networkResponse.headers)); + deliverResponse(new Result(null, error.networkResponse.headers)); } else { - super.deliverError(error); + deliver(error); } } } + @Override + protected void deliverResponse(Result response) { + successResponseListener.emit(response); + super.deliverResponse(response); + } + + private void deliver(VolleyError error) { + errorResponseListener.emit(error); + super.deliverError(error); + } + public Map getHeaders() throws AuthFailureError { Map params = super.getHeaders(); if (cookies != null) { @@ -109,24 +139,35 @@ public class HTTPService { @Override public void deliverError(VolleyError error) { if (error == null) { - super.deliverError(new VolleyError(new NetworkResponse(500, null, true, 0, null))); + deliver(new VolleyError(new NetworkResponse(500, null, true, 0, null))); } else if (error.networkResponse == null) { int statusCode; if (error instanceof TimeoutError) statusCode = 408; else statusCode = 500; - super.deliverError(new VolleyError(new NetworkResponse(statusCode, null, true, error.getNetworkTimeMs(), null))); + deliver(new VolleyError(new NetworkResponse(statusCode, null, true, error.getNetworkTimeMs(), null))); } else { final int status = error.networkResponse.statusCode; if (status == 302) { - super.deliverResponse(new Result(null, error.networkResponse.headers)); + deliverResponse(new Result(null, error.networkResponse.headers)); } else { - super.deliverError(error); + deliver(error); } } } + private void deliver(VolleyError error) { + errorResponseListener.emit(error); + super.deliverError(error); + } + + @Override + protected void deliverResponse(Result response) { + successResponseListener.emit(response); + super.deliverResponse(response); + } + public Map getHeaders() throws AuthFailureError { Map params = super.getHeaders(); if (cookies != null) { diff --git a/app/src/main/java/de/sebse/fuplanner/tools/ui/AnnouncementViewHolder.java b/app/src/main/java/de/sebse/fuplanner/tools/ui/AnnouncementViewHolder.java index 91694b7..d8ae17b 100644 --- a/app/src/main/java/de/sebse/fuplanner/tools/ui/AnnouncementViewHolder.java +++ b/app/src/main/java/de/sebse/fuplanner/tools/ui/AnnouncementViewHolder.java @@ -1,7 +1,6 @@ package de.sebse.fuplanner.tools.ui; import android.view.View; -import android.widget.ImageView; import android.widget.TextView; import com.cunoraz.tagview.TagView; diff --git a/app/src/main/java/de/sebse/fuplanner/tools/ui/ListViewHolder.java b/app/src/main/java/de/sebse/fuplanner/tools/ui/ListViewHolder.java index 9c7534f..8e04b95 100644 --- a/app/src/main/java/de/sebse/fuplanner/tools/ui/ListViewHolder.java +++ b/app/src/main/java/de/sebse/fuplanner/tools/ui/ListViewHolder.java @@ -1,8 +1,6 @@ package de.sebse.fuplanner.tools.ui; import android.view.View; -import android.widget.ListView; -import android.widget.TextView; import androidx.recyclerview.widget.RecyclerView; import de.sebse.fuplanner.R; diff --git a/app/src/main/java/de/sebse/fuplanner/tools/ui/MailViewHolder.java b/app/src/main/java/de/sebse/fuplanner/tools/ui/MailViewHolder.java new file mode 100644 index 0000000..c24b47e --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner/tools/ui/MailViewHolder.java @@ -0,0 +1,26 @@ +package de.sebse.fuplanner.tools.ui; + +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.recyclerview.widget.RecyclerView; +import de.sebse.fuplanner.R; + +public class MailViewHolder extends StringViewHolder { + public final TextView mTitle; + public final TextView mSubLeft; + public final ImageView mIcon; + + public MailViewHolder(View view) { + super(view); + mTitle = view.findViewById(R.id.title); + mSubLeft = view.findViewById(R.id.sub_left); + mIcon = view.findViewById(R.id.icon); + } + + @Override + public String toString() { + return super.toString() + " '" + mTitle.getText() + "' '" + mSubLeft.getText() + "'"; + } +} diff --git a/app/src/main/java/de/sebse/fuplanner/tools/ui/treeview/TreeViewAdapter.java b/app/src/main/java/de/sebse/fuplanner/tools/ui/treeview/TreeViewAdapter.java index 69e5862..ffe2a813 100644 --- a/app/src/main/java/de/sebse/fuplanner/tools/ui/treeview/TreeViewAdapter.java +++ b/app/src/main/java/de/sebse/fuplanner/tools/ui/treeview/TreeViewAdapter.java @@ -17,6 +17,7 @@ import androidx.recyclerview.widget.RecyclerView; /** * Created by tlh on 2016/10/1 :) */ +@SuppressWarnings("unused") public class TreeViewAdapter extends RecyclerView.Adapter { private static final String KEY_IS_EXPAND = "IS_EXPAND"; private final List viewBinders; @@ -42,7 +43,7 @@ public class TreeViewAdapter extends RecyclerView.Adapter nodes) { - for (TreeNode node : nodes) { + for (TreeNode node : nodes) { displayNodes.add(node); if (!node.isLeaf() && node.isExpand()) findDisplayNodes(node.getChildList()); @@ -54,8 +55,9 @@ public class TreeViewAdapter extends RecyclerView.Adapter payloads) { - if (payloads != null && !payloads.isEmpty()) { + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List payloads) { + if (!payloads.isEmpty()) { Bundle b = (Bundle) payloads.get(0); for (String key : b.keySet()) { switch (key) { @@ -84,44 +86,42 @@ public class TreeViewAdapter extends RecyclerView.Adapter { + TreeNode selectedNode = displayNodes.get(holder.getLayoutPosition()); + // Prevent multi-click during the short interval. + try { + long lastClickTime = (long) holder.itemView.getTag(); + if (System.currentTimeMillis() - lastClickTime < 500) + return; + } catch (Exception e) { holder.itemView.setTag(System.currentTimeMillis()); + } + holder.itemView.setTag(System.currentTimeMillis()); - if (onTreeNodeListener != null && onTreeNodeListener.onClick(selectedNode, holder)) - return; - if (selectedNode.isLeaf()) - return; - // This TreeNode was locked to click. - if (selectedNode.isLocked()) return; - boolean isExpand = selectedNode.isExpand(); - int positionStart = displayNodes.indexOf(selectedNode) + 1; - if (!isExpand) { - notifyItemRangeInserted(positionStart, addChildNodes(selectedNode, positionStart)); - } else { - notifyItemRangeRemoved(positionStart, removeChildNodes(selectedNode, true)); - } + if (onTreeNodeListener != null && onTreeNodeListener.onClick(selectedNode, holder)) + return; + if (selectedNode.isLeaf()) + return; + // This TreeNode was locked to click. + if (selectedNode.isLocked()) return; + boolean isExpand = selectedNode.isExpand(); + int positionStart = displayNodes.indexOf(selectedNode) + 1; + if (!isExpand) { + notifyItemRangeInserted(positionStart, addChildNodes(selectedNode, positionStart)); + } else { + notifyItemRangeRemoved(positionStart, removeChildNodes(selectedNode, true)); } }); for (TreeViewBinder viewBinder : viewBinders) { if (viewBinder.getLayoutId() == displayNodes.get(position).getContent().getLayoutId()) + //noinspection unchecked viewBinder.bindView(holder, position, displayNodes.get(position)); } } - private int addChildNodes(TreeNode pNode, int startIndex) { + private int addChildNodes(TreeNode pNode, int startIndex) { List childList = pNode.getChildList(); int addChildCount = 0; for (TreeNode treeNode : childList) { @@ -139,7 +139,7 @@ public class TreeViewAdapter extends RecyclerView.Adapter pNode, boolean shouldToggle) { if (pNode.isLeaf()) return 0; List childList = pNode.getChildList(); @@ -287,7 +287,7 @@ public class TreeViewAdapter extends RecyclerView.Adapter pNode) { List temp = backupDisplayNodes(); if (pNode.isRoot()) { List roots = new ArrayList<>(); @@ -301,7 +301,7 @@ public class TreeViewAdapter extends RecyclerView.Adapter parent = pNode.getParent(); if (parent == null) return; List childList = parent.getChildList(); diff --git a/app/src/main/res/drawable/ic_mail.xml b/app/src/main/res/drawable/ic_mail.xml new file mode 100644 index 0000000..6701eb8 --- /dev/null +++ b/app/src/main/res/drawable/ic_mail.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 6b82597..e962ffb 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -10,6 +10,7 @@ diff --git a/app/src/main/res/layout/list_all_items.xml b/app/src/main/res/layout/list_all_items.xml index 05fa9b9..09d8e47 100644 --- a/app/src/main/res/layout/list_all_items.xml +++ b/app/src/main/res/layout/list_all_items.xml @@ -1,51 +1,56 @@ - - - + + android:orientation="horizontal" + android:padding="5dip" > + - + - + - - \ No newline at end of file + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_all_mails.xml b/app/src/main/res/layout/list_all_mails.xml new file mode 100644 index 0000000..ce391c3 --- /dev/null +++ b/app/src/main/res/layout/list_all_mails.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 4221c85..08af5f2 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -76,4 +76,9 @@ Klausur Andere Abgabe + Wintersemester %1$d/%2$d + Sommersemester %1$d + Dozenten + Mail Icon + Hallo %1$s, \n\n\n\nMit freundlichen Grüßen\n\n\n\nGesendet von der FUPlanner Android App \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5500875..9528137 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -84,4 +84,9 @@ Exam Other Deadline + Winter Semester %1$d/%2$d + Summer Semester %1$d + Lecturers + Mail Icon + Dear %1$s, \n\n\n\nYours sincerely\n\n\n\nSend by FUPlanner Android App