From 041f2ca3d488e860fd8a60b09eb0e9d421e41de9 Mon Sep 17 00:00:00 2001 From: M66B Date: Thu, 14 Feb 2019 12:28:03 +0000 Subject: [PATCH] Added simple scheduling --- .../eu/faircode/email/FragmentOptions.java | 94 +++++++++++++++++-- .../main/java/eu/faircode/email/JobDaily.java | 19 ++-- .../eu/faircode/email/ServiceSynchronize.java | 90 ++++++++++++++++-- app/src/main/res/layout/fragment_options.xml | 80 ++++++++++++++-- app/src/main/res/values/strings.xml | 4 +- 5 files changed, 254 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/eu/faircode/email/FragmentOptions.java b/app/src/main/java/eu/faircode/email/FragmentOptions.java index 10d5485b16..cd97ceaa10 100644 --- a/app/src/main/java/eu/faircode/email/FragmentOptions.java +++ b/app/src/main/java/eu/faircode/email/FragmentOptions.java @@ -19,6 +19,8 @@ package eu.faircode.email; Copyright 2018-2019 by Marcel Bokhorst (M66B) */ +import android.app.Dialog; +import android.app.TimePickerDialog; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -31,6 +33,7 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; +import android.text.format.DateFormat; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -42,13 +45,17 @@ import android.widget.Button; import android.widget.CompoundButton; import android.widget.Spinner; import android.widget.TextView; +import android.widget.TimePicker; +import java.text.SimpleDateFormat; +import java.util.Calendar; import java.util.List; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.SwitchCompat; import androidx.constraintlayout.widget.Group; +import androidx.fragment.app.DialogFragment; import androidx.fragment.app.FragmentActivity; import androidx.lifecycle.Lifecycle; @@ -56,7 +63,8 @@ import static android.app.Activity.RESULT_OK; public class FragmentOptions extends FragmentBase implements SharedPreferences.OnSharedPreferenceChangeListener { private SwitchCompat swEnabled; - private SwitchCompat swUpdates; + private TextView tvScheduleStart; + private TextView tvScheduleEnd; private TextView tvConnectionType; private SwitchCompat swMetered; @@ -90,6 +98,7 @@ public class FragmentOptions extends FragmentBase implements SharedPreferences.O private SwitchCompat swLight; private Button btnSound; + private SwitchCompat swUpdates; private SwitchCompat swDebug; private Group grpNotification; @@ -101,13 +110,13 @@ public class FragmentOptions extends FragmentBase implements SharedPreferences.O }; private final static String[] ADVANCED_OPTIONS = new String[]{ - "enabled", "updates", + "enabled", "schedule_start", "schedule_end", "metered", "download", "unified", "date", "threading", "avatars", "identicons", "name_email", "preview", "addresses", "autoimages", "actionbar", "pull", "swipenav", "autoexpand", "autoclose", "autonext", "collapse", "autoread", "automove", "autoresize", "sender", "autosend", "notify_preview", "light", "sound", - "debug", + "updates", "debug", "first", "why", "last_update_check", "app_support", "message_swipe", "message_select", "folder_actions", "folder_sync", "edit_ref_confirmed", "show_html_confirmed", "show_images_confirmed" }; @@ -122,7 +131,8 @@ public class FragmentOptions extends FragmentBase implements SharedPreferences.O // Get controls swEnabled = view.findViewById(R.id.swEnabled); - swUpdates = view.findViewById(R.id.swUpdates); + tvScheduleStart = view.findViewById(R.id.tvScheduleStart); + tvScheduleEnd = view.findViewById(R.id.tvScheduleEnd); tvConnectionType = view.findViewById(R.id.tvConnectionType); swMetered = view.findViewById(R.id.swMetered); @@ -156,6 +166,7 @@ public class FragmentOptions extends FragmentBase implements SharedPreferences.O swLight = view.findViewById(R.id.swLight); btnSound = view.findViewById(R.id.btnSound); + swUpdates = view.findViewById(R.id.swUpdates); swDebug = view.findViewById(R.id.swDebug); grpNotification = view.findViewById(R.id.grpNotification); @@ -174,6 +185,28 @@ public class FragmentOptions extends FragmentBase implements SharedPreferences.O } }); + tvScheduleStart.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Bundle args = new Bundle(); + args.putBoolean("start", true); + DialogFragment timePicker = new TimePickerFragment(); + timePicker.setArguments(args); + timePicker.show(getFragmentManager(), "timePicker"); + } + }); + + tvScheduleEnd.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Bundle args = new Bundle(); + args.putBoolean("start", false); + DialogFragment timePicker = new TimePickerFragment(); + timePicker.setArguments(args); + timePicker.show(getFragmentManager(), "timePicker"); + } + }); + swUpdates.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton compoundButton, boolean checked) { @@ -480,8 +513,9 @@ public class FragmentOptions extends FragmentBase implements SharedPreferences.O SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); swEnabled.setChecked(prefs.getBoolean("enabled", true)); - swUpdates.setChecked(prefs.getBoolean("updates", true)); - swUpdates.setVisibility(Helper.isPlayStoreInstall(getContext()) ? View.GONE : View.VISIBLE); + + tvScheduleStart.setText(formatHour(prefs.getInt("schedule_start", 0))); + tvScheduleEnd.setText(formatHour(prefs.getInt("schedule_end", 0))); swMetered.setChecked(prefs.getBoolean("metered", true)); @@ -524,11 +558,55 @@ public class FragmentOptions extends FragmentBase implements SharedPreferences.O swNotifyPreview.setChecked(prefs.getBoolean("notify_preview", true)); swLight.setChecked(prefs.getBoolean("light", false)); + swUpdates.setChecked(prefs.getBoolean("updates", true)); + swUpdates.setVisibility(Helper.isPlayStoreInstall(getContext()) ? View.GONE : View.VISIBLE); swDebug.setChecked(prefs.getBoolean("debug", false)); grpNotification.setVisibility(BuildConfig.DEBUG || Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O ? View.VISIBLE : View.GONE); } + private String formatHour(int minutes) { + Calendar cal = Calendar.getInstance(); + cal.set(Calendar.HOUR_OF_DAY, minutes / 60); + cal.set(Calendar.MINUTE, minutes % 60); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); + return SimpleDateFormat.getTimeInstance(SimpleDateFormat.SHORT).format(cal.getTime()); + } + + public static class TimePickerFragment extends DialogFragment implements TimePickerDialog.OnTimeSetListener { + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + Bundle args = getArguments(); + boolean start = args.getBoolean("start"); + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + int minutes = prefs.getInt("schedule_" + (start ? "start" : "end"), 0); + + Calendar cal = Calendar.getInstance(); + cal.set(Calendar.HOUR_OF_DAY, minutes / 60); + cal.set(Calendar.MINUTE, minutes % 60); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); + + return new TimePickerDialog(getActivity(), this, + cal.get(Calendar.HOUR_OF_DAY), + cal.get(Calendar.MINUTE), + DateFormat.is24HourFormat(getActivity())); + } + + public void onTimeSet(TimePicker view, int hour, int minute) { + Bundle args = getArguments(); + boolean start = args.getBoolean("start"); + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + prefs.edit().putInt("schedule_" + (start ? "start" : "end"), hour * 60 + minute).apply(); + + ServiceSynchronize.schedule(getContext()); + } + } + private ConnectivityManager.NetworkCallback networkCallback = new ConnectivityManager.NetworkCallback() { @Override public void onAvailable(Network network) { @@ -587,5 +665,9 @@ public class FragmentOptions extends FragmentBase implements SharedPreferences.O public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { if ("enabled".equals(key)) swEnabled.setChecked(prefs.getBoolean(key, true)); + else if ("schedule_start".equals(key)) + tvScheduleStart.setText(formatHour(prefs.getInt(key, 0))); + else if ("schedule_end".equals(key)) + tvScheduleEnd.setText(formatHour(prefs.getInt(key, 0))); } } diff --git a/app/src/main/java/eu/faircode/email/JobDaily.java b/app/src/main/java/eu/faircode/email/JobDaily.java index 4ab051d0ce..71545e3610 100644 --- a/app/src/main/java/eu/faircode/email/JobDaily.java +++ b/app/src/main/java/eu/faircode/email/JobDaily.java @@ -42,19 +42,22 @@ public class JobDaily extends JobService { private static final long CACHE_IMAGE_DURATION = 3 * 24 * 3600 * 1000L; // milliseconds private static final long KEEP_LOG_DURATION = 24 * 3600 * 1000L; // milliseconds - public static void schedule(Context context) { - Log.i("Scheduling daily job"); + public static void schedule(Context context, boolean enabled) { + JobScheduler scheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); JobInfo.Builder job = new JobInfo.Builder(Helper.JOB_DAILY, new ComponentName(context, JobDaily.class)) .setPeriodic(CLEANUP_INTERVAL) .setRequiresDeviceIdle(true); - JobScheduler scheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); - scheduler.cancel(Helper.JOB_DAILY); - if (scheduler.schedule(job.build()) == JobScheduler.RESULT_SUCCESS) - Log.i("Scheduled daily job"); - else - Log.e("Scheduling daily job failed"); + if (enabled) + if (scheduler.schedule(job.build()) == JobScheduler.RESULT_SUCCESS) + Log.i("Scheduled daily job"); + else + Log.e("Scheduling daily job failed"); + else { + Log.i("Cancelled daily job"); + scheduler.cancel(Helper.JOB_DAILY); + } } @Override diff --git a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java index 5c35655057..12daaddde0 100644 --- a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java +++ b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java @@ -163,6 +163,7 @@ public class ServiceSynchronize extends LifecycleService { static final int PI_TRASH = 6; static final int PI_IGNORED = 7; static final int PI_SNOOZED = 8; + static final int PI_SCHEDULE = 9; @Override public void onCreate() { @@ -304,6 +305,7 @@ public class ServiceSynchronize extends LifecycleService { public int onStartCommand(Intent intent, int flags, int startId) { String action = (intent == null ? null : intent.getAction()); Log.i("Service command intent=" + intent + " action=" + action); + Log.logExtras(intent); startForeground(NOTIFICATION_SYNCHRONIZE, getNotificationService(null).build()); @@ -335,6 +337,10 @@ public class ServiceSynchronize extends LifecycleService { serviceManager.service_init(); break; + case "schedule": + serviceManager.service_schedule(); + break; + case "reload": serviceManager.service_reload(intent.getStringExtra("reason")); break; @@ -3015,6 +3021,18 @@ public class ServiceSynchronize extends LifecycleService { private void service_init() { EntityLog.log(ServiceSynchronize.this, "Service init"); + + next_schedule(); + + boolean enabled = isEnabled(); + JobDaily.schedule(ServiceSynchronize.this, enabled); + if (!enabled) + stopSelf(); + } + + private void service_schedule() { + next_schedule(); + service_reload("schedule"); } private void service_reload(String reason) { @@ -3033,6 +3051,8 @@ public class ServiceSynchronize extends LifecycleService { if (started) queue_reload(false, "service destroy"); } + + JobDaily.schedule(ServiceSynchronize.this, false); } private void start() { @@ -3209,11 +3229,63 @@ public class ServiceSynchronize extends LifecycleService { state = null; } + private void next_schedule() { + Intent schedule = new Intent(ServiceSynchronize.this, ServiceSynchronize.class); + schedule.setAction("schedule"); + PendingIntent piSchedule = PendingIntent.getService( + ServiceSynchronize.this, PI_SCHEDULE, schedule, PendingIntent.FLAG_UPDATE_CURRENT); + + AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE); + am.cancel(piSchedule); + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ServiceSynchronize.this); + int minuteStart = prefs.getInt("schedule_start", 0); + int minuteEnd = prefs.getInt("schedule_end", 0); + + if (minuteEnd <= minuteStart) + minuteEnd += 24 * 60; + + Calendar calStart = Calendar.getInstance(); + calStart.set(Calendar.HOUR_OF_DAY, minuteStart / 60); + calStart.set(Calendar.MINUTE, minuteStart % 60); + calStart.set(Calendar.SECOND, 0); + calStart.set(Calendar.MILLISECOND, 0); + + Calendar calEnd = Calendar.getInstance(); + calEnd.set(Calendar.HOUR_OF_DAY, minuteEnd / 60); + calEnd.set(Calendar.MINUTE, minuteEnd % 60); + calEnd.set(Calendar.SECOND, 0); + calEnd.set(Calendar.MILLISECOND, 0); + + long now = new Date().getTime(); + if (now > calEnd.getTimeInMillis()) { + calStart.set(Calendar.DAY_OF_MONTH, calStart.get(Calendar.DAY_OF_MONTH) + 1); + calEnd.set(Calendar.DAY_OF_MONTH, calEnd.get(Calendar.DAY_OF_MONTH) + 1); + } + + long start = calStart.getTimeInMillis(); + long end = calEnd.getTimeInMillis(); + long next = (now < start ? start : end); + + EntityLog.log(ServiceSynchronize.this, "Schedule now=" + new Date(now)); + EntityLog.log(ServiceSynchronize.this, "Schedule start=" + new Date(start)); + EntityLog.log(ServiceSynchronize.this, "Schedule end=" + new Date(end)); + EntityLog.log(ServiceSynchronize.this, "Schedule next=" + new Date(next)); + + boolean enabled = (now >= start && now < end); + prefs.edit().putBoolean("enabled", enabled).apply(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) + am.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, next, piSchedule); + else + am.set(AlarmManager.RTC_WAKEUP, next, piSchedule); + } + private void queue_reload(final boolean start, final String reason) { final boolean doStop = started; final boolean doStart = (start && isEnabled() && suitableNetwork()); - EntityLog.log(ServiceSynchronize.this, "Queue reload " + + EntityLog.log(ServiceSynchronize.this, "Queue reload" + " doStop=" + doStop + " doStart=" + doStart + " queued=" + queued + " " + reason); started = doStart; @@ -3276,13 +3348,15 @@ public class ServiceSynchronize extends LifecycleService { } public static void init(Context context) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - if (prefs.getBoolean("enabled", true)) { - ContextCompat.startForegroundService(context, - new Intent(context, ServiceSynchronize.class) - .setAction("init")); - JobDaily.schedule(context); - } + ContextCompat.startForegroundService(context, + new Intent(context, ServiceSynchronize.class) + .setAction("init")); + } + + public static void schedule(Context context) { + ContextCompat.startForegroundService(context, + new Intent(context, ServiceSynchronize.class) + .setAction("schedule")); } public static void reload(Context context, String reason) { diff --git a/app/src/main/res/layout/fragment_options.xml b/app/src/main/res/layout/fragment_options.xml index 699c86bf0d..4f6174f1c5 100644 --- a/app/src/main/res/layout/fragment_options.xml +++ b/app/src/main/res/layout/fragment_options.xml @@ -57,17 +57,65 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/swEnabled" /> - + app:layout_constraintTop_toTopOf="@+id/tvScheduleStart" /> + + + + + + + + + app:layout_constraintTop_toBottomOf="@id/tvScheduleHint" /> + + Miscellaneous Synchronize - Check for updates + Schedule + Tap on a time to set a time Use metered connections Automatically download messages and attachments on a metered connection up to @@ -174,6 +175,7 @@ Show message preview in notifications Use notification light Select notification sound + Check for updates Debug mode Globally disable or enable receiving and sending of messages