Android 5 Lollipop compatibility

pull/146/head
M66B 6 years ago
parent be415c6a5f
commit ea895040f5

@ -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
<a name="faq2"></a>

@ -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

@ -4,7 +4,7 @@ android {
compileSdkVersion 28
defaultConfig {
applicationId "eu.faircode.email"
minSdkVersion 23
minSdkVersion 21
targetSdkVersion 28
versionCode 210
versionName "1.210"

@ -7,6 +7,9 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission
android:name="android.permission.USE_CREDENTIALS"
android:maxSdkVersion="22" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="com.android.vending.BILLING" />

@ -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);

@ -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");

@ -1440,7 +1440,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
db.identity().setIdentityError(message.identity, null);
NotificationManager nm = context.getSystemService(NotificationManager.class);
NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
nm.cancel("send", message.identity.intValue());
} else
EntityOperation.queue(db, message, EntityOperation.DELETE);

@ -23,6 +23,7 @@ import android.app.Application;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.os.Build;
import android.os.DeadSystemException;
import android.os.RemoteException;
@ -62,7 +63,7 @@ public class ApplicationEx extends Application {
private void createNotificationChannels() {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
NotificationManager nm = getSystemService(NotificationManager.class);
NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
NotificationChannel service = new NotificationChannel(
"service",

@ -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,

@ -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);

@ -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);

@ -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()) {

@ -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);
}

@ -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));
}

@ -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");

@ -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<TupleAccountStats>() {
@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<ServiceState> threadState = new ArrayList<>();
@ -2512,7 +2532,7 @@ public class ServiceSynchronize extends LifecycleService {
private Observer<List<EntityOperation>> observer = new Observer<List<EntityOperation>>() {
private List<Long> 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");

Loading…
Cancel
Save