explicitly declare foreground service type

Daniel Gultsch created

Change summary

src/main/AndroidManifest.xml                                             | 40 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java | 71 
2 files changed, 90 insertions(+), 21 deletions(-)

Detailed changes

src/main/AndroidManifest.xml 🔗

@@ -3,8 +3,13 @@
     xmlns:tools="http://schemas.android.com/tools">
 
     <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission
+        android:name="android.permission.WRITE_EXTERNAL_STORAGE"
+        android:maxSdkVersion="32"
+        tools:ignore="ScopedStorage" />
+    <uses-permission
+        android:name="android.permission.READ_EXTERNAL_STORAGE"
+        android:maxSdkVersion="32" />
     <uses-permission android:name="android.permission.READ_CONTACTS" />
     <uses-permission android:name="android.permission.READ_PROFILE" />
     <uses-permission
@@ -40,6 +45,16 @@
     <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
     <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
 
+    <!--    New permissions required to run as foreground service on Android 14.
+            SYSTEM_EXEMPTED is used when the app is on the doze allow list. This is normal
+            and the expected default behaviour. The other two hijack RECORD_AUDIO and CAMERA if they
+            happen to be granted. -->
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
+
     <uses-feature
         android:name="android.hardware.camera"
         android:required="false" />
@@ -77,32 +92,40 @@
     <application
         android:allowBackup="true"
         android:appCategory="social"
-        android:fullBackupContent="@xml/backup_content"
         android:dataExtractionRules="@xml/data_extraction_rules"
+        android:fullBackupContent="@xml/backup_content"
         android:hardwareAccelerated="true"
         android:icon="@mipmap/new_launcher"
         android:label="@string/app_name"
         android:largeHeap="true"
+        android:localeConfig="@xml/locales_config"
         android:networkSecurityConfig="@xml/network_security_configuration"
         android:preserveLegacyExternalStorage="true"
         android:requestLegacyExternalStorage="true"
         android:theme="@style/ConversationsTheme"
         tools:replace="android:label"
-        android:localeConfig="@xml/locales_config"
         tools:targetApi="tiramisu">
 
         <meta-data
             android:name="com.google.android.gms.car.application"
             android:resource="@xml/automotive_app_desc" />
 
-        <service android:name=".services.XmppConnectionService" />
+        <service
+            android:name=".services.XmppConnectionService"
+            android:exported="false"
+            android:foregroundServiceType="specialUse|systemExempted|microphone|camera">
+            <property
+                android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
+                android:value="im" />
+        </service>
 
         <receiver
             android:name=".services.EventReceiver"
             android:exported="false">
             <intent-filter>
                 <action android:name="android.intent.action.BOOT_COMPLETED" />
-                <action android:name="android.net.conn.CONNECTIVITY_CHANGE"
+                <action
+                    android:name="android.net.conn.CONNECTIVITY_CHANGE"
                     tools:ignore="BatteryLife" />
                 <action android:name="android.intent.action.ACTION_SHUTDOWN" />
                 <action android:name="android.media.RINGER_MODE_CHANGED" />
@@ -150,7 +173,6 @@
         </activity>
         <activity
             android:name=".ui.ConversationsActivity"
-            android:label="@string/app_name"
             android:launchMode="singleTask"
             android:minWidth="300dp"
             android:minHeight="300dp"
@@ -162,8 +184,7 @@
             android:windowSoftInputMode="stateAlwaysHidden" />
         <activity
             android:name=".ui.UriHandlerActivity"
-            android:exported="true"
-            android:label="@string/app_name">
+            android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.VIEW" />
 
@@ -265,7 +286,6 @@
         <activity
             android:name=".ui.ShareWithActivity"
             android:exported="true"
-            android:label="@string/app_name"
             android:launchMode="singleTop">
 
             <intent-filter>

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java 🔗

@@ -19,6 +19,7 @@ import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.SharedPreferences;
 import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
 import android.database.ContentObserver;
 import android.graphics.Bitmap;
 import android.media.AudioManager;
@@ -487,6 +488,7 @@ public class XmppConnectionService extends Service {
     private WakeLock wakeLock;
     private LruCache<String, Bitmap> mBitmapCache;
     private final BroadcastReceiver mInternalEventReceiver = new InternalEventReceiver();
+    private final BroadcastReceiver mInternalRestrictedEventReceiver = new RestrictedEventReceiver(Arrays.asList(TorServiceUtils.ACTION_STATUS));
     private final BroadcastReceiver mInternalScreenEventReceiver = new InternalEventReceiver();
 
     private static String generateFetchKey(Account account, final Avatar avatar) {
@@ -1236,16 +1238,26 @@ public class XmppConnectionService extends Service {
         toggleForegroundService();
         updateUnreadCountBadge();
         toggleScreenEventReceiver();
-        final IntentFilter intentFilter = new IntentFilter();
-        intentFilter.addAction(TorServiceUtils.ACTION_STATUS);
+        final IntentFilter systemBroadcastFilter = new IntentFilter();
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
             scheduleNextIdlePing();
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
-                intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
-            }
-            intentFilter.addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED);
-        }
-        registerReceiver(this.mInternalEventReceiver, intentFilter);
+                systemBroadcastFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
+            }
+            systemBroadcastFilter.addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED);
+        }
+        ContextCompat.registerReceiver(
+                this,
+                this.mInternalEventReceiver,
+                systemBroadcastFilter,
+                ContextCompat.RECEIVER_NOT_EXPORTED);
+        final IntentFilter exportedBroadcastFilter = new IntentFilter();
+        exportedBroadcastFilter.addAction(TorServiceUtils.ACTION_STATUS);
+        ContextCompat.registerReceiver(
+                this,
+                this.mInternalRestrictedEventReceiver,
+                exportedBroadcastFilter,
+                ContextCompat.RECEIVER_EXPORTED);
         mForceDuringOnCreate.set(false);
         toggleForegroundService();
         setupPhoneStateListener();
@@ -1315,6 +1327,7 @@ public class XmppConnectionService extends Service {
     public void onDestroy() {
         try {
             unregisterReceiver(this.mInternalEventReceiver);
+            unregisterReceiver(this.mInternalRestrictedEventReceiver);
             unregisterReceiver(this.mInternalScreenEventReceiver);
         } catch (final IllegalArgumentException e) {
             //ignored
@@ -1396,9 +1409,26 @@ public class XmppConnectionService extends Service {
 
     private void startForegroundOrCatch(final int id, final Notification notification) {
         try {
-            startForeground(id, notification);
-        } catch (final IllegalStateException e) {
-            Log.e(Config.LOGTAG,"Could not start foreground service", e);
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+                final int foregroundServiceType;
+                if (getSystemService(PowerManager.class)
+                        .isIgnoringBatteryOptimizations(getPackageName())) {
+                    foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED;
+                } else if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
+                        == PackageManager.PERMISSION_GRANTED) {
+                    foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE;
+                } else if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
+                        == PackageManager.PERMISSION_GRANTED) {
+                    foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA;
+                } else {
+                    foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE;
+                }
+                startForeground(id, notification, foregroundServiceType);
+            } else {
+                startForeground(id, notification);
+            }
+        } catch (final IllegalStateException | SecurityException e) {
+            Log.e(Config.LOGTAG, "Could not start foreground service", e);
         }
     }
 
@@ -5027,11 +5057,30 @@ public class XmppConnectionService extends Service {
     private class InternalEventReceiver extends BroadcastReceiver {
 
         @Override
-        public void onReceive(Context context, Intent intent) {
+        public void onReceive(final Context context, final Intent intent) {
             onStartCommand(intent, 0, 0);
         }
     }
 
+    private class RestrictedEventReceiver extends BroadcastReceiver {
+
+        private final Collection<String> allowedActions;
+
+        private RestrictedEventReceiver(final Collection<String> allowedActions) {
+            this.allowedActions = allowedActions;
+        }
+
+        @Override
+        public void onReceive(final Context context, final Intent intent) {
+            final String action = intent == null ? null : intent.getAction();
+            if (allowedActions.contains(action)) {
+                onStartCommand(intent,0,0);
+            } else {
+                Log.e(Config.LOGTAG,"restricting broadcast of event "+action);
+            }
+        }
+    }
+
     public static class OngoingCall {
         public final AbstractJingleConnection.Id id;
         public final Set<Media> media;