diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/README.md b/README.md index ac52d9c..eda3a1c 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# TaskTraker \ No newline at end of file +Это клиентская часть приложения. Если вы хотите запустить его локально замените IP в файле ApiFactory на ваш. \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..6b831bb --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,63 @@ +plugins { + alias(libs.plugins.android.application) +} + +android { + namespace = "com.example.tasktracker" + compileSdk = 35 + + defaultConfig { + applicationId = "com.example.tasktracker" + minSdk = 27 + targetSdk = 35 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildFeatures { + viewBinding = true + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} + +dependencies { + implementation(libs.room.runtime) + // If this project only uses Java source, use the Java annotationProcessor + // No additional plugins are necessary + annotationProcessor(libs.room.compiler) + // optional - RxJava3 support for Room + implementation(libs.room.rxjava3) + + implementation(libs.gson) + implementation(libs.retrofit) + implementation(libs.converter.gson) + implementation(libs.adapter.rxjava3) + implementation(libs.rxandroid) + // Because RxAndroid releases are few and far between, it is recommended you also + // explicitly depend on RxJava's latest version for bug fixes and new features. + // (see https://github.com/ReactiveX/RxJava/releases for latest 3.x.x version) + implementation(libs.rxjava) + + implementation(libs.appcompat) + implementation(libs.material) + implementation(libs.activity) + implementation(libs.constraintlayout) + testImplementation(libs.junit) + androidTestImplementation(libs.ext.junit) + androidTestImplementation(libs.espresso.core) +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/tasktracker/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/example/tasktracker/ExampleInstrumentedTest.java new file mode 100644 index 0000000..9b4e559 --- /dev/null +++ b/app/src/androidTest/java/com/example/tasktracker/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.example.tasktracker; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.example.tasktracker", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..819adcd --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..39a16aa Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/com/example/tasktracker/Activities/AddItemViewModel.java b/app/src/main/java/com/example/tasktracker/Activities/AddItemViewModel.java new file mode 100644 index 0000000..d48a1f3 --- /dev/null +++ b/app/src/main/java/com/example/tasktracker/Activities/AddItemViewModel.java @@ -0,0 +1,63 @@ +package com.example.tasktracker.Activities; + +import android.app.Application; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.Observer; + +import com.example.tasktracker.Answers.Task; +import com.example.tasktracker.Api.ApiFactory; +import com.example.tasktracker.Api.TaskRepository; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class AddItemViewModel extends AndroidViewModel { + private static final String TAG = "ADD_ITEM_VIEW_MODEL"; + private final MutableLiveData liveDataSave = new MutableLiveData<>(); + private final MutableLiveData Failure = new MutableLiveData<>(); + + public AddItemViewModel(@NonNull Application application) { + super(application); + } + + public void OfflineSaveData(Task task) { + new Thread(new Runnable() { + @Override + public void run() { + TaskRepository.newInstance(getApplication()).taskDao().addTask(task); + liveDataSave.postValue(true); + } + }).start(); + } + + public void OnlineSaveData(Task task, String token) { + + Call send = ApiFactory.getApiService().addTask(token, task); + send.enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + Log.d(TAG, "Task add successfully"); + liveDataSave.postValue(true); + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable throwable) { + Log.w(TAG, throwable.toString()); + Failure.postValue(throwable.getMessage()); + } + }); + } + + public LiveData getFailure() { + return Failure; + } + public LiveData getClose() { + return liveDataSave; + } +} diff --git a/app/src/main/java/com/example/tasktracker/Activities/AddNewTask.java b/app/src/main/java/com/example/tasktracker/Activities/AddNewTask.java new file mode 100644 index 0000000..ef52dbb --- /dev/null +++ b/app/src/main/java/com/example/tasktracker/Activities/AddNewTask.java @@ -0,0 +1,135 @@ +package com.example.tasktracker.Activities; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.lifecycle.Observer; +import androidx.lifecycle.ViewModelProvider; + +import com.example.tasktracker.Answers.Task; +import com.example.tasktracker.R; +import com.example.tasktracker.databinding.ActivityAddNewTaskBinding; +import com.google.android.material.snackbar.Snackbar; + +import java.util.Random; + +public class AddNewTask extends AppCompatActivity { + private static final String TAG = "ADD_NEW_TASK"; + private static final String TOKEN_KEY = "TOKEN"; + + private ActivityAddNewTaskBinding binding; + private static String token; + private AddItemViewModel viewModel; + + @Override + protected void onCreate(Bundle savedInstanceState) { + binding = ActivityAddNewTaskBinding.inflate(getLayoutInflater()); + super.onCreate(savedInstanceState); + setContentView(binding.getRoot()); + + token = getIntent().getStringExtra(TOKEN_KEY); + if(token == null) + { + Log.w(TAG, "Token is null"); + return; + } + + viewModel = new ViewModelProvider(this).get(AddItemViewModel.class); + viewModel.getClose().observe(this, new Observer() { + @Override + public void onChanged(Boolean aBoolean) { + if(aBoolean) + finish(); + } + }); + + // Offline + if(token.equals("null")) { + Offline(); + } + // Online + else { + Online(); + } + } + + private void Offline() { + binding.AddTask.setOnClickListener(v->{ + String name = binding.TaskName.getText().toString(); + String description = binding.TaskDescription.getText().toString(); + String date = getDate(); + + if(name.isEmpty()) { + Snackbar.make(binding.getRoot(), R.string.snackbar_EnterName, Snackbar.LENGTH_SHORT).show(); + return; + } + if(date.isEmpty()) { + Snackbar.make(binding.getRoot(), R.string.snackbar_EnterDate, Snackbar.LENGTH_SHORT).show(); + return; + } + + long ID = generateID(name, description, date); + Task task = new Task(ID, name, description, date); + viewModel.OfflineSaveData(task); + }); + } + + private void Online() { + viewModel.getFailure().observe(this, new Observer() { + @Override + public void onChanged(String s) { + Snackbar.make(binding.getRoot(), R.string.snackbar_InternetError, Snackbar.LENGTH_SHORT).show(); + } + }); + + binding.AddTask.setOnClickListener(v->{ + String name = binding.TaskName.getText().toString(); + String description = binding.TaskDescription.getText().toString(); + String date = getDate(); + + if(name.isEmpty()) { + Snackbar.make(binding.getRoot(), R.string.snackbar_EnterName, Snackbar.LENGTH_SHORT).show(); + return; + } + if(date.isEmpty()) { + Snackbar.make(binding.getRoot(), R.string.snackbar_EnterDate, Snackbar.LENGTH_SHORT).show(); + return; + } + + long ID = generateID(name, description, date); + Task task = new Task(ID, name, description, date); + viewModel.OnlineSaveData(task, token); + }); + } + + private String getDate() { + String day = binding.day.getText().toString(); + String month = binding.month.getText().toString(); + String year = binding.year.getText().toString(); + String minute = binding.minute.getText().toString(); + String hour = binding.hour.getText().toString(); + + return day + '.' + month + '.' + year + ' ' + minute + ':' + hour; + } + + private long generateID(String name, String Description, String date) { + Random rand = new Random(); + + long key1 = name.hashCode(); + long key2 = Description.hashCode(); + long key3 = date.hashCode(); + long key4 = rand.nextInt(); + + long hash = key1 ^ key2 ^ key3 ^ key4; + return hash; + } + + public static Intent getIntent(Context context, String token) { + Intent intent = new Intent(context, AddNewTask.class); + intent.putExtra(TOKEN_KEY, token); + return intent; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/tasktracker/Activities/LoginActivity.java b/app/src/main/java/com/example/tasktracker/Activities/LoginActivity.java new file mode 100644 index 0000000..9f08c41 --- /dev/null +++ b/app/src/main/java/com/example/tasktracker/Activities/LoginActivity.java @@ -0,0 +1,137 @@ +package com.example.tasktracker.Activities; + +import android.annotation.SuppressLint; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; + +import androidx.activity.EdgeToEdge; +import androidx.appcompat.app.AppCompatActivity; + +import com.example.tasktracker.Answers.LoginAnswer; +import com.example.tasktracker.Api.ApiFactory; +import com.example.tasktracker.R; +import com.example.tasktracker.databinding.ActivityLoginBinding; +import com.google.android.material.snackbar.Snackbar; + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.disposables.CompositeDisposable; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.schedulers.Schedulers; + +public class LoginActivity extends AppCompatActivity { + private static final String TAG = "LOGIN_ACTIVITY"; + private ActivityLoginBinding binding; + private CompositeDisposable compositeDisposable = new CompositeDisposable(); + + @SuppressLint("UnsafeIntentLaunch") + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + binding = ActivityLoginBinding.inflate(getLayoutInflater()); + EdgeToEdge.enable(this); + setContentView(binding.getRoot()); + + binding.buttonSingIn.setOnClickListener(v->{ SingIn(); }); + + binding.buttonSingUp.setOnClickListener(v->{ SingUp(); }); + + binding.buttonOffline.setOnClickListener(v->{ + Intent intent = MainMenu.getIntent(this, "null"); + startActivity(intent); + finish(); + }); + } + + private void SingIn() + { + String login = binding.fieldLogin.getText().toString(); + if(login.isEmpty()) + { + Snackbar.make(binding.getRoot(), R.string.snackbar_EnterLogin, Snackbar.LENGTH_SHORT).show(); + return; + } + + String pass = binding.fieldPassword.getText().toString(); + if(pass.isEmpty()) + { + Snackbar.make(binding.getRoot(), R.string.snackbar_EnterPassword, Snackbar.LENGTH_SHORT).show(); + return; + } + + Single loginAnswer = ApiFactory.getApiService().singin(login, pass); + Disposable disposable = loginAnswer + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Consumer() { + @Override + public void accept(LoginAnswer loginAnswer) throws Throwable { + if (loginAnswer.getStatus().equals("error")) { + Snackbar.make(binding.getRoot(), R.string.snackbar_InvalidLogin, Snackbar.LENGTH_SHORT).show(); + return; + } + + String token = loginAnswer.getToken(); + OpenMainMenu(token); + } + }, new Consumer() { + @Override + public void accept(Throwable throwable) throws Throwable { + Log.w(TAG, throwable.toString()); + Snackbar.make(binding.getRoot(), R.string.snackbar_InternetError, Snackbar.LENGTH_LONG).show(); + } + }); + compositeDisposable.add(disposable); + } + + private void SingUp() + { + String login = binding.fieldLogin.getText().toString(); + if(login.isEmpty()) + { + Snackbar.make(binding.getRoot(), R.string.snackbar_EnterLogin, Snackbar.LENGTH_SHORT).show(); + return; + } + + String pass = binding.fieldPassword.getText().toString(); + if(pass.isEmpty()) + { + Snackbar.make(binding.getRoot(), R.string.snackbar_EnterPassword, Snackbar.LENGTH_SHORT).show(); + return; + } + + Single loginAnswer = ApiFactory.getApiService().singup(login, pass); + Disposable disposable = loginAnswer + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Consumer() { + @Override + public void accept(LoginAnswer loginAnswer) throws Throwable { + if (loginAnswer.getStatus().equals("error")) { + Snackbar.make(binding.getRoot(), R.string.snackbar_AlredyCreated, Snackbar.LENGTH_LONG).show(); + return; + } + + String token = loginAnswer.getToken(); + OpenMainMenu(token); + } + }, new Consumer() { + @Override + public void accept(Throwable throwable) throws Throwable { + Snackbar.make(binding.getRoot(), R.string.snackbar_InternetError, Snackbar.LENGTH_LONG).show(); + Log.w(TAG, throwable.toString()); + } + }); + compositeDisposable.add(disposable); + } + + private void OpenMainMenu(String token) + { + @SuppressLint("UnsafeIntentLaunch") + Intent intent = MainMenu.getIntent(this, token); + startActivity(intent); + finish(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/tasktracker/Activities/MainMenu.java b/app/src/main/java/com/example/tasktracker/Activities/MainMenu.java new file mode 100644 index 0000000..996cf39 --- /dev/null +++ b/app/src/main/java/com/example/tasktracker/Activities/MainMenu.java @@ -0,0 +1,232 @@ +package com.example.tasktracker.Activities; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.lifecycle.Observer; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.RecyclerView; + +import com.example.tasktracker.Answers.Task; +import com.example.tasktracker.Api.ApiFactory; +import com.example.tasktracker.Api.TaskRepository; +import com.example.tasktracker.R; +import com.example.tasktracker.databinding.ActivityMainMenuBinding; +import com.google.android.material.snackbar.Snackbar; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class MainMenu extends AppCompatActivity { + private static final String TAG = "MainMenu"; + private static final String TOKEN_KEY = "TOKEN"; + + private ActivityMainMenuBinding binding; + private String token; + private TaskAdapter taskAdapter; + private MainMenuViewModel viewModel; + + private List taskListCache; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + binding = ActivityMainMenuBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + + token = getIntent().getStringExtra(TOKEN_KEY); + if(token == null) + { + Log.e(TAG, "Token is empty"); + return; + } + taskAdapter = new TaskAdapter(); + binding.scrollTask.setAdapter(taskAdapter); + + binding.buttonAddNewTask.setOnClickListener(v->{ + Intent intent = AddNewTask.getIntent(this, token); + startActivity(intent); + }); + viewModel = new ViewModelProvider(this).get(MainMenuViewModel.class); + + binding.findLine.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { + + } + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { + String find = binding.findLine.getText().toString(); + if(find.isEmpty()) { + if(token.equals("null")) { + TaskRepository.newInstance(getApplication()).taskDao().getTaskList().observe(MainMenu.this, new Observer>() { + @Override + public void onChanged(List tasks) { + taskListCache = tasks; + taskAdapter.setTaskList(tasks); + } + }); + } + else { + viewModel.OnlineUpdateList(token); + } + } + new Thread(new Runnable() { + @Override + public void run() { + List searchList = new ArrayList<>(); + for (int i = 0; i < taskListCache.size(); i++) { + if (taskListCache.get(i).getTaskName().toLowerCase().contains(charSequence) || + taskListCache.get(i).getTaskDescription().toLowerCase().contains(charSequence)) { + searchList.add(taskListCache.get(i)); + } + } + new Handler(getMainLooper()).post(new Runnable() { + @Override + public void run() { + taskAdapter.setTaskList(searchList); + } + }); + } + }).start(); + } + + @Override + public void afterTextChanged(Editable editable) { + + } + }); + + // offline + if(token.equals("null")) { + Offline(); + } + //online + else { + Online(); + } + } + + public void Offline() { + viewModel.getOnLoad().observe(this, new Observer>() { + @Override + public void onChanged(List tasks) { + taskListCache = tasks; + taskAdapter.setTaskList(tasks); + } + }); + + ItemTouchHelper helper = new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(0, + ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) { + @Override + public boolean onMove(@NonNull RecyclerView recyclerView, + @NonNull RecyclerView.ViewHolder viewHolder, + @NonNull RecyclerView.ViewHolder target) { + return false; + } + + @Override + public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { + int position = viewHolder.getAdapterPosition(); + + Task task = taskAdapter.getItem(position); + new Thread(new Runnable() { + @Override + public void run() { + TaskRepository.newInstance(getApplication()).taskDao().delete(task); + } + }).start(); + taskAdapter.removeItemByPosition(position); + } + }); + helper.attachToRecyclerView(binding.scrollTask); + } + + private void Online() { + + viewModel.getOnLoad().observe(this, new Observer>() { + @Override + public void onChanged(List tasks) { + taskListCache = tasks; + taskAdapter.setTaskList(tasks); + } + }); + viewModel.onFail().observe(this, new Observer() { + @Override + public void onChanged(String s) { + Snackbar.make(binding.getRoot(), R.string.snackbar_InternetError, Snackbar.LENGTH_LONG).show(); + Log.w(TAG, s); + } + }); + + ItemTouchHelper helper = new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(0, + ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) { + @Override + public boolean onMove(@NonNull RecyclerView recyclerView, + @NonNull RecyclerView.ViewHolder viewHolder, + @NonNull RecyclerView.ViewHolder target) { + return false; + } + + @Override + public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { + int position = viewHolder.getAdapterPosition(); + + Task task = taskAdapter.getItem(position); + ApiFactory.getApiService().removeTask(token, task.getID()).enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + Task task = taskAdapter.getItem(position); + taskAdapter.removeItemByPosition(position); + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable throwable) { + Snackbar.make(binding.getRoot(), R.string.snackbar_InternetError, Snackbar.LENGTH_LONG).show(); + Log.w(TAG, Objects.requireNonNull(throwable.getMessage())); + } + }); + } + }); + helper.attachToRecyclerView(binding.scrollTask); + } + + @Override + protected void onResume() { + super.onResume(); + if(token.equals("null")) { + TaskRepository.newInstance(getApplication()).taskDao().getTaskList().observe(this, new Observer>() { + @Override + public void onChanged(List tasks) { + taskListCache = tasks; + taskAdapter.setTaskList(tasks); + } + }); + } + else { + viewModel.OnlineUpdateList(token); + } + } + + public static Intent getIntent(Context context, String token) { + Intent intent = new Intent(context, MainMenu.class); + intent.putExtra(TOKEN_KEY, token); + return intent; + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/tasktracker/Activities/MainMenuViewModel.java b/app/src/main/java/com/example/tasktracker/Activities/MainMenuViewModel.java new file mode 100644 index 0000000..0057f85 --- /dev/null +++ b/app/src/main/java/com/example/tasktracker/Activities/MainMenuViewModel.java @@ -0,0 +1,62 @@ +package com.example.tasktracker.Activities; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.Observer; + +import com.example.tasktracker.Answers.Task; +import com.example.tasktracker.Api.ApiFactory; +import com.example.tasktracker.Api.TaskRepository; + +import java.util.ArrayList; +import java.util.List; + +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.disposables.CompositeDisposable; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.schedulers.Schedulers; + +public class MainMenuViewModel extends AndroidViewModel { + private final CompositeDisposable compositeDisposable = new CompositeDisposable(); + private final MutableLiveData> Load = new MutableLiveData<>(); + private final MutableLiveData failLoad = new MutableLiveData<>(); + + public MainMenuViewModel(@NonNull Application application) { + super(application); + } + + public void OnlineUpdateList(String token) { + Single> single = ApiFactory.getApiService().getTaskList(token); + Disposable disposable = single.subscribeOn(Schedulers.io()).subscribe(new Consumer>() { + @Override + public void accept(ArrayList tasks) throws Throwable { + Load.postValue(tasks); + } + }, new Consumer() { + @Override + public void accept(Throwable throwable) throws Throwable { + failLoad.postValue(throwable.getMessage()); + } + }); + compositeDisposable.add(disposable); + } + + public LiveData> getOnLoad() { + return Load; + } + + public LiveData onFail() { + return failLoad; + } + + @Override + protected void onCleared() { + super.onCleared(); + compositeDisposable.dispose(); + } +} diff --git a/app/src/main/java/com/example/tasktracker/Activities/TaskAdapter.java b/app/src/main/java/com/example/tasktracker/Activities/TaskAdapter.java new file mode 100644 index 0000000..cbef260 --- /dev/null +++ b/app/src/main/java/com/example/tasktracker/Activities/TaskAdapter.java @@ -0,0 +1,82 @@ +package com.example.tasktracker.Activities; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.example.tasktracker.Answers.Task; +import com.example.tasktracker.R; +import com.example.tasktracker.databinding.ItemBinding; + +import java.util.ArrayList; +import java.util.List; + +public class TaskAdapter extends RecyclerView.Adapter { + private List taskList = new ArrayList<>(); + + public List getTaskList() { + return taskList; + } + + public void setTaskList(@NonNull List taskList) + { + this.taskList = taskList; + notifyDataSetChanged(); + } + + @NonNull + @Override + public TaskViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item, parent, false); + + return new TaskViewHolder(ItemBinding.bind(view)); + } + + @Override + public void onBindViewHolder(@NonNull TaskViewHolder holder, int position) { + Task task = taskList.get(position); + holder.binding.TaskName.setText(task.getTaskName()); + holder.binding.TaskDescription.setText(task.getTaskDescription()); + holder.binding.TaskDate.setText(task.getTaskDate()); + holder.binding.getRoot().setOnClickListener(v->{ + if(onItemClick != null) + onItemClick.Click(position); + }); + } + + interface OnItemClick { + void Click(int position); + } + OnItemClick onItemClick; + void setOnItemClick(OnItemClick onItemClick) { + this.onItemClick = onItemClick; + } + + @Override + public int getItemCount() { + return taskList.size(); + } + + Task getItem(int position) { + return taskList.get(position); + } + + void removeItemByPosition(int position) + { + taskList.remove(position); + notifyDataSetChanged(); + } + + static class TaskViewHolder extends RecyclerView.ViewHolder { + ItemBinding binding; + TaskViewHolder(ItemBinding binding) + { + super(binding.getRoot()); + this.binding = binding; + } + } +} diff --git a/app/src/main/java/com/example/tasktracker/Answers/LoginAnswer.java b/app/src/main/java/com/example/tasktracker/Answers/LoginAnswer.java new file mode 100644 index 0000000..fe79501 --- /dev/null +++ b/app/src/main/java/com/example/tasktracker/Answers/LoginAnswer.java @@ -0,0 +1,21 @@ +package com.example.tasktracker.Answers; + +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; + +public class LoginAnswer implements Serializable { + @SerializedName("status") + private String status; + + @SerializedName("token") + private String token; + + public String getStatus() { + return status; + } + + public String getToken() { + return token; + } +} diff --git a/app/src/main/java/com/example/tasktracker/Answers/Task.java b/app/src/main/java/com/example/tasktracker/Answers/Task.java new file mode 100644 index 0000000..aad0b36 --- /dev/null +++ b/app/src/main/java/com/example/tasktracker/Answers/Task.java @@ -0,0 +1,84 @@ +package com.example.tasktracker.Answers; + +import androidx.room.Entity; +import androidx.room.Ignore; +import androidx.room.PrimaryKey; + +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; +import java.util.Objects; + +@Entity(tableName = "task_room") +public class Task implements Serializable { + @PrimaryKey(autoGenerate = false) + @SerializedName("id") + private long ID; + + @SerializedName("taskName") + private String TaskName; + + + @SerializedName("taskDescription") + private String TaskDescription; + + @SerializedName("taskDate") + private String TaskDate; + + public Task() { + + } + + @Ignore + public Task(long ID, String TaskName, String TaskDescription, String TaskData) { + this.TaskName = TaskName; + this.TaskDescription = TaskDescription; + this.TaskDate = TaskData; + this.ID = ID; + } + + public String getTaskName() { + return TaskName; + } + + public String getTaskDescription() { + return TaskDescription; + } + + public String getTaskDate() { + return TaskDate; + } + + public long getID() { + return ID; + } + + public void setTaskName(String taskName) { + TaskName = taskName; + } + + public void setTaskDescription(String taskDescription) { + TaskDescription = taskDescription; + } + + public void setTaskDate(String taskDate) { + TaskDate = taskDate; + } + + public void setID(long id) { + ID = id; + } + + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + Task task = (Task) o; + return ID == task.ID && Objects.equals(TaskName, task.TaskName) && Objects.equals(TaskDescription, task.TaskDescription) && Objects.equals(TaskDate, task.TaskDate); + } + + @Override + public int hashCode() { + return Objects.hash(ID, TaskName, TaskDescription, TaskDate); + } +} diff --git a/app/src/main/java/com/example/tasktracker/Api/ApiFactory.java b/app/src/main/java/com/example/tasktracker/Api/ApiFactory.java new file mode 100644 index 0000000..024cb14 --- /dev/null +++ b/app/src/main/java/com/example/tasktracker/Api/ApiFactory.java @@ -0,0 +1,24 @@ +package com.example.tasktracker.Api; + +import retrofit2.Retrofit; +import retrofit2.adapter.rxjava3.RxJava3CallAdapterFactory; +import retrofit2.converter.gson.GsonConverterFactory; + +public class ApiFactory { + private static final String BASE_URL = "http://192.168.0.3:8080/"; + + private static ApiService apiService = null; + + public static ApiService getApiService() + { + if(apiService == null) + { + Retrofit retrofit = new Retrofit.Builder().baseUrl(BASE_URL) + .addConverterFactory(GsonConverterFactory.create()) + .addCallAdapterFactory(RxJava3CallAdapterFactory.create()) + .build(); + apiService = retrofit.create(ApiService.class); + } + return apiService; + } +} diff --git a/app/src/main/java/com/example/tasktracker/Api/ApiService.java b/app/src/main/java/com/example/tasktracker/Api/ApiService.java new file mode 100644 index 0000000..21fcc55 --- /dev/null +++ b/app/src/main/java/com/example/tasktracker/Api/ApiService.java @@ -0,0 +1,30 @@ +package com.example.tasktracker.Api; + +import com.example.tasktracker.Answers.LoginAnswer; +import com.example.tasktracker.Answers.Task; + +import java.util.ArrayList; + +import io.reactivex.rxjava3.core.Single; +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.GET; +import retrofit2.http.POST; +import retrofit2.http.Query; + +public interface ApiService { + @GET("singin") + Single singin(@Query("login") String login, @Query("password") String password); + + @POST("singup") + Single singup(@Query("login") String login, @Query("password") String password); + + @GET("user/task") + Single> getTaskList(@Query("token") String token); + + @POST("user/task/add") + Call addTask(@Query("token") String token, @Body Task task); + + @POST("user/task/remove") + Call removeTask(@Query("token") String token, @Query("id") long ID); +} diff --git a/app/src/main/java/com/example/tasktracker/Api/TaskDao.java b/app/src/main/java/com/example/tasktracker/Api/TaskDao.java new file mode 100644 index 0000000..80401e4 --- /dev/null +++ b/app/src/main/java/com/example/tasktracker/Api/TaskDao.java @@ -0,0 +1,29 @@ +package com.example.tasktracker.Api; + +import androidx.lifecycle.LiveData; +import androidx.room.Dao; +import androidx.room.Delete; +import androidx.room.Insert; +import androidx.room.OnConflictStrategy; +import androidx.room.Query; + +import com.example.tasktracker.Answers.Task; + +import java.util.List; + +import retrofit2.Call; + +@Dao +public interface TaskDao { + @Query("SELECT * FROM task_room") + LiveData> getTaskList(); + + @Query("DELETE FROM task_room") + void DropTable(); + + @Insert(onConflict = OnConflictStrategy.REPLACE) + void addTask(Task... task); + + @Delete + void delete(Task task); +} diff --git a/app/src/main/java/com/example/tasktracker/Api/TaskRepository.java b/app/src/main/java/com/example/tasktracker/Api/TaskRepository.java new file mode 100644 index 0000000..c865e34 --- /dev/null +++ b/app/src/main/java/com/example/tasktracker/Api/TaskRepository.java @@ -0,0 +1,26 @@ +package com.example.tasktracker.Api; + +import android.app.Application; + +import androidx.room.Database; +import androidx.room.Room; +import androidx.room.RoomDatabase; + +import com.example.tasktracker.Answers.Task; + +@Database(entities = {Task.class}, version = 1) +public abstract class TaskRepository extends RoomDatabase { + private static final String DATABASE_NAME = "task.db"; + private static TaskRepository instance = null; + + public static TaskRepository newInstance(Application app) { + if(instance == null) { + instance = Room.databaseBuilder(app, + TaskRepository.class, + DATABASE_NAME).build(); + } + return instance; + } + + public abstract TaskDao taskDao(); +} diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..ca3826a --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/icon.png b/app/src/main/res/drawable/icon.png new file mode 100644 index 0000000..9f796c7 Binary files /dev/null and b/app/src/main/res/drawable/icon.png differ diff --git a/app/src/main/res/layout/activity_add_new_task.xml b/app/src/main/res/layout/activity_add_new_task.xml new file mode 100644 index 0000000..24547b9 --- /dev/null +++ b/app/src/main/res/layout/activity_add_new_task.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + +