diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 80e1c62d..840c9b46 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -158,6 +158,10 @@
android:name=".activities.SettingsNotificationsActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|uiMode|keyboard|keyboardHidden|navigation" />
+
+
diff --git a/app/src/main/java/org/mian/gitnex/activities/AdminCronTasksActivity.java b/app/src/main/java/org/mian/gitnex/activities/AdminCronTasksActivity.java
new file mode 100644
index 00000000..7067cb29
--- /dev/null
+++ b/app/src/main/java/org/mian/gitnex/activities/AdminCronTasksActivity.java
@@ -0,0 +1,89 @@
+package org.mian.gitnex.activities;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.View;
+import androidx.appcompat.widget.Toolbar;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.recyclerview.widget.DividerItemDecoration;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import org.mian.gitnex.adapters.AdminCronTasksAdapter;
+import org.mian.gitnex.databinding.ActivityAdminCronTasksBinding;
+import org.mian.gitnex.helpers.Authorization;
+import org.mian.gitnex.viewmodels.AdminCronTasksViewModel;
+
+/**
+ * Author M M Arif
+ */
+
+public class AdminCronTasksActivity extends BaseActivity {
+
+ private View.OnClickListener onClickListener;
+ private AdminCronTasksAdapter adapter;
+
+ private ActivityAdminCronTasksBinding activityAdminCronTasksBinding;
+
+ public static final int PAGE = 1;
+ public static final int LIMIT = 50;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+
+ super.onCreate(savedInstanceState);
+
+ activityAdminCronTasksBinding = ActivityAdminCronTasksBinding.inflate(getLayoutInflater());
+ setContentView(activityAdminCronTasksBinding.getRoot());
+
+ initCloseListener();
+ activityAdminCronTasksBinding.close.setOnClickListener(onClickListener);
+
+ Toolbar toolbar = activityAdminCronTasksBinding.toolbar;
+ setSupportActionBar(toolbar);
+
+ activityAdminCronTasksBinding.recyclerView.setHasFixedSize(true);
+ activityAdminCronTasksBinding.recyclerView.setLayoutManager(new LinearLayoutManager(ctx));
+
+ DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(activityAdminCronTasksBinding.recyclerView.getContext(),
+ DividerItemDecoration.VERTICAL);
+ activityAdminCronTasksBinding.recyclerView.addItemDecoration(dividerItemDecoration);
+
+ activityAdminCronTasksBinding.pullToRefresh.setOnRefreshListener(() -> new Handler(Looper.getMainLooper()).postDelayed(() -> {
+
+ activityAdminCronTasksBinding.pullToRefresh.setRefreshing(false);
+ AdminCronTasksViewModel.loadCronTasksList(ctx, Authorization.get(ctx), PAGE, LIMIT);
+
+ }, 500));
+
+ fetchDataAsync(ctx, Authorization.get(ctx));
+ }
+
+ private void fetchDataAsync(Context ctx, String instanceToken) {
+
+ AdminCronTasksViewModel cronTasksViewModel = new ViewModelProvider(this).get(AdminCronTasksViewModel.class);
+
+ cronTasksViewModel.getCronTasksList(ctx, instanceToken, PAGE, LIMIT).observe(this, cronTasksListMain -> {
+
+ adapter = new AdminCronTasksAdapter(ctx, cronTasksListMain);
+
+ if(adapter.getItemCount() > 0) {
+
+ activityAdminCronTasksBinding.recyclerView.setVisibility(View.VISIBLE);
+ activityAdminCronTasksBinding.recyclerView.setAdapter(adapter);
+ activityAdminCronTasksBinding.noData.setVisibility(View.GONE);
+ }
+ else {
+
+ activityAdminCronTasksBinding.recyclerView.setVisibility(View.GONE);
+ activityAdminCronTasksBinding.noData.setVisibility(View.VISIBLE);
+ }
+
+ });
+
+ }
+
+ private void initCloseListener() {
+ onClickListener = view -> finish();
+ }
+}
diff --git a/app/src/main/java/org/mian/gitnex/adapters/AdminCronTasksAdapter.java b/app/src/main/java/org/mian/gitnex/adapters/AdminCronTasksAdapter.java
new file mode 100644
index 00000000..7ebf2c7f
--- /dev/null
+++ b/app/src/main/java/org/mian/gitnex/adapters/AdminCronTasksAdapter.java
@@ -0,0 +1,176 @@
+package org.mian.gitnex.adapters;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
+import androidx.recyclerview.widget.RecyclerView;
+import com.google.gson.JsonElement;
+import org.apache.commons.lang3.StringUtils;
+import org.mian.gitnex.R;
+import org.mian.gitnex.clients.RetrofitClient;
+import org.mian.gitnex.helpers.AlertDialogs;
+import org.mian.gitnex.helpers.TimeHelper;
+import org.mian.gitnex.helpers.TinyDB;
+import org.mian.gitnex.helpers.Toasty;
+import org.mian.gitnex.models.CronTasks;
+import java.util.List;
+import java.util.Locale;
+import retrofit2.Call;
+import retrofit2.Callback;
+
+/**
+ * Author M M Arif
+ */
+
+public class AdminCronTasksAdapter extends RecyclerView.Adapter {
+
+ private final List tasksList;
+ private final Context mCtx;
+ private static TinyDB tinyDb;
+
+ static class CronTasksViewHolder extends RecyclerView.ViewHolder {
+
+ private CronTasks cronTasks;
+
+ private final ImageView runTask;
+ private final TextView taskName;
+ private final LinearLayout cronTasksInfo;
+ private final LinearLayout cronTasksRun;
+
+ private CronTasksViewHolder(View itemView) {
+
+ super(itemView);
+ Context ctx = itemView.getContext();
+
+ final String locale = tinyDb.getString("locale");
+ final String timeFormat = tinyDb.getString("dateFormat");
+
+ runTask = itemView.findViewById(R.id.runTask);
+ taskName = itemView.findViewById(R.id.taskName);
+ cronTasksInfo = itemView.findViewById(R.id.cronTasksInfo);
+ cronTasksRun = itemView.findViewById(R.id.cronTasksRun);
+
+ cronTasksInfo.setOnClickListener(taskInfo -> {
+
+ String nextRun = "";
+ String lastRun = "";
+
+ if(cronTasks.getNext() != null) {
+ nextRun = TimeHelper.formatTime(cronTasks.getNext(), new Locale(locale), timeFormat, ctx);
+ }
+ if(cronTasks.getPrev() != null) {
+ lastRun = TimeHelper.formatTime(cronTasks.getPrev(), new Locale(locale), timeFormat, ctx);
+ }
+
+ View view = LayoutInflater.from(ctx).inflate(R.layout.layout_cron_task_info, null);
+
+ TextView taskScheduleContent = view.findViewById(R.id.taskScheduleContent);
+ TextView nextRunContent = view.findViewById(R.id.nextRunContent);
+ TextView lastRunContent = view.findViewById(R.id.lastRunContent);
+ TextView execTimeContent = view.findViewById(R.id.execTimeContent);
+
+ taskScheduleContent.setText(cronTasks.getSchedule());
+ nextRunContent.setText(nextRun);
+ lastRunContent.setText(lastRun);
+ execTimeContent.setText(String.valueOf(cronTasks.getExec_times()));
+
+ AlertDialog.Builder alertDialog = new AlertDialog.Builder(ctx);
+
+ alertDialog.setTitle(StringUtils.capitalize(cronTasks.getName().replace("_", " ")));
+ alertDialog.setView(view);
+ alertDialog.setPositiveButton(ctx.getString(R.string.close), null);
+ alertDialog.create().show();
+
+ });
+
+ cronTasksRun.setOnClickListener(taskInfo -> {
+
+ runCronTask(ctx, cronTasks.getName());
+ });
+ }
+ }
+
+ public AdminCronTasksAdapter(Context mCtx, List tasksListMain) {
+
+ tinyDb = TinyDB.getInstance(mCtx);
+ this.mCtx = mCtx;
+ this.tasksList = tasksListMain;
+ }
+
+ @NonNull
+ @Override
+ public AdminCronTasksAdapter.CronTasksViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+
+ View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_admin_cron_tasks, parent, false);
+ return new AdminCronTasksAdapter.CronTasksViewHolder(v);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull AdminCronTasksAdapter.CronTasksViewHolder holder, int position) {
+
+ CronTasks currentItem = tasksList.get(position);
+
+ holder.cronTasks = currentItem;
+ holder.taskName.setText(StringUtils.capitalize(currentItem.getName().replace("_", " ")));
+ }
+
+ private static void runCronTask(final Context ctx, final String taskName) {
+
+ final String loginUid = tinyDb.getString("loginUid");
+ final String instanceToken = "token " + tinyDb.getString(loginUid + "-token");
+
+ Call call = RetrofitClient
+ .getApiInterface(ctx)
+ .adminRunCronTask(instanceToken, taskName);
+
+ call.enqueue(new Callback() {
+
+ @Override
+ public void onResponse(@NonNull Call call, @NonNull retrofit2.Response response) {
+
+ switch(response.code()) {
+
+ case 204:
+ Toasty.success(ctx, ctx.getString(R.string.adminCronTaskSuccessMsg, taskName));
+ break;
+
+ case 401:
+ AlertDialogs.authorizationTokenRevokedDialog(ctx, ctx.getString(R.string.alertDialogTokenRevokedTitle),
+ ctx.getResources().getString(R.string.alertDialogTokenRevokedMessage),
+ ctx.getResources().getString(R.string.alertDialogTokenRevokedCopyNegativeButton),
+ ctx.getResources().getString(R.string.alertDialogTokenRevokedCopyPositiveButton));
+ break;
+
+ case 403:
+ Toasty.error(ctx, ctx.getString(R.string.authorizeError));
+ break;
+
+ case 404:
+ Toasty.warning(ctx, ctx.getString(R.string.apiNotFound));
+ break;
+
+ default:
+ Toasty.error(ctx, ctx.getString(R.string.genericError));
+
+ }
+ }
+
+ @Override
+ public void onFailure(@NonNull Call call, @NonNull Throwable t) {
+
+ Toasty.error(ctx, ctx.getString(R.string.genericServerResponseError));
+ }
+ });
+ }
+
+ @Override
+ public int getItemCount() {
+ return tasksList.size();
+ }
+}
diff --git a/app/src/main/java/org/mian/gitnex/fragments/AdministrationFragment.java b/app/src/main/java/org/mian/gitnex/fragments/AdministrationFragment.java
index a7b9f8b3..976fc950 100644
--- a/app/src/main/java/org/mian/gitnex/fragments/AdministrationFragment.java
+++ b/app/src/main/java/org/mian/gitnex/fragments/AdministrationFragment.java
@@ -1,5 +1,6 @@
package org.mian.gitnex.fragments;
+import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
@@ -8,8 +9,11 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
+import org.mian.gitnex.activities.AdminCronTasksActivity;
import org.mian.gitnex.activities.AdminGetUsersActivity;
import org.mian.gitnex.databinding.FragmentAdministrationBinding;
+import org.mian.gitnex.helpers.TinyDB;
+import org.mian.gitnex.helpers.Version;
/**
* Author M M Arif
@@ -17,12 +21,25 @@ import org.mian.gitnex.databinding.FragmentAdministrationBinding;
public class AdministrationFragment extends Fragment {
+ private Context ctx;
+ private TinyDB tinyDB;
+
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ ctx = getContext();
+ tinyDB = TinyDB.getInstance(ctx);
FragmentAdministrationBinding fragmentAdministrationBinding = FragmentAdministrationBinding.inflate(inflater, container, false);
fragmentAdministrationBinding.adminUsers.setOnClickListener(v1 -> startActivity(new Intent(getContext(), AdminGetUsersActivity.class)));
+ // if gitea version is greater/equal(1.13.0) than user installed version (installed.higherOrEqual(compareVer))
+ if(new Version(tinyDB.getString("giteaVersion")).higherOrEqual("1.13.0")) {
+
+ fragmentAdministrationBinding.adminCron.setVisibility(View.VISIBLE);
+ }
+
+ fragmentAdministrationBinding.adminCron.setOnClickListener(v1 -> startActivity(new Intent(getContext(), AdminCronTasksActivity.class)));
+
return fragmentAdministrationBinding.getRoot();
}
diff --git a/app/src/main/java/org/mian/gitnex/interfaces/ApiInterface.java b/app/src/main/java/org/mian/gitnex/interfaces/ApiInterface.java
index c8cf41ef..a1fc22e8 100644
--- a/app/src/main/java/org/mian/gitnex/interfaces/ApiInterface.java
+++ b/app/src/main/java/org/mian/gitnex/interfaces/ApiInterface.java
@@ -11,6 +11,7 @@ import org.mian.gitnex.models.CreateIssue;
import org.mian.gitnex.models.CreateLabel;
import org.mian.gitnex.models.CreatePullRequest;
import org.mian.gitnex.models.CreateStatusOption;
+import org.mian.gitnex.models.CronTasks;
import org.mian.gitnex.models.DeleteFile;
import org.mian.gitnex.models.EditFile;
import org.mian.gitnex.models.Emails;
@@ -430,5 +431,10 @@ public interface ApiInterface {
@GET("repos/{owner}/{repo}/statuses/{sha}") // Get a commit's statuses
Call> getCommitStatuses(@Header("Authorization") String token, @Path("owner") String owner, @Path("repo") String repo, @Query("sort") String sort, @Query("state") String state, @Query("page") int page, @Query("limit") int limit);
+ @GET("admin/cron") // get cron tasks
+ Call> adminGetCronTasks(@Header("Authorization") String token, @Query("page") int page, @Query("limit") int limit);
+
+ @POST("admin/cron/{taskName}") // run cron task
+ Call adminRunCronTask(@Header("Authorization") String token, @Path("taskName") String taskName);
}
diff --git a/app/src/main/java/org/mian/gitnex/models/CronTasks.java b/app/src/main/java/org/mian/gitnex/models/CronTasks.java
new file mode 100644
index 00000000..699abcfa
--- /dev/null
+++ b/app/src/main/java/org/mian/gitnex/models/CronTasks.java
@@ -0,0 +1,42 @@
+package org.mian.gitnex.models;
+
+import java.util.Date;
+
+/**
+ * Author M M Arif
+ */
+
+public class CronTasks {
+
+ private String name;
+ private String schedule;
+ private Date next;
+ private Date prev;
+ private int exec_times;
+
+ public String getName() {
+
+ return name;
+ }
+
+ public String getSchedule() {
+
+ return schedule;
+ }
+
+ public Date getNext() {
+
+ return next;
+ }
+
+ public Date getPrev() {
+
+ return prev;
+ }
+
+ public int getExec_times() {
+
+ return exec_times;
+ }
+
+}
diff --git a/app/src/main/java/org/mian/gitnex/viewmodels/AdminCronTasksViewModel.java b/app/src/main/java/org/mian/gitnex/viewmodels/AdminCronTasksViewModel.java
new file mode 100644
index 00000000..e7f0c0a3
--- /dev/null
+++ b/app/src/main/java/org/mian/gitnex/viewmodels/AdminCronTasksViewModel.java
@@ -0,0 +1,79 @@
+package org.mian.gitnex.viewmodels;
+
+import android.content.Context;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+import org.mian.gitnex.R;
+import org.mian.gitnex.clients.RetrofitClient;
+import org.mian.gitnex.helpers.AlertDialogs;
+import org.mian.gitnex.helpers.Toasty;
+import org.mian.gitnex.models.CronTasks;
+import java.util.List;
+import retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
+
+/**
+ * Author M M Arif
+ */
+
+public class AdminCronTasksViewModel extends ViewModel {
+
+ private static MutableLiveData> tasksList;
+
+ public LiveData> getCronTasksList(Context ctx, String token, int page, int limit) {
+
+ tasksList = new MutableLiveData<>();
+ loadCronTasksList(ctx, token, page, limit);
+
+ return tasksList;
+ }
+
+ public static void loadCronTasksList(final Context ctx, String token, int page, int limit) {
+
+ Call> call = RetrofitClient
+ .getApiInterface(ctx)
+ .adminGetCronTasks(token, page, limit);
+
+ call.enqueue(new Callback>() {
+
+ @Override
+ public void onResponse(@NonNull Call> call, @NonNull Response> response) {
+
+ if (response.code() == 200) {
+ tasksList.postValue(response.body());
+ }
+
+ else if(response.code() == 401) {
+
+ AlertDialogs.authorizationTokenRevokedDialog(ctx, ctx.getResources().getString(R.string.alertDialogTokenRevokedTitle),
+ ctx.getResources().getString(R.string.alertDialogTokenRevokedMessage),
+ ctx.getResources().getString(R.string.alertDialogTokenRevokedCopyNegativeButton),
+ ctx.getResources().getString(R.string.alertDialogTokenRevokedCopyPositiveButton));
+ }
+ else if(response.code() == 403) {
+
+ Toasty.error(ctx, ctx.getString(R.string.authorizeError));
+ }
+ else if(response.code() == 404) {
+
+ Toasty.warning(ctx, ctx.getString(R.string.apiNotFound));
+ }
+ else {
+
+ Toasty.error(ctx, ctx.getString(R.string.genericError));
+ Log.i("onResponse", String.valueOf(response.code()));
+ }
+ }
+
+ @Override
+ public void onFailure(@NonNull Call> call, @NonNull Throwable t) {
+ Log.e("onFailure", t.toString());
+ }
+
+ });
+ }
+}
diff --git a/app/src/main/res/drawable/ic_play.xml b/app/src/main/res/drawable/ic_play.xml
new file mode 100644
index 00000000..9bf43095
--- /dev/null
+++ b/app/src/main/res/drawable/ic_play.xml
@@ -0,0 +1,13 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_tasks.xml b/app/src/main/res/drawable/ic_tasks.xml
new file mode 100644
index 00000000..c60621ba
--- /dev/null
+++ b/app/src/main/res/drawable/ic_tasks.xml
@@ -0,0 +1,20 @@
+
+
+
+
diff --git a/app/src/main/res/layout/activity_admin_cron_tasks.xml b/app/src/main/res/layout/activity_admin_cron_tasks.xml
new file mode 100644
index 00000000..56bac93f
--- /dev/null
+++ b/app/src/main/res/layout/activity_admin_cron_tasks.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_administration.xml b/app/src/main/res/layout/fragment_administration.xml
index 9c810f13..d74afcaf 100644
--- a/app/src/main/res/layout/fragment_administration.xml
+++ b/app/src/main/res/layout/fragment_administration.xml
@@ -26,6 +26,25 @@
android:padding="16dp"
app:drawableStartCompat="@drawable/ic_people" />
+
+
+
+
diff --git a/app/src/main/res/layout/layout_cron_task_info.xml b/app/src/main/res/layout/layout_cron_task_info.xml
new file mode 100644
index 00000000..a3c2537b
--- /dev/null
+++ b/app/src/main/res/layout/layout_cron_task_info.xml
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/list_admin_cron_tasks.xml b/app/src/main/res/layout/list_admin_cron_tasks.xml
new file mode 100644
index 00000000..458d4356
--- /dev/null
+++ b/app/src/main/res/layout/list_admin_cron_tasks.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/list_drafts.xml b/app/src/main/res/layout/list_drafts.xml
index e0a9f0a3..23f853de 100644
--- a/app/src/main/res/layout/list_drafts.xml
+++ b/app/src/main/res/layout/list_drafts.xml
@@ -33,7 +33,7 @@
android:id="@+id/editCommentStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginLeft="10dp"
+ android:layout_marginStart="10dp"
android:contentDescription="@string/menuDeleteText"
android:src="@drawable/ic_edit" />
@@ -41,7 +41,7 @@
android:id="@+id/deleteDraft"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginLeft="10dp"
+ android:layout_marginStart="10dp"
android:contentDescription="@string/menuDeleteText"
android:src="@drawable/ic_delete" />
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 9c071121..bc683c6f 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -412,6 +412,12 @@
Add New User
System Users
Admin
+ Cron Tasks
+ Schedule
+ Next Run
+ Last Run
+ Executions
+ Task %1$s is initiated successfully