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