diff --git a/FAQ.md b/FAQ.md index 35bf4d1f05..1ff297152a 100644 --- a/FAQ.md +++ b/FAQ.md @@ -75,6 +75,7 @@ Note that your contacts could unknowingly send malicious messages if they got in * foreground service (FOREGROUND_SERVICE): to run a foreground service on Android 9 Pie and later, see also the next question * prevent device from sleeping (WAKE_LOCK): to keep the device awake while synchronizing messages * Optional: read your contacts (READ_CONTACTS): to autocomplete addresses and to show photos +* USE_CREDENTIALS: needed to select accounts on Android version 5.1 Lollipop and before (not used on later Android versions) * Optional: find accounts on the device (GET_ACCOUNTS): to use [OAuth](https://en.wikipedia.org/wiki/OAuth) instead of passwords diff --git a/README.md b/README.md index ab838d3f27..e6e7a74d57 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,6 @@ See also [this FAQ](https://github.com/M66B/open-source-email/blob/master/FAQ.md * [IMAP IDLE](https://en.wikipedia.org/wiki/IMAP_IDLE) (push messages) supported * Built with latest development tools and libraries -* Android 6 Marshmallow or later required ## Screenshots diff --git a/app/build.gradle b/app/build.gradle index 90e8762a2e..541bca1c52 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,7 +4,7 @@ android { compileSdkVersion 28 defaultConfig { applicationId "eu.faircode.email" - minSdkVersion 23 + minSdkVersion 21 targetSdkVersion 28 versionCode 210 versionName "1.210" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6841df0307..7f6e705d12 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,6 +7,9 @@ + diff --git a/app/src/main/java/eu/faircode/email/ActivitySearch.java b/app/src/main/java/eu/faircode/email/ActivitySearch.java index 66d090bbb4..fad02419b7 100644 --- a/app/src/main/java/eu/faircode/email/ActivitySearch.java +++ b/app/src/main/java/eu/faircode/email/ActivitySearch.java @@ -1,11 +1,15 @@ package eu.faircode.email; import android.content.Intent; +import android.os.Build; import android.os.Bundle; +import androidx.annotation.RequiresApi; + public class ActivitySearch extends ActivityBase { @Override + @RequiresApi(api = Build.VERSION_CODES.M) protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); diff --git a/app/src/main/java/eu/faircode/email/ActivityView.java b/app/src/main/java/eu/faircode/email/ActivityView.java index 6725234100..560b2cf311 100644 --- a/app/src/main/java/eu/faircode/email/ActivityView.java +++ b/app/src/main/java/eu/faircode/email/ActivityView.java @@ -922,7 +922,7 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB os = new BufferedOutputStream(new FileOutputStream(file)); int size = 0; - ConnectivityManager cm = context.getSystemService(ConnectivityManager.class); + ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo ani = cm.getActiveNetworkInfo(); size += write(os, "active=" + ani + "\r\n\r\n"); diff --git a/app/src/main/java/eu/faircode/email/AdapterMessage.java b/app/src/main/java/eu/faircode/email/AdapterMessage.java index c5d55729ea..388b0eced4 100644 --- a/app/src/main/java/eu/faircode/email/AdapterMessage.java +++ b/app/src/main/java/eu/faircode/email/AdapterMessage.java @@ -1440,7 +1440,7 @@ public class AdapterMessage extends RecyclerView.Adapter= android.os.Build.VERSION_CODES.O) { - NotificationManager nm = getSystemService(NotificationManager.class); + NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); NotificationChannel service = new NotificationChannel( "service", diff --git a/app/src/main/java/eu/faircode/email/FragmentAccount.java b/app/src/main/java/eu/faircode/email/FragmentAccount.java index c4f7ea86c3..b5c116da77 100644 --- a/app/src/main/java/eu/faircode/email/FragmentAccount.java +++ b/app/src/main/java/eu/faircode/email/FragmentAccount.java @@ -808,7 +808,7 @@ public class FragmentAccount extends FragmentEx { ServiceSynchronize.reload(getContext(), "save account"); if (!synchronize) { - NotificationManager nm = getContext().getSystemService(NotificationManager.class); + NotificationManager nm = (NotificationManager) getContext().getSystemService(Context.NOTIFICATION_SERVICE); nm.cancel("receive", account.id.intValue()); } @@ -1138,6 +1138,7 @@ public class FragmentAccount extends FragmentEx { null, null, new String[]{provider.type}, + false, null, null, null, diff --git a/app/src/main/java/eu/faircode/email/FragmentCompose.java b/app/src/main/java/eu/faircode/email/FragmentCompose.java index 63f97e4dbe..903e2a9fb0 100644 --- a/app/src/main/java/eu/faircode/email/FragmentCompose.java +++ b/app/src/main/java/eu/faircode/email/FragmentCompose.java @@ -528,7 +528,7 @@ public class FragmentCompose extends FragmentEx { break; case R.id.menu_link: Uri uri = null; - ClipboardManager cbm = getContext().getSystemService(ClipboardManager.class); + ClipboardManager cbm = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE); if (cbm.hasPrimaryClip()) { String link = cbm.getPrimaryClip().getItemAt(0).coerceToText(getContext()).toString(); uri = Uri.parse(link); diff --git a/app/src/main/java/eu/faircode/email/FragmentEx.java b/app/src/main/java/eu/faircode/email/FragmentEx.java index 3e3478671e..aa4b1c5981 100644 --- a/app/src/main/java/eu/faircode/email/FragmentEx.java +++ b/app/src/main/java/eu/faircode/email/FragmentEx.java @@ -19,6 +19,7 @@ package eu.faircode.email; Copyright 2018 by Marcel Bokhorst (M66B) */ +import android.content.Context; import android.content.res.Configuration; import android.os.Bundle; import android.util.Log; @@ -100,7 +101,7 @@ public class FragmentEx extends Fragment { public void onDetach() { super.onDetach(); - InputMethodManager im = getContext().getSystemService(InputMethodManager.class); + InputMethodManager im = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); View focused = getActivity().getCurrentFocus(); if (focused != null) im.hideSoftInputFromWindow(focused.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); diff --git a/app/src/main/java/eu/faircode/email/FragmentOptions.java b/app/src/main/java/eu/faircode/email/FragmentOptions.java index 7b33681720..7fc198d0e0 100644 --- a/app/src/main/java/eu/faircode/email/FragmentOptions.java +++ b/app/src/main/java/eu/faircode/email/FragmentOptions.java @@ -171,7 +171,7 @@ public class FragmentOptions extends FragmentEx implements SharedPreferences.OnS protected Void onLoad(Context context, Bundle args) { DB db = DB.getInstance(context); - ConnectivityManager cm = context.getSystemService(ConnectivityManager.class); + ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); boolean metered = (cm == null || cm.isActiveNetworkMetered()); for (Long id : db.message().getMessageWithoutPreview()) { diff --git a/app/src/main/java/eu/faircode/email/FragmentSetup.java b/app/src/main/java/eu/faircode/email/FragmentSetup.java index cf8964ffee..5f16816cc2 100644 --- a/app/src/main/java/eu/faircode/email/FragmentSetup.java +++ b/app/src/main/java/eu/faircode/email/FragmentSetup.java @@ -371,14 +371,16 @@ public class FragmentSetup extends FragmentEx { public void onResume() { super.onResume(); - PowerManager pm = getContext().getSystemService(PowerManager.class); - boolean ignoring = pm.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID); + PowerManager pm = (PowerManager) getContext().getSystemService(Context.POWER_SERVICE); + boolean ignoring = true; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) + ignoring = pm.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID); btnDoze.setEnabled(!ignoring); tvDozeDone.setText(ignoring ? R.string.title_setup_done : R.string.title_setup_to_do); tvDozeDone.setCompoundDrawablesWithIntrinsicBounds(ignoring ? check : null, null, null, null); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - ConnectivityManager cm = getContext().getSystemService(ConnectivityManager.class); + ConnectivityManager cm = (ConnectivityManager) getContext().getSystemService(Context.CONNECTIVITY_SERVICE); boolean saving = (cm.getRestrictBackgroundStatus() == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED); btnData.setVisibility(saving ? View.VISIBLE : View.GONE); } diff --git a/app/src/main/java/eu/faircode/email/Helper.java b/app/src/main/java/eu/faircode/email/Helper.java index d90d93b03e..1b5541b56c 100644 --- a/app/src/main/java/eu/faircode/email/Helper.java +++ b/app/src/main/java/eu/faircode/email/Helper.java @@ -305,7 +305,10 @@ public class Helper { } static Boolean isMetered(Context context) { - ConnectivityManager cm = context.getSystemService(ConnectivityManager.class); + ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.M) + return cm.isActiveNetworkMetered(); Network active = cm.getActiveNetwork(); if (active == null) { @@ -325,7 +328,6 @@ public class Helper { Log.i(Helper.TAG, "isMetered: active not connected"); return null; } - NetworkCapabilities caps = cm.getNetworkCapabilities(active); if (caps == null) { Log.i(Helper.TAG, "isMetered: active no caps"); @@ -393,7 +395,8 @@ public class Helper { return true; } - static void connect(Context context, IMAPStore istore, EntityAccount account) throws MessagingException { + static void connect(Context context, IMAPStore istore, EntityAccount account) throws + MessagingException { try { istore.connect(account.host, account.port, account.user, account.password); } catch (AuthenticationFailedException ex) { @@ -467,18 +470,20 @@ public class Helper { sb.append(String.format("Id: %s\r\n", Build.ID)); sb.append("\r\n"); - PowerManager pm = context.getSystemService(PowerManager.class); - boolean ignoring = pm.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID); + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + boolean ignoring = true; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) + ignoring = pm.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID); sb.append(String.format("Battery optimizations: %b\r\n", !ignoring)); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - UsageStatsManager usm = context.getSystemService(UsageStatsManager.class); + UsageStatsManager usm = (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE); int bucket = usm.getAppStandbyBucket(); sb.append(String.format("Standby bucket: %d\r\n", bucket)); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - ConnectivityManager cm = context.getSystemService(ConnectivityManager.class); + ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); boolean saving = (cm.getRestrictBackgroundStatus() == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED); sb.append(String.format("Data saving: %b\r\n", saving)); } diff --git a/app/src/main/java/eu/faircode/email/JobDaily.java b/app/src/main/java/eu/faircode/email/JobDaily.java index a50847389a..d275dccf6d 100644 --- a/app/src/main/java/eu/faircode/email/JobDaily.java +++ b/app/src/main/java/eu/faircode/email/JobDaily.java @@ -46,7 +46,7 @@ public class JobDaily extends JobService { .setPeriodic(CLEANUP_INTERVAL) .setRequiresDeviceIdle(true); - JobScheduler scheduler = context.getSystemService(JobScheduler.class); + JobScheduler scheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); scheduler.cancel(Helper.JOB_DAILY); if (scheduler.schedule(job.build()) == JobScheduler.RESULT_SUCCESS) Log.i(Helper.TAG, "Scheduled daily job"); diff --git a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java index 43d8100fda..6834440e88 100644 --- a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java +++ b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java @@ -31,6 +31,7 @@ import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.database.Cursor; +import android.graphics.BitmapFactory; import android.graphics.Color; import android.graphics.drawable.Icon; import android.media.RingtoneManager; @@ -65,6 +66,7 @@ import org.json.JSONException; import org.jsoup.Jsoup; import java.io.IOException; +import java.io.InputStream; import java.net.SocketException; import java.net.SocketTimeoutException; import java.net.UnknownHostException; @@ -172,7 +174,7 @@ public class ServiceSynchronize extends LifecycleService { db.account().liveStats().observe(this, new Observer() { @Override public void onChanged(@Nullable TupleAccountStats stats) { - NotificationManager nm = getSystemService(NotificationManager.class); + NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); nm.notify(NOTIFICATION_SYNCHRONIZE, getNotificationService(stats).build()); } }); @@ -186,7 +188,7 @@ public class ServiceSynchronize extends LifecycleService { @Override public void run() { try { - NotificationManager nm = getSystemService(NotificationManager.class); + NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); Widget.update(ServiceSynchronize.this, messages.size()); @@ -264,7 +266,7 @@ public class ServiceSynchronize extends LifecycleService { stopForeground(true); - NotificationManager nm = getSystemService(NotificationManager.class); + NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); nm.cancel(NOTIFICATION_SYNCHRONIZE); super.onDestroy(); @@ -553,18 +555,19 @@ public class ServiceSynchronize extends LifecycleService { trash.setAction("trash:" + message.id); PendingIntent piTrash = PendingIntent.getService(this, PI_TRASH, trash, PendingIntent.FLAG_UPDATE_CURRENT); + Notification.Action.Builder actionSeen = new Notification.Action.Builder( - Icon.createWithResource(this, R.drawable.baseline_visibility_24), + R.drawable.baseline_visibility_24, getString(R.string.title_action_seen), piSeen); Notification.Action.Builder actionArchive = new Notification.Action.Builder( - Icon.createWithResource(this, R.drawable.baseline_archive_24), + R.drawable.baseline_archive_24, getString(R.string.title_action_archive), piArchive); Notification.Action.Builder actionTrash = new Notification.Action.Builder( - Icon.createWithResource(this, R.drawable.baseline_delete_24), + R.drawable.baseline_delete_24, getString(R.string.title_action_trash), piTrash); @@ -627,7 +630,12 @@ public class ServiceSynchronize extends LifecycleService { Uri photo = Uri.withAppendedPath( ContactsContract.Contacts.CONTENT_URI, cursor.getLong(0) + "/photo"); - mbuilder.setLargeIcon(Icon.createWithContentUri(photo)); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + InputStream is = ContactsContract.Contacts.openContactPhotoInputStream( + getContentResolver(), photo); + mbuilder.setLargeIcon(BitmapFactory.decodeStream(is)); + } else + mbuilder.setLargeIcon(Icon.createWithContentUri(photo)); } } catch (SecurityException ex) { Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex)); @@ -728,7 +736,7 @@ public class ServiceSynchronize extends LifecycleService { EntityLog.log(this, title + " " + Helper.formatThrowable(ex)); if ((ex instanceof SendFailedException) || (ex instanceof AlertException)) { - NotificationManager nm = getSystemService(NotificationManager.class); + NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); nm.notify(tag, 1, getNotificationError(title, ex).build()); } @@ -748,13 +756,13 @@ public class ServiceSynchronize extends LifecycleService { !(ex instanceof MessagingException && ex.getCause() instanceof SocketTimeoutException) && !(ex instanceof MessagingException && ex.getCause() instanceof SSLException) && !(ex instanceof MessagingException && "connection failure".equals(ex.getMessage()))) { - NotificationManager nm = getSystemService(NotificationManager.class); + NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); nm.notify(tag, 1, getNotificationError(title, ex).build()); } } private void monitorAccount(final EntityAccount account, final ServiceState state) throws NoSuchProviderException { - final PowerManager pm = getSystemService(PowerManager.class); + final PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); final PowerManager.WakeLock wlAccount = pm.newWakeLock( PowerManager.PARTIAL_WAKE_LOCK, BuildConfig.APPLICATION_ID + ":account." + account.id); try { @@ -880,7 +888,7 @@ public class ServiceSynchronize extends LifecycleService { long delayed = now - account.last_connected; if (delayed > ACCOUNT_ERROR_AFTER * 60 * 1000L) { Log.i(Helper.TAG, "Reporting sync error after=" + delayed); - NotificationManager nm = getSystemService(NotificationManager.class); + NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); nm.notify("receive", account.id.intValue(), getNotificationError(account.name, account.last_connected, ex, false).build()); } @@ -894,7 +902,7 @@ public class ServiceSynchronize extends LifecycleService { db.account().setAccountState(account.id, "connected"); - NotificationManager nm = getSystemService(NotificationManager.class); + NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); nm.cancel("receive", account.id.intValue()); EntityLog.log(this, account.name + " connected"); @@ -1222,7 +1230,7 @@ public class ServiceSynchronize extends LifecycleService { registerReceiver(alarm, new IntentFilter(id)); // Keep alive - AlarmManager am = getSystemService(AlarmManager.class); + AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE); try { while (state.running()) { if (!istore.isConnected()) @@ -1245,10 +1253,16 @@ public class ServiceSynchronize extends LifecycleService { // Schedule keep alive alarm EntityLog.log(this, account.name + " wait=" + account.poll_interval); - am.setAndAllowWhileIdle( - AlarmManager.RTC_WAKEUP, - System.currentTimeMillis() + account.poll_interval * 60 * 1000L, - pi); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) + am.set( + AlarmManager.RTC_WAKEUP, + System.currentTimeMillis() + account.poll_interval * 60 * 1000L, + pi); + else + am.setAndAllowWhileIdle( + AlarmManager.RTC_WAKEUP, + System.currentTimeMillis() + account.poll_interval * 60 * 1000L, + pi); try { wlAccount.release(); @@ -1327,12 +1341,18 @@ public class ServiceSynchronize extends LifecycleService { PendingIntent pi = PendingIntent.getBroadcast(ServiceSynchronize.this, 0, new Intent(id), 0); registerReceiver(alarm, new IntentFilter(id)); - AlarmManager am = getSystemService(AlarmManager.class); + AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE); try { - am.setAndAllowWhileIdle( - AlarmManager.RTC_WAKEUP, - System.currentTimeMillis() + CONNECT_BACKOFF_AlARM * 60 * 1000L, - pi); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) + am.set( + AlarmManager.RTC_WAKEUP, + System.currentTimeMillis() + CONNECT_BACKOFF_AlARM * 60 * 1000L, + pi); + else + am.setAndAllowWhileIdle( + AlarmManager.RTC_WAKEUP, + System.currentTimeMillis() + CONNECT_BACKOFF_AlARM * 60 * 1000L, + pi); try { wlAccount.release(); @@ -1705,7 +1725,7 @@ public class ServiceSynchronize extends LifecycleService { db.identity().setIdentityState(ident.id, "connected"); db.identity().setIdentityError(ident.id, null); - NotificationManager nm = getSystemService(NotificationManager.class); + NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); nm.cancel("send", message.identity.intValue()); // Send message @@ -1779,7 +1799,7 @@ public class ServiceSynchronize extends LifecycleService { long delayed = now - message.last_attempt; if (delayed > IDENTITY_ERROR_AFTER * 60 * 1000L) { Log.i(Helper.TAG, "Reporting send error after=" + delayed); - NotificationManager nm = getSystemService(NotificationManager.class); + NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); nm.notify("send", message.identity.intValue(), getNotificationError(ident.name, ex).build()); } @@ -2408,7 +2428,7 @@ public class ServiceSynchronize extends LifecycleService { @Override public void onAvailable(Network network) { try { - ConnectivityManager cm = getSystemService(ConnectivityManager.class); + ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); EntityLog.log(ServiceSynchronize.this, "Available " + network + " " + cm.getNetworkInfo(network)); if (!started && suitableNetwork()) @@ -2466,7 +2486,7 @@ public class ServiceSynchronize extends LifecycleService { state = new ServiceState(); state.runnable(new Runnable() { - PowerManager pm = getSystemService(PowerManager.class); + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); PowerManager.WakeLock wl = pm.newWakeLock( PowerManager.PARTIAL_WAKE_LOCK, BuildConfig.APPLICATION_ID + ":main"); private List threadState = new ArrayList<>(); @@ -2512,7 +2532,7 @@ public class ServiceSynchronize extends LifecycleService { private Observer> observer = new Observer>() { private List handling = new ArrayList<>(); private ExecutorService executor = Executors.newSingleThreadExecutor(Helper.backgroundThreadFactory); - PowerManager pm = getSystemService(PowerManager.class); + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); PowerManager.WakeLock wl = pm.newWakeLock( PowerManager.PARTIAL_WAKE_LOCK, BuildConfig.APPLICATION_ID + ":outbox"); @@ -2640,7 +2660,7 @@ public class ServiceSynchronize extends LifecycleService { queued++; queue.submit(new Runnable() { - PowerManager pm = getSystemService(PowerManager.class); + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); PowerManager.WakeLock wl = pm.newWakeLock( PowerManager.PARTIAL_WAKE_LOCK, BuildConfig.APPLICATION_ID + ":manage");