mirror of https://github.com/M66B/FairEmail.git
Added account ignore schedule
This commit is contained in:
parent
bdc5c81669
commit
f457849a78
|
@ -4,6 +4,12 @@
|
|||
|
||||
### [Hulsanpes](https://en.wikipedia.org/wiki/Hulsanpes)
|
||||
|
||||
### Next version
|
||||
|
||||
* Added advanced account option to sync outside schedule
|
||||
* Small improvements and minor bug fixes
|
||||
* Updated translations
|
||||
|
||||
### 1.1879 - 2022-04-23
|
||||
|
||||
* Added warning about airplane mode enabled
|
||||
|
|
2
FAQ.md
2
FAQ.md
|
@ -2636,6 +2636,8 @@ so there is little room for performance improvements.
|
|||
In the receive settings you can enable scheduling and set a time period and the days of the week *when* messages should be *received*.
|
||||
Note that an end time equal to or earlier than the start time is considered to be 24 hours later.
|
||||
|
||||
Since version 1.1880 is is possible to exclude accounts from scheduling in the advanced account settings.
|
||||
|
||||
Automation, see below, can be used for more advanced schedules,
|
||||
like for example multiple synchronization periods per day or different synchronization periods for different days.
|
||||
|
||||
|
|
|
@ -4,6 +4,12 @@
|
|||
|
||||
### [Hulsanpes](https://en.wikipedia.org/wiki/Hulsanpes)
|
||||
|
||||
### Next version
|
||||
|
||||
* Added advanced account option to sync outside schedule
|
||||
* Small improvements and minor bug fixes
|
||||
* Updated translations
|
||||
|
||||
### 1.1879 - 2022-04-23
|
||||
|
||||
* Added warning about airplane mode enabled
|
||||
|
|
|
@ -112,6 +112,7 @@ public class FragmentAccount extends FragmentBase {
|
|||
|
||||
private Button btnAdvanced;
|
||||
private CheckBox cbSynchronize;
|
||||
private CheckBox cbIgnoreSchedule;
|
||||
private CheckBox cbOnDemand;
|
||||
private TextView tvLeave;
|
||||
private CheckBox cbPrimary;
|
||||
|
@ -220,6 +221,7 @@ public class FragmentAccount extends FragmentBase {
|
|||
|
||||
btnAdvanced = view.findViewById(R.id.btnAdvanced);
|
||||
cbSynchronize = view.findViewById(R.id.cbSynchronize);
|
||||
cbIgnoreSchedule = view.findViewById(R.id.cbIgnoreSchedule);
|
||||
cbOnDemand = view.findViewById(R.id.cbOnDemand);
|
||||
tvLeave = view.findViewById(R.id.tvLeave);
|
||||
cbPrimary = view.findViewById(R.id.cbPrimary);
|
||||
|
@ -441,6 +443,7 @@ public class FragmentAccount extends FragmentBase {
|
|||
cbSynchronize.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton compoundButton, boolean checked) {
|
||||
cbIgnoreSchedule.setEnabled(checked);
|
||||
cbOnDemand.setEnabled(checked);
|
||||
cbPrimary.setEnabled(checked);
|
||||
}
|
||||
|
@ -878,6 +881,7 @@ public class FragmentAccount extends FragmentBase {
|
|||
args.putInt("color", btnColor.getColor());
|
||||
|
||||
args.putBoolean("synchronize", cbSynchronize.isChecked());
|
||||
args.putBoolean("ignore_schedule", cbIgnoreSchedule.isChecked());
|
||||
args.putBoolean("ondemand", cbOnDemand.isChecked());
|
||||
args.putBoolean("primary", cbPrimary.isChecked());
|
||||
args.putBoolean("notify", cbNotify.isChecked());
|
||||
|
@ -948,6 +952,7 @@ public class FragmentAccount extends FragmentBase {
|
|||
Integer color = args.getInt("color");
|
||||
|
||||
boolean synchronize = args.getBoolean("synchronize");
|
||||
boolean ignore_schedule = args.getBoolean("ignore_schedule");
|
||||
boolean ondemand = args.getBoolean("ondemand");
|
||||
boolean primary = args.getBoolean("primary");
|
||||
boolean notify = args.getBoolean("notify");
|
||||
|
@ -1043,6 +1048,8 @@ public class FragmentAccount extends FragmentBase {
|
|||
return true;
|
||||
if (!Objects.equals(account.synchronize, synchronize))
|
||||
return true;
|
||||
if (ignore_schedule != jconditions.optBoolean("ignore_schedule"))
|
||||
return true;
|
||||
if (!Objects.equals(account.ondemand, ondemand))
|
||||
return true;
|
||||
if (!Objects.equals(account.primary, account.synchronize && primary))
|
||||
|
@ -1185,6 +1192,7 @@ public class FragmentAccount extends FragmentBase {
|
|||
account.color = color;
|
||||
|
||||
account.synchronize = synchronize;
|
||||
jconditions.put("ignore_schedule", ignore_schedule);
|
||||
account.ondemand = ondemand;
|
||||
account.primary = (account.synchronize && primary);
|
||||
account.notify = notify;
|
||||
|
@ -1486,6 +1494,14 @@ public class FragmentAccount extends FragmentBase {
|
|||
spProvider.setAdapter(aaProvider);
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
JSONObject jcondition = new JSONObject();
|
||||
try {
|
||||
if (account != null && account.conditions != null)
|
||||
jcondition = new JSONObject(account.conditions);
|
||||
} catch (Throwable ex) {
|
||||
Log.e(ex);
|
||||
}
|
||||
|
||||
if (account != null) {
|
||||
boolean found = false;
|
||||
for (int pos = 2; pos < providers.size(); pos++) {
|
||||
|
@ -1544,6 +1560,7 @@ public class FragmentAccount extends FragmentBase {
|
|||
cbNotify.setEnabled(pro);
|
||||
|
||||
cbSynchronize.setChecked(account == null ? true : account.synchronize);
|
||||
cbIgnoreSchedule.setChecked(jcondition.optBoolean("ignore_schedule"));
|
||||
cbOnDemand.setChecked(account == null ? false : account.ondemand);
|
||||
cbPrimary.setChecked(account == null ? false : account.primary);
|
||||
cbBrowse.setChecked(account == null ? true : account.browse);
|
||||
|
@ -1551,14 +1568,6 @@ public class FragmentAccount extends FragmentBase {
|
|||
etInterval.setText(account == null ? "" : Long.toString(account.poll_interval));
|
||||
cbPartialFetch.setChecked(account == null ? true : account.partial_fetch);
|
||||
cbIgnoreSize.setChecked(account == null ? false : account.ignore_size);
|
||||
|
||||
JSONObject jcondition = new JSONObject();
|
||||
try {
|
||||
if (account != null && account.conditions != null)
|
||||
jcondition = new JSONObject(account.conditions);
|
||||
} catch (Throwable ex) {
|
||||
Log.e(ex);
|
||||
}
|
||||
cbUnmetered.setChecked(jcondition.optBoolean("unmetered"));
|
||||
|
||||
if (account != null && account.use_date)
|
||||
|
@ -1686,6 +1695,7 @@ public class FragmentAccount extends FragmentBase {
|
|||
});
|
||||
}
|
||||
|
||||
cbIgnoreSchedule.setEnabled(cbSynchronize.isChecked());
|
||||
cbOnDemand.setEnabled(cbSynchronize.isChecked());
|
||||
cbPrimary.setEnabled(cbSynchronize.isChecked());
|
||||
|
||||
|
|
|
@ -86,6 +86,7 @@ public class FragmentPop extends FragmentBase {
|
|||
private TextView tvColorPro;
|
||||
|
||||
private CheckBox cbSynchronize;
|
||||
private CheckBox cbIgnoreSchedule;
|
||||
private CheckBox cbOnDemand;
|
||||
private CheckBox cbPrimary;
|
||||
private CheckBox cbNotify;
|
||||
|
@ -152,6 +153,7 @@ public class FragmentPop extends FragmentBase {
|
|||
tvColorPro = view.findViewById(R.id.tvColorPro);
|
||||
|
||||
cbSynchronize = view.findViewById(R.id.cbSynchronize);
|
||||
cbIgnoreSchedule = view.findViewById(R.id.cbIgnoreSchedule);
|
||||
cbOnDemand = view.findViewById(R.id.cbOnDemand);
|
||||
cbPrimary = view.findViewById(R.id.cbPrimary);
|
||||
cbNotify = view.findViewById(R.id.cbNotify);
|
||||
|
@ -245,6 +247,7 @@ public class FragmentPop extends FragmentBase {
|
|||
cbSynchronize.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton compoundButton, boolean checked) {
|
||||
cbIgnoreSchedule.setEnabled(checked);
|
||||
cbOnDemand.setEnabled(checked);
|
||||
cbPrimary.setEnabled(checked);
|
||||
}
|
||||
|
@ -320,6 +323,7 @@ public class FragmentPop extends FragmentBase {
|
|||
args.putInt("color", btnColor.getColor());
|
||||
|
||||
args.putBoolean("synchronize", cbSynchronize.isChecked());
|
||||
args.putBoolean("ignore_schedule", cbIgnoreSchedule.isChecked());
|
||||
args.putBoolean("ondemand", cbOnDemand.isChecked());
|
||||
args.putBoolean("primary", cbPrimary.isChecked());
|
||||
args.putBoolean("notify", cbNotify.isChecked());
|
||||
|
@ -372,6 +376,7 @@ public class FragmentPop extends FragmentBase {
|
|||
Integer color = args.getInt("color");
|
||||
|
||||
boolean synchronize = args.getBoolean("synchronize");
|
||||
boolean ignore_schedule = args.getBoolean("ignore_schedule");
|
||||
boolean ondemand = args.getBoolean("ondemand");
|
||||
boolean primary = args.getBoolean("primary");
|
||||
boolean notify = args.getBoolean("notify");
|
||||
|
@ -452,6 +457,8 @@ public class FragmentPop extends FragmentBase {
|
|||
return true;
|
||||
if (!Objects.equals(account.synchronize, synchronize))
|
||||
return true;
|
||||
if (ignore_schedule != jconditions.optBoolean("ignore_schedule"))
|
||||
return true;
|
||||
if (!Objects.equals(account.ondemand, ondemand))
|
||||
return true;
|
||||
if (!Objects.equals(account.primary, account.synchronize && primary))
|
||||
|
@ -539,6 +546,7 @@ public class FragmentPop extends FragmentBase {
|
|||
account.color = color;
|
||||
|
||||
account.synchronize = synchronize;
|
||||
jconditions.put("ignore_schedule", ignore_schedule);
|
||||
account.ondemand = ondemand;
|
||||
account.primary = (account.synchronize && primary);
|
||||
account.notify = notify;
|
||||
|
@ -695,6 +703,14 @@ public class FragmentPop extends FragmentBase {
|
|||
@Override
|
||||
protected void onExecuted(Bundle args, final EntityAccount account) {
|
||||
if (savedInstanceState == null) {
|
||||
JSONObject jcondition = new JSONObject();
|
||||
try {
|
||||
if (account != null && account.conditions != null)
|
||||
jcondition = new JSONObject(account.conditions);
|
||||
} catch (Throwable ex) {
|
||||
Log.e(ex);
|
||||
}
|
||||
|
||||
etHost.setText(account == null ? null : account.host);
|
||||
etPort.setText(account == null ? null : Long.toString(account.port));
|
||||
|
||||
|
@ -715,6 +731,7 @@ public class FragmentPop extends FragmentBase {
|
|||
btnColor.setColor(account == null ? null : account.color);
|
||||
|
||||
cbSynchronize.setChecked(account == null ? true : account.synchronize);
|
||||
cbIgnoreSchedule.setChecked(jcondition.optBoolean("ignore_schedule"));
|
||||
cbOnDemand.setChecked(account == null ? false : account.ondemand);
|
||||
cbPrimary.setChecked(account == null ? false : account.primary);
|
||||
|
||||
|
@ -735,16 +752,7 @@ public class FragmentPop extends FragmentBase {
|
|||
? EntityAccount.DEFAULT_MAX_MESSAGES : account.max_messages));
|
||||
|
||||
etInterval.setText(account == null ? "" : Long.toString(account.poll_interval));
|
||||
|
||||
JSONObject jcondition = new JSONObject();
|
||||
try {
|
||||
if (account != null && account.conditions != null)
|
||||
jcondition = new JSONObject(account.conditions);
|
||||
} catch (Throwable ex) {
|
||||
Log.e(ex);
|
||||
}
|
||||
cbUnmetered.setChecked(jcondition.optBoolean("unmetered"));
|
||||
|
||||
cbIdentity.setChecked(account == null);
|
||||
|
||||
List<EntityFolder> folders = getSwipeActions();
|
||||
|
@ -793,6 +801,7 @@ public class FragmentPop extends FragmentBase {
|
|||
tilPassword.setEnabled(false);
|
||||
}
|
||||
|
||||
cbIgnoreSchedule.setEnabled(cbSynchronize.isChecked());
|
||||
cbOnDemand.setEnabled(cbSynchronize.isChecked());
|
||||
cbPrimary.setEnabled(cbSynchronize.isChecked());
|
||||
|
||||
|
|
|
@ -59,6 +59,8 @@ import com.sun.mail.imap.IMAPStore;
|
|||
import com.sun.mail.imap.protocol.IMAPProtocol;
|
||||
import com.sun.mail.imap.protocol.IMAPResponse;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.text.DateFormat;
|
||||
|
@ -1221,17 +1223,32 @@ public class ServiceSynchronize extends ServiceBase implements SharedPreferences
|
|||
@Override
|
||||
public void delegate() {
|
||||
try {
|
||||
long now = new Date().getTime();
|
||||
long[] schedule = ServiceSynchronize.getSchedule(ServiceSynchronize.this);
|
||||
boolean scheduled = (schedule == null || (now >= schedule[0] && now < schedule[1]));
|
||||
|
||||
boolean work = false;
|
||||
DB db = DB.getInstance(ServiceSynchronize.this);
|
||||
try {
|
||||
db.beginTransaction();
|
||||
|
||||
List<EntityAccount> accounts = db.account().getPollAccounts(null);
|
||||
for (EntityAccount account : accounts) {
|
||||
List<EntityFolder> folders = db.folder().getSynchronizingFolders(account.id);
|
||||
if (folders.size() > 0)
|
||||
Collections.sort(folders, folders.get(0).getComparator(ServiceSynchronize.this));
|
||||
for (EntityFolder folder : folders)
|
||||
EntityOperation.poll(ServiceSynchronize.this, folder.id);
|
||||
JSONObject jcondition = new JSONObject();
|
||||
try {
|
||||
jcondition = new JSONObject(account.conditions);
|
||||
} catch (Throwable ex) {
|
||||
Log.e(ex);
|
||||
}
|
||||
|
||||
if (scheduled || jcondition.optBoolean("ignore_schedule")) {
|
||||
work = true;
|
||||
List<EntityFolder> folders = db.folder().getSynchronizingFolders(account.id);
|
||||
if (folders.size() > 0)
|
||||
Collections.sort(folders, folders.get(0).getComparator(ServiceSynchronize.this));
|
||||
for (EntityFolder folder : folders)
|
||||
EntityOperation.poll(ServiceSynchronize.this, folder.id);
|
||||
}
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
|
@ -1239,10 +1256,7 @@ public class ServiceSynchronize extends ServiceBase implements SharedPreferences
|
|||
db.endTransaction();
|
||||
}
|
||||
|
||||
long now = new Date().getTime();
|
||||
long[] schedule = ServiceSynchronize.getSchedule(ServiceSynchronize.this);
|
||||
boolean scheduled = (schedule == null || (now >= schedule[0] && now < schedule[1]));
|
||||
schedule(ServiceSynchronize.this, scheduled, true, null);
|
||||
schedule(ServiceSynchronize.this, work, true, null);
|
||||
|
||||
// Prevent service stop
|
||||
eval(ServiceSynchronize.this, "poll");
|
||||
|
@ -2826,7 +2840,8 @@ public class ServiceSynchronize extends ServiceBase implements SharedPreferences
|
|||
List<TupleAccountNetworkState> result = new ArrayList<>();
|
||||
for (TupleAccountState accountState : accountStates)
|
||||
result.add(new TupleAccountNetworkState(
|
||||
enabled && (pollInterval == 0 || accountState.isExempted(ServiceSynchronize.this)) && scheduled,
|
||||
enabled && (pollInterval == 0 || accountState.isExempted(ServiceSynchronize.this)),
|
||||
scheduled,
|
||||
command,
|
||||
networkState,
|
||||
accountState));
|
||||
|
@ -2899,15 +2914,12 @@ public class ServiceSynchronize extends ServiceBase implements SharedPreferences
|
|||
AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
|
||||
am.cancel(pi);
|
||||
|
||||
boolean scheduled;
|
||||
Long at = null;
|
||||
long[] schedule = getSchedule(context);
|
||||
if (schedule == null)
|
||||
scheduled = true;
|
||||
else {
|
||||
long now = new Date().getTime();
|
||||
long now = new Date().getTime();
|
||||
long[] schedule = ServiceSynchronize.getSchedule(context);
|
||||
boolean scheduled = (schedule == null || (now >= schedule[0] && now < schedule[1]));
|
||||
|
||||
if (schedule != null) {
|
||||
long next = (now < schedule[0] ? schedule[0] : schedule[1]);
|
||||
scheduled = (now >= schedule[0] && now < schedule[1]);
|
||||
|
||||
Log.i("Schedule now=" + new Date(now));
|
||||
Log.i("Schedule start=" + new Date(schedule[0]));
|
||||
|
@ -2916,14 +2928,37 @@ public class ServiceSynchronize extends ServiceBase implements SharedPreferences
|
|||
Log.i("Schedule scheduled=" + scheduled);
|
||||
|
||||
AlarmManagerCompatEx.setAndAllowWhileIdle(context, am, AlarmManager.RTC_WAKEUP, next, pi);
|
||||
|
||||
if (scheduled && polled) {
|
||||
at = now + 30 * 1000L;
|
||||
Log.i("Sync at schedule start=" + new Date(at));
|
||||
}
|
||||
}
|
||||
|
||||
schedule(context, scheduled, polled, at);
|
||||
executor.submit(new RunnableEx("schedule") {
|
||||
@Override
|
||||
protected void delegate() {
|
||||
boolean work = false;
|
||||
DB db = DB.getInstance(context);
|
||||
List<EntityAccount> accounts = db.account().getPollAccounts(null);
|
||||
for (EntityAccount account : accounts) {
|
||||
JSONObject jcondition = new JSONObject();
|
||||
try {
|
||||
jcondition = new JSONObject(account.conditions);
|
||||
} catch (Throwable ex) {
|
||||
Log.e(ex);
|
||||
}
|
||||
|
||||
if (scheduled || jcondition.optBoolean("ignore_schedule")) {
|
||||
work = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Long at = null;
|
||||
if (scheduled && polled) {
|
||||
at = now + 30 * 1000L;
|
||||
Log.i("Sync at schedule start=" + new Date(at));
|
||||
}
|
||||
|
||||
schedule(context, work, polled, at);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void schedule(Context context, boolean scheduled, boolean polled, Long at) {
|
||||
|
|
|
@ -39,6 +39,7 @@ public class TupleAccountNetworkState {
|
|||
|
||||
public TupleAccountNetworkState(
|
||||
boolean enabled,
|
||||
boolean scheduled,
|
||||
@NonNull Bundle command,
|
||||
@NonNull ConnectionHelper.NetworkState networkState,
|
||||
@NonNull TupleAccountState accountState) {
|
||||
|
@ -54,6 +55,9 @@ public class TupleAccountNetworkState {
|
|||
} catch (Throwable ex) {
|
||||
Log.e(ex);
|
||||
}
|
||||
|
||||
if (!scheduled && !jconditions.optBoolean("ignore_schedule"))
|
||||
this.enabled = false;
|
||||
}
|
||||
|
||||
public boolean canRun() {
|
||||
|
|
|
@ -477,6 +477,15 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/btnAdvanced" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/cbIgnoreSchedule"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@string/title_ignore_schedule"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/cbSynchronize" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/cbOnDemand"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -484,7 +493,7 @@
|
|||
android:layout_marginTop="12dp"
|
||||
android:text="@string/title_account_ondemand"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/cbSynchronize" />
|
||||
app:layout_constraintTop_toBottomOf="@id/cbIgnoreSchedule" />
|
||||
|
||||
<eu.faircode.email.FixedTextView
|
||||
android:id="@+id/tvLeave"
|
||||
|
@ -1057,7 +1066,7 @@
|
|||
android:layout_height="0dp"
|
||||
app:constraint_referenced_ids="
|
||||
cbNotify,tvNotifyPro,
|
||||
cbSynchronize,cbOnDemand,tvLeave,cbPrimary,
|
||||
cbSynchronize,cbIgnoreSchedule,cbOnDemand,tvLeave,cbPrimary,
|
||||
cbBrowse,tvBrowseHint,
|
||||
cbAutoSeen,
|
||||
tvInterval,etInterval,tvIntervalRemark,
|
||||
|
|
|
@ -337,6 +337,15 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvColorPro" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/cbIgnoreSchedule"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@string/title_ignore_schedule"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/cbSynchronize" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/cbOnDemand"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -344,7 +353,7 @@
|
|||
android:layout_marginTop="12dp"
|
||||
android:text="@string/title_account_ondemand"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/cbSynchronize" />
|
||||
app:layout_constraintTop_toBottomOf="@id/cbIgnoreSchedule" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/cbPrimary"
|
||||
|
|
|
@ -951,6 +951,7 @@
|
|||
<string name="title_date_header">Use \'Date\' header (sent time)</string>
|
||||
<string name="title_date_remark">Changes will be applied to new messages only</string>
|
||||
<string name="title_unmetered_only">Connect only via unmetered networks</string>
|
||||
<string name="title_ignore_schedule">Sync outside the schedule</string>
|
||||
<string name="title_related_identity">Add related identity (SMTP server)</string>
|
||||
<string name="title_check">Check</string>
|
||||
<string name="title_trust">Trust server certificate with fingerprint %1$s</string>
|
||||
|
|
|
@ -4,6 +4,12 @@
|
|||
|
||||
### [Hulsanpes](https://en.wikipedia.org/wiki/Hulsanpes)
|
||||
|
||||
### Next version
|
||||
|
||||
* Added advanced account option to sync outside schedule
|
||||
* Small improvements and minor bug fixes
|
||||
* Updated translations
|
||||
|
||||
### 1.1879 - 2022-04-23
|
||||
|
||||
* Added warning about airplane mode enabled
|
||||
|
|
Loading…
Reference in New Issue