Run GCM services as foreground service if needed

That's when we run with battery optimizations disabled but not in foreground
This commit is contained in:
Marvin W 2019-12-04 22:30:26 +01:00
parent cfc90b45bd
commit db4bb568e1
No known key found for this signature in database
GPG Key ID: 072E9235DB996F2A
8 changed files with 207 additions and 73 deletions

View File

@ -93,6 +93,7 @@
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission
android:name="android.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST"
tools:ignore="ProtectedPermissions"/>

View File

@ -29,6 +29,7 @@ import android.util.Log;
import com.google.android.gms.checkin.internal.ICheckinService;
import org.microg.gms.auth.AuthConstants;
import org.microg.gms.common.ForegroundServiceContext;
import org.microg.gms.gcm.McsService;
import org.microg.gms.people.PeopleManager;
@ -55,6 +56,7 @@ public class CheckinService extends IntentService {
@Override
protected void onHandleIntent(Intent intent) {
try {
ForegroundServiceContext.completeForegroundService(this, intent, TAG);
if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean(PREF_ENABLE_CHECKIN, false)) {
LastCheckinInfo info = CheckinManager.checkin(this, intent.getBooleanExtra(EXTRA_FORCE_CHECKIN, false));
if (info != null) {

View File

@ -24,6 +24,10 @@ import android.preference.PreferenceManager;
import android.support.v4.content.WakefulBroadcastReceiver;
import android.util.Log;
import org.microg.gms.common.ForegroundServiceContext;
import static org.microg.gms.checkin.CheckinService.EXTRA_FORCE_CHECKIN;
public class TriggerReceiver extends WakefulBroadcastReceiver {
private static final String TAG = "GmsCheckinTrigger";
public static final String PREF_ENABLE_CHECKIN = "checkin_enable_service";
@ -31,23 +35,27 @@ public class TriggerReceiver extends WakefulBroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
boolean force = "android.provider.Telephony.SECRET_CODE".equals(intent.getAction());
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
try {
boolean force = "android.provider.Telephony.SECRET_CODE".equals(intent.getAction());
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean(PREF_ENABLE_CHECKIN, false) || force) {
if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction()) &&
LastCheckinInfo.read(context).lastCheckin > System.currentTimeMillis() - REGULAR_CHECKIN_INTERVAL) {
return;
}
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean(PREF_ENABLE_CHECKIN, false) || force) {
if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction()) &&
LastCheckinInfo.read(context).lastCheckin > System.currentTimeMillis() - REGULAR_CHECKIN_INTERVAL) {
return;
}
NetworkInfo networkInfo = cm.getActiveNetworkInfo();
if (networkInfo != null && networkInfo.isConnected() || force) {
Intent subIntent = new Intent(context, CheckinService.class);
subIntent.putExtra(CheckinService.EXTRA_FORCE_CHECKIN, force);
startWakefulService(context, subIntent);
NetworkInfo networkInfo = cm.getActiveNetworkInfo();
if (networkInfo != null && networkInfo.isConnected() || force) {
Intent subIntent = new Intent(context, CheckinService.class);
subIntent.putExtra(EXTRA_FORCE_CHECKIN, force);
startWakefulService(new ForegroundServiceContext(context), subIntent);
}
} else {
Log.d(TAG, "Ignoring " + intent + ": checkin is disabled");
}
} else {
Log.d(TAG, "Ignoring " + intent + ": checkin is disabled");
} catch (Exception e) {
Log.w(TAG, e);
}
}
}

View File

@ -0,0 +1,80 @@
package org.microg.gms.common;
import android.app.ActivityManager;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.os.Build;
import android.os.PowerManager;
import android.support.annotation.RequiresApi;
import android.util.Log;
import com.google.android.gms.R;
import java.util.List;
public class ForegroundServiceContext extends ContextWrapper {
private static final String TAG = "ForegroundService";
public static final String EXTRA_FOREGROUND = "foreground";
public ForegroundServiceContext(Context base) {
super(base);
}
@Override
public ComponentName startService(Intent service) {
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !isIgnoringBatteryOptimizations() && !isAppOnForeground()) {
Log.d(TAG, "Starting in foreground mode.");
service.putExtra(EXTRA_FOREGROUND, true);
return super.startForegroundService(service);
}
return super.startService(service);
}
@RequiresApi(api = Build.VERSION_CODES.M)
private boolean isIgnoringBatteryOptimizations() {
PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);
return powerManager.isIgnoringBatteryOptimizations(getPackageName());
}
private boolean isAppOnForeground() {
ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningAppProcessInfo> appProcesses = activityManager.getRunningAppProcesses();
if (appProcesses == null) {
return false;
}
final String packageName = getPackageName();
for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {
if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND && appProcess.processName.equals(packageName)) {
return true;
}
}
return false;
}
public static void completeForegroundService(Service service, Intent intent, String tag) {
if (intent.getBooleanExtra(EXTRA_FOREGROUND, false) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Log.d(tag, "Started in foreground mode.");
service.startForeground(tag.hashCode(), buildForegroundNotification(service));
}
}
@RequiresApi(api = Build.VERSION_CODES.O)
private static Notification buildForegroundNotification(Context context) {
NotificationChannel channel = new NotificationChannel("foreground-service", "Foreground Service", NotificationManager.IMPORTANCE_NONE);
channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET);
channel.setShowBadge(false);
channel.setVibrationPattern(new long[0]);
context.getSystemService(NotificationManager.class).createNotificationChannel(channel);
return new Notification.Builder(context, channel.getId())
.setOngoing(true)
.setContentTitle("Running in background")
.setSmallIcon(R.drawable.gcm_bell)
.build();
}
}

View File

@ -31,6 +31,7 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static android.os.Build.VERSION.SDK_INT;
@ -198,8 +199,11 @@ public class PackageUtils {
ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
if (manager == null) return null;
if (pid <= 0) return null;
for (ActivityManager.RunningAppProcessInfo processInfo : manager.getRunningAppProcesses()) {
if (processInfo.pid == pid) return processInfo.processName;
List<ActivityManager.RunningAppProcessInfo> runningAppProcesses = manager.getRunningAppProcesses();
if (runningAppProcesses != null) {
for (ActivityManager.RunningAppProcessInfo processInfo : runningAppProcesses) {
if (processInfo.pid == pid) return processInfo.processName;
}
}
return null;
}

View File

@ -28,7 +28,6 @@ import java.util.Collections;
import java.util.List;
public class GcmPrefs implements SharedPreferences.OnSharedPreferenceChangeListener {
public static final String PREF_HEARTBEAT = "gcm_heartbeat_interval";
public static final String PREF_FULL_LOG = "gcm_full_log";
public static final String PREF_LAST_PERSISTENT_ID = "gcm_last_persistent_id";
public static final String PREF_CONFIRM_NEW_APPS = "gcm_confirm_new_apps";
@ -43,6 +42,9 @@ public class GcmPrefs implements SharedPreferences.OnSharedPreferenceChangeListe
public static final String PREF_LEARNT_WIFI = "gcm_learnt_wifi";
public static final String PREF_LEARNT_OTHER = "gcm_learnt_other";
private static final int MIN_INTERVAL = 5 * 60 * 1000; // 5 minutes
private static final int MAX_INTERVAL = 30 * 60 * 1000; // 30 minutes
private static GcmPrefs INSTANCE;
public static GcmPrefs get(Context context) {
@ -53,7 +55,6 @@ public class GcmPrefs implements SharedPreferences.OnSharedPreferenceChangeListe
return INSTANCE;
}
private int heartbeatMs = 300000;
private boolean gcmLogEnabled = true;
private String lastPersistedId = "";
private boolean confirmNewApps = false;
@ -79,7 +80,6 @@ public class GcmPrefs implements SharedPreferences.OnSharedPreferenceChangeListe
}
public void update() {
heartbeatMs = Integer.parseInt(defaultPreferences.getString(PREF_HEARTBEAT, "300")) * 1000;
gcmLogEnabled = defaultPreferences.getBoolean(PREF_FULL_LOG, true);
lastPersistedId = defaultPreferences.getString(PREF_LAST_PERSISTENT_ID, "");
confirmNewApps = defaultPreferences.getBoolean(PREF_CONFIRM_NEW_APPS, false);
@ -95,10 +95,6 @@ public class GcmPrefs implements SharedPreferences.OnSharedPreferenceChangeListe
learntOther = defaultPreferences.getInt(PREF_LEARNT_OTHER, 300000);
}
public int getHeartbeatMs() {
return heartbeatMs;
}
public String getNetworkPrefForInfo(NetworkInfo info) {
if (info == null) return PREF_NETWORK_OTHER;
if (info.isRoaming()) return PREF_NETWORK_ROAMING;
@ -158,7 +154,7 @@ public class GcmPrefs implements SharedPreferences.OnSharedPreferenceChangeListe
learntOther *= 0.95;
break;
}
defaultPreferences.edit().putInt(PREF_LEARNT_MOBILE, learntMobile).putInt(PREF_LEARNT_WIFI, learntWifi).putInt(PREF_LEARNT_OTHER, learntOther).apply();
updateLearntValues();
}
public void learnReached(String pref, long time) {
@ -178,6 +174,13 @@ public class GcmPrefs implements SharedPreferences.OnSharedPreferenceChangeListe
learntOther *= 1.02;
break;
}
updateLearntValues();
}
private void updateLearntValues() {
learntMobile = Math.max(MIN_INTERVAL, Math.min(learntMobile, MAX_INTERVAL));
learntWifi = Math.max(MIN_INTERVAL, Math.min(learntWifi, MAX_INTERVAL));
learntOther = Math.max(MIN_INTERVAL, Math.min(learntOther, MAX_INTERVAL));
defaultPreferences.edit().putInt(PREF_LEARNT_MOBILE, learntMobile).putInt(PREF_LEARNT_WIFI, learntWifi).putInt(PREF_LEARNT_OTHER, learntOther).apply();
}

View File

@ -17,13 +17,15 @@
package org.microg.gms.gcm;
import android.app.AlarmManager;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.PermissionInfo;
import android.content.pm.ResolveInfo;
import android.net.ConnectivityManager;
import android.os.Build;
@ -36,12 +38,14 @@ import android.os.Parcelable;
import android.os.PowerManager;
import android.os.SystemClock;
import android.os.UserHandle;
import android.support.annotation.RequiresApi;
import android.support.v4.content.WakefulBroadcastReceiver;
import android.util.Log;
import com.squareup.wire.Message;
import org.microg.gms.checkin.LastCheckinInfo;
import org.microg.gms.common.ForegroundServiceContext;
import org.microg.gms.common.PackageUtils;
import org.microg.gms.gcm.mcs.AppData;
import org.microg.gms.gcm.mcs.Close;
@ -66,6 +70,7 @@ import okio.ByteString;
import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP;
import static android.os.Build.VERSION.SDK_INT;
import static org.microg.gms.common.ForegroundServiceContext.EXTRA_FOREGROUND;
import static org.microg.gms.gcm.GcmConstants.ACTION_C2DM_RECEIVE;
import static org.microg.gms.gcm.GcmConstants.EXTRA_APP;
import static org.microg.gms.gcm.GcmConstants.EXTRA_COLLAPSE_KEY;
@ -173,11 +178,16 @@ public class McsService extends Service implements Handler.Callback {
heartbeatIntent = PendingIntent.getService(this, 0, new Intent(ACTION_HEARTBEAT, null, this, McsService.class), 0);
alarmManager = (AlarmManager) getSystemService(ALARM_SERVICE);
powerManager = (PowerManager) getSystemService(POWER_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && checkSelfPermission("android.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST") == PackageManager.PERMISSION_GRANTED) {
try {
Field field = Context.class.getField("DEVICE_IDLE_CONTROLLER");
String deviceIdleControllerName = "deviceidle";
try {
Field field = Context.class.getField("DEVICE_IDLE_CONTROLLER");
deviceIdleControllerName = (String) field.get(null);
} catch (Exception ignored) {
}
IBinder binder = (IBinder) Class.forName("android.os.ServiceManager")
.getMethod("getService", String.class).invoke(null, field.get(null));
.getMethod("getService", String.class).invoke(null, deviceIdleControllerName);
if (binder != null) {
deviceIdleController = Class.forName("android.os.IDeviceIdleController$Stub")
.getMethod("asInterface", IBinder.class).invoke(null, binder);
@ -240,8 +250,12 @@ public class McsService extends Service implements Handler.Callback {
AlarmManager alarmManager = (AlarmManager) context.getSystemService(ALARM_SERVICE);
long delay = getCurrentDelay();
logd("Scheduling reconnect in " + delay / 1000 + " seconds...");
alarmManager.set(ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + delay,
PendingIntent.getBroadcast(context, 1, new Intent(ACTION_RECONNECT, null, context, TriggerReceiver.class), 0));
PendingIntent pi = PendingIntent.getBroadcast(context, 1, new Intent(ACTION_RECONNECT, null, context, TriggerReceiver.class), 0);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmManager.setExactAndAllowWhileIdle(ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + delay, pi);
} else {
alarmManager.set(ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + delay, pi);
}
}
public void scheduleHeartbeat(Context context) {
@ -252,13 +266,16 @@ public class McsService extends Service implements Handler.Callback {
closeAll();
}
logd("Scheduling heartbeat in " + heartbeatMs / 1000 + " seconds...");
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
alarmManager.set(ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + heartbeatMs, heartbeatIntent);
} else {
// with KitKat, the alarms become inexact by default, but with the newly available setWindow we can get inexact alarms with guarantees.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// This is supposed to work even when running in idle and without battery optimization disabled
alarmManager.setExactAndAllowWhileIdle(ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + heartbeatMs, heartbeatIntent);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// With KitKat, the alarms become inexact by default, but with the newly available setWindow we can get inexact alarms with guarantees.
// Schedule the alarm to fire within the interval [heartbeatMs/3*4, heartbeatMs]
alarmManager.setWindow(ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + heartbeatMs / 4 * 3, heartbeatMs / 4,
heartbeatIntent);
} else {
alarmManager.set(ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + heartbeatMs, heartbeatIntent);
}
}
@ -277,6 +294,7 @@ public class McsService extends Service implements Handler.Callback {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
ForegroundServiceContext.completeForegroundService(this, intent, TAG);
synchronized (McsService.class) {
if (rootHandler != null) {
if (intent == null) return START_REDELIVER_INTENT;
@ -299,6 +317,19 @@ public class McsService extends Service implements Handler.Callback {
return START_REDELIVER_INTENT;
}
@RequiresApi(api = Build.VERSION_CODES.O)
private Notification buildForegroundNotification() {
NotificationChannel channel = new NotificationChannel("foreground-service", "Foreground Service", NotificationManager.IMPORTANCE_LOW);
channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET);
channel.setShowBadge(false);
getSystemService(NotificationManager.class).createNotificationChannel(channel);
return new Notification.Builder(this, channel.getId())
.setOngoing(true)
.setContentTitle("Running in background")
.setSmallIcon(android.R.drawable.stat_notify_sync)
.build();
}
private void handleSendMessage(Intent intent) {
String messageId = intent.getStringExtra(EXTRA_MESSAGE_ID);
String collapseKey = intent.getStringExtra(EXTRA_COLLAPSE_KEY);

View File

@ -25,6 +25,7 @@ import android.support.v4.content.WakefulBroadcastReceiver;
import android.util.Log;
import org.microg.gms.checkin.LastCheckinInfo;
import org.microg.gms.common.ForegroundServiceContext;
import static android.os.Build.VERSION.SDK_INT;
import static android.os.Build.VERSION_CODES.N;
@ -51,51 +52,55 @@ public class TriggerReceiver extends WakefulBroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
boolean force = "android.provider.Telephony.SECRET_CODE".equals(intent.getAction()) || FORCE_TRY_RECONNECT.equals(intent.getAction());
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
try {
boolean force = "android.provider.Telephony.SECRET_CODE".equals(intent.getAction()) || FORCE_TRY_RECONNECT.equals(intent.getAction());
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (!GcmPrefs.get(context).isEnabled() && !force) {
Log.d(TAG, "Ignoring " + intent + ": gcm is disabled");
return;
}
if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) {
McsService.resetCurrentDelay();
}
if (LastCheckinInfo.read(context).androidId == 0) {
Log.d(TAG, "Ignoring " + intent + ": need to checkin first.");
return;
}
force |= "android.intent.action.BOOT_COMPLETED".equals(intent.getAction());
NetworkInfo networkInfo = cm.getActiveNetworkInfo();
if (!force) {
if (networkInfo == null || !networkInfo.isConnected()) {
Log.d(TAG, "Ignoring " + intent + ": network is offline, scheduling new attempt.");
McsService.scheduleReconnect(context);
return;
} else if (!GcmPrefs.get(context).isEnabledFor(networkInfo)) {
Log.d(TAG, "Ignoring " + intent + ": gcm is disabled for " + networkInfo.getTypeName());
if (!GcmPrefs.get(context).isEnabled() && !force) {
Log.d(TAG, "Ignoring " + intent + ": gcm is disabled");
return;
}
}
if (!McsService.isConnected() || force) {
Log.d(TAG, "Not connected to GCM but should be, asking the service to start up. Triggered by: " + intent);
startWakefulService(context, new Intent(ACTION_CONNECT, null, context, McsService.class)
.putExtra(EXTRA_REASON, intent));
} else {
if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) {
Log.d(TAG, "Ignoring " + intent + ": service is running. schedule reconnect instead.");
McsService.scheduleReconnect(context);
} else {
Log.d(TAG, "Ignoring " + intent + ": service is running. heartbeat instead.");
startWakefulService(context, new Intent(ACTION_HEARTBEAT, null, context, McsService.class)
.putExtra(EXTRA_REASON, intent));
McsService.resetCurrentDelay();
}
if (LastCheckinInfo.read(context).androidId == 0) {
Log.d(TAG, "Ignoring " + intent + ": need to checkin first.");
return;
}
force |= "android.intent.action.BOOT_COMPLETED".equals(intent.getAction());
NetworkInfo networkInfo = cm.getActiveNetworkInfo();
if (!force) {
if (networkInfo == null || !networkInfo.isConnected()) {
Log.d(TAG, "Ignoring " + intent + ": network is offline, scheduling new attempt.");
McsService.scheduleReconnect(context);
return;
} else if (!GcmPrefs.get(context).isEnabledFor(networkInfo)) {
Log.d(TAG, "Ignoring " + intent + ": gcm is disabled for " + networkInfo.getTypeName());
return;
}
}
if (!McsService.isConnected() || force) {
Log.d(TAG, "Not connected to GCM but should be, asking the service to start up. Triggered by: " + intent);
startWakefulService(new ForegroundServiceContext(context), new Intent(ACTION_CONNECT, null, context, McsService.class)
.putExtra(EXTRA_REASON, intent));
} else {
if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) {
Log.d(TAG, "Ignoring " + intent + ": service is running. schedule reconnect instead.");
McsService.scheduleReconnect(context);
} else {
Log.d(TAG, "Ignoring " + intent + ": service is running. heartbeat instead.");
startWakefulService(new ForegroundServiceContext(context), new Intent(ACTION_HEARTBEAT, null, context, McsService.class)
.putExtra(EXTRA_REASON, intent));
}
}
} catch (Exception e) {
Log.w(TAG, e);
}
}