Loading device.mk +3 −0 Original line number Diff line number Diff line Loading @@ -167,6 +167,9 @@ PRODUCT_COPY_FILES += \ PRODUCT_COPY_FILES += \ $(LOCAL_PATH)/configs/permissions/privapp-permissions-fpcam.xml:$(TARGET_COPY_OUT_SYSTEM)/etc/permissions/privapp-permissions-fpcam.xml # Fairphone Moments $(call inherit-product, external/FairphoneMoments/config.mk) # GPS PRODUCT_COPY_FILES += \ frameworks/native/data/etc/android.hardware.location.gps.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.location.gps.xml Loading parts/res/values/strings.xml +1 −0 Original line number Diff line number Diff line Loading @@ -34,6 +34,7 @@ <!-- Toast --> <string name="sensor_access">Camera & mic access</string> <string name="fairphone_moments">Fairphone Moments</string> <string name="do_not_disturb">Do Not Disturb</string> <string name="flight_mode">Airplane mode</string> <string name="torch">Torch</string> Loading parts/res/xml/switch_preferences.xml +6 −0 Original line number Diff line number Diff line Loading @@ -14,6 +14,12 @@ android:summary="@string/sensor_privacy_summary" android:defaultValue="true" /> <org.lineageos.settings.preference.RadioButtonPreference android:key="fairphone_moments" android:title="@string/fairphone_moments_title" android:summary="@string/fairphone_moments_summary" android:defaultValue="false" /> <org.lineageos.settings.preference.RadioButtonPreference android:key="do_not_disturb" android:title="@string/do_not_disturb_title" Loading parts/src/org/lineageos/settings/AppPrefs.java +27 −0 Original line number Diff line number Diff line Loading @@ -14,6 +14,7 @@ public class AppPrefs { private static final String KEY_FIRST_BOOT = "first_boot"; private static final String KEY_FPCAMERA = "fp_camera_pref"; private static final String KEY_CURRENT_LAUNCHER = "current_launcher"; private AppPrefs(Context context) { this.context = context.getApplicationContext(); Loading @@ -27,6 +28,19 @@ public class AppPrefs { return instance; } public String getSavedHomeApp() { return prefs.getString(KEY_CURRENT_LAUNCHER, ""); } public void saveDefaultHomeApp(String packageName) { prefs.edit().putString(KEY_CURRENT_LAUNCHER, packageName).apply(); } public void saveCurrentHomeApp() { String currentHomeApp = getDefaultHomeAppPackageName(); saveDefaultHomeApp(currentHomeApp); } public boolean getIsFirstBoot() { return prefs.getBoolean(KEY_FIRST_BOOT, true); } Loading @@ -42,4 +56,17 @@ public class AppPrefs { public void setFpCameraEnabled(boolean enabled) { prefs.edit().putBoolean(KEY_FPCAMERA, enabled).apply(); } private String getDefaultHomeAppPackageName() { Intent intent = new Intent(Intent.ACTION_MAIN); intent.addCategory(Intent.CATEGORY_HOME); ResolveInfo resolveInfo = context.getPackageManager() .resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY); if (resolveInfo != null && resolveInfo.activityInfo != null) { return resolveInfo.activityInfo.packageName; } return ""; } } parts/src/org/lineageos/settings/switchcust/LauncherSwitcherService.java 0 → 100644 +256 −0 Original line number Diff line number Diff line package org.lineageos.settings.switchcust; import android.app.ActivityManager; import android.app.role.RoleManager; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.os.UserHandle; import android.util.Log; import androidx.core.content.ContextCompat; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import org.lineageos.settings.AppPrefs; public class LauncherSwitcherService { private static final String TAG = "LauncherSwitcherService"; /** Callback interface for launcher switching operations */ public interface LauncherSwitchCallback { void onSuccess(); void onError(Exception exception); } public void switchToUserLauncher( Context context, LauncherSwitchCallback callback) { Executor executor = ContextCompat.getMainExecutor(context); executor.execute( () -> { try { AppPrefs appPrefs = AppPrefs.getInstance(context); String userLauncherApp = appPrefs.getSavedHomeApp(); boolean shouldShowOverlayAnimation = shouldShowOverlayAnimation(context); if (!isPackageAvailable(context, userLauncherApp)) { userLauncherApp = SwitchConstants.PACKAGE_MOMENTS; } // Create a final variable for use in lambda final String finalUserLauncherApp = userLauncherApp; setDefaultHomeAppAsync(context, finalUserLauncherApp) .thenAccept( success -> { if (success) { startSwitchStateChangeActivity( context, false, shouldShowOverlayAnimation); callback.onSuccess(); } else { callback.onError( new Exception( "Failed to set default launcher")); } }) .exceptionally( throwable -> { String fallbackLauncher = SwitchConstants.PACKAGE_BLISS; Log.e( TAG, "Exception occurred: " + throwable.getMessage()); callback.onError( new Exception( "Async operation failed", throwable)); return null; }); } catch (Exception e) { callback.onError(e); } }); } public void switchToFairphoneMoments( Context context, LauncherSwitchCallback callback) { Executor executor = ContextCompat.getMainExecutor(context); executor.execute( () -> { try { if (!isFairphoneMomentsAvailable(context)) { callback.onError(new Exception("Fairphone Moments is not available")); return; } AppPrefs appPrefs = AppPrefs.getInstance(context); String currentHomeApp = getDefaultHomeAppPackageName(context); appPrefs.saveDefaultHomeApp(currentHomeApp); boolean shouldShowOverlayAnimation = shouldShowOverlayAnimation(context); setDefaultHomeAppAsync(context, SwitchConstants.PACKAGE_MOMENTS) .thenAccept( success -> { if (success) { startSwitchStateChangeActivity( context, true, shouldShowOverlayAnimation); callback.onSuccess(); } else { callback.onError( new Exception( "Failed to set default launcher")); } }) .exceptionally( throwable -> { callback.onError( new Exception( "Eror setting default launcher", throwable)); return null; }); } catch (Exception e) { callback.onError(e); } }); } private CompletableFuture<Boolean> setDefaultHomeAppAsync( Context context, String packageName) { CompletableFuture<Boolean> future = new CompletableFuture<>(); Log.d(TAG, "Setting default home app: " + packageName); try { RoleManager roleManager = getRoleManager(context); if (roleManager == null) { future.complete(false); return future; } int foregroundUser = ActivityManager.getCurrentUser(); roleManager.addRoleHolderAsUser( RoleManager.ROLE_HOME, packageName, 0, UserHandle.of(foregroundUser), ContextCompat.getMainExecutor(context), future::complete); } catch (Exception e) { Log.e(TAG, "Error setting " + packageName + " as default home app", e); future.complete(false); } return future; } private String getDefaultHomeAppPackageName(Context context) { RoleManager roleManager = getRoleManager(context); if (roleManager == null) { throw new IllegalStateException("RoleManager is not available"); } String packageName = roleManager.getDefaultApplication(RoleManager.ROLE_HOME); Log.d(TAG, "Default home app package name: " + packageName); if (packageName == null) { throw new IllegalStateException("Could not get default home app package name"); } if (SwitchConstants.PACKAGE_MOMENTS.equals(packageName)) { return SwitchConstants.PACKAGE_BLISS; } else { return packageName; } } /** Start switch state change activity in detox launcher to display overlay. */ private void startSwitchStateChangeActivity( Context context, boolean detoxEnabled, boolean showOverlay) { Intent intent = new Intent(SwitchConstants.ACTION_SHOW_SWITCH_BUTTON_HINT); intent.putExtra( SwitchConstants.EXTRA_SWITCH_BUTTON_STATE, detoxEnabled ? SwitchConstants.EXTRA_SWITCH_BUTTON_STATE_ENABLED : SwitchConstants.EXTRA_SWITCH_BUTTON_STATE_DISABLED); intent.putExtra(SwitchConstants.EXTRA_SHOW_OVERLAY, showOverlay); intent.setPackage(SwitchConstants.PACKAGE_MOMENTS); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); context.startActivityAsUser(intent, UserHandle.SYSTEM); } /** * Detox Launcher should display an overlay animation when user is interacting with any app, or * what is the same: it should NOT display an overlay animation when user is interacting with * stock launcher or spring launcher */ private boolean shouldShowOverlayAnimation(Context context) { ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); if (am == null) { return true; } try { // Get the foreground task List<ActivityManager.RunningTaskInfo> runningTasks = am.getRunningTasks(1); String defaultHomeAppPackage = getDefaultHomeAppPackageName(context); if (runningTasks != null && !runningTasks.isEmpty()) { ActivityManager.RunningTaskInfo task = runningTasks.get(0); if (task != null && task.topActivity != null) { String topPackageName = task.topActivity.getPackageName(); String topClassName = task.topActivity.getClassName(); Log.d(TAG, "Top activity package: " + topPackageName); Log.d(TAG, "Top activity name: " + topClassName); Log.d(TAG, "Default home app package: " + defaultHomeAppPackage); if (defaultHomeAppPackage.equals(topPackageName)) { return false; } if (SwitchConstants.PACKAGE_MOMENTS.equals(topPackageName) && SwitchConstants.FAIRPHONE_MOMENTS_HOME_ACTIVITY.equals( topClassName)) { return false; } return true; } } } catch (SecurityException e) { Log.e(TAG, "Permission needed for getRunningTasks", e); } catch (Exception e) { Log.e(TAG, "Error checking overlay animation requirement", e); } return true; } private RoleManager getRoleManager(Context context) { try { return context.getSystemService(RoleManager.class); } catch (Exception e) { Log.e(TAG, "Failed to get RoleManager", e); return null; } } private boolean isPackageAvailable(Context context, String packageName) { try { context.getPackageManager().getPackageInfo(packageName, 0); return true; } catch (PackageManager.NameNotFoundException e) { return false; } } private boolean isFairphoneMomentsAvailable(Context context) { return isPackageAvailable(context, SwitchConstants.PACKAGE_MOMENTS); } } Loading
device.mk +3 −0 Original line number Diff line number Diff line Loading @@ -167,6 +167,9 @@ PRODUCT_COPY_FILES += \ PRODUCT_COPY_FILES += \ $(LOCAL_PATH)/configs/permissions/privapp-permissions-fpcam.xml:$(TARGET_COPY_OUT_SYSTEM)/etc/permissions/privapp-permissions-fpcam.xml # Fairphone Moments $(call inherit-product, external/FairphoneMoments/config.mk) # GPS PRODUCT_COPY_FILES += \ frameworks/native/data/etc/android.hardware.location.gps.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.location.gps.xml Loading
parts/res/values/strings.xml +1 −0 Original line number Diff line number Diff line Loading @@ -34,6 +34,7 @@ <!-- Toast --> <string name="sensor_access">Camera & mic access</string> <string name="fairphone_moments">Fairphone Moments</string> <string name="do_not_disturb">Do Not Disturb</string> <string name="flight_mode">Airplane mode</string> <string name="torch">Torch</string> Loading
parts/res/xml/switch_preferences.xml +6 −0 Original line number Diff line number Diff line Loading @@ -14,6 +14,12 @@ android:summary="@string/sensor_privacy_summary" android:defaultValue="true" /> <org.lineageos.settings.preference.RadioButtonPreference android:key="fairphone_moments" android:title="@string/fairphone_moments_title" android:summary="@string/fairphone_moments_summary" android:defaultValue="false" /> <org.lineageos.settings.preference.RadioButtonPreference android:key="do_not_disturb" android:title="@string/do_not_disturb_title" Loading
parts/src/org/lineageos/settings/AppPrefs.java +27 −0 Original line number Diff line number Diff line Loading @@ -14,6 +14,7 @@ public class AppPrefs { private static final String KEY_FIRST_BOOT = "first_boot"; private static final String KEY_FPCAMERA = "fp_camera_pref"; private static final String KEY_CURRENT_LAUNCHER = "current_launcher"; private AppPrefs(Context context) { this.context = context.getApplicationContext(); Loading @@ -27,6 +28,19 @@ public class AppPrefs { return instance; } public String getSavedHomeApp() { return prefs.getString(KEY_CURRENT_LAUNCHER, ""); } public void saveDefaultHomeApp(String packageName) { prefs.edit().putString(KEY_CURRENT_LAUNCHER, packageName).apply(); } public void saveCurrentHomeApp() { String currentHomeApp = getDefaultHomeAppPackageName(); saveDefaultHomeApp(currentHomeApp); } public boolean getIsFirstBoot() { return prefs.getBoolean(KEY_FIRST_BOOT, true); } Loading @@ -42,4 +56,17 @@ public class AppPrefs { public void setFpCameraEnabled(boolean enabled) { prefs.edit().putBoolean(KEY_FPCAMERA, enabled).apply(); } private String getDefaultHomeAppPackageName() { Intent intent = new Intent(Intent.ACTION_MAIN); intent.addCategory(Intent.CATEGORY_HOME); ResolveInfo resolveInfo = context.getPackageManager() .resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY); if (resolveInfo != null && resolveInfo.activityInfo != null) { return resolveInfo.activityInfo.packageName; } return ""; } }
parts/src/org/lineageos/settings/switchcust/LauncherSwitcherService.java 0 → 100644 +256 −0 Original line number Diff line number Diff line package org.lineageos.settings.switchcust; import android.app.ActivityManager; import android.app.role.RoleManager; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.os.UserHandle; import android.util.Log; import androidx.core.content.ContextCompat; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import org.lineageos.settings.AppPrefs; public class LauncherSwitcherService { private static final String TAG = "LauncherSwitcherService"; /** Callback interface for launcher switching operations */ public interface LauncherSwitchCallback { void onSuccess(); void onError(Exception exception); } public void switchToUserLauncher( Context context, LauncherSwitchCallback callback) { Executor executor = ContextCompat.getMainExecutor(context); executor.execute( () -> { try { AppPrefs appPrefs = AppPrefs.getInstance(context); String userLauncherApp = appPrefs.getSavedHomeApp(); boolean shouldShowOverlayAnimation = shouldShowOverlayAnimation(context); if (!isPackageAvailable(context, userLauncherApp)) { userLauncherApp = SwitchConstants.PACKAGE_MOMENTS; } // Create a final variable for use in lambda final String finalUserLauncherApp = userLauncherApp; setDefaultHomeAppAsync(context, finalUserLauncherApp) .thenAccept( success -> { if (success) { startSwitchStateChangeActivity( context, false, shouldShowOverlayAnimation); callback.onSuccess(); } else { callback.onError( new Exception( "Failed to set default launcher")); } }) .exceptionally( throwable -> { String fallbackLauncher = SwitchConstants.PACKAGE_BLISS; Log.e( TAG, "Exception occurred: " + throwable.getMessage()); callback.onError( new Exception( "Async operation failed", throwable)); return null; }); } catch (Exception e) { callback.onError(e); } }); } public void switchToFairphoneMoments( Context context, LauncherSwitchCallback callback) { Executor executor = ContextCompat.getMainExecutor(context); executor.execute( () -> { try { if (!isFairphoneMomentsAvailable(context)) { callback.onError(new Exception("Fairphone Moments is not available")); return; } AppPrefs appPrefs = AppPrefs.getInstance(context); String currentHomeApp = getDefaultHomeAppPackageName(context); appPrefs.saveDefaultHomeApp(currentHomeApp); boolean shouldShowOverlayAnimation = shouldShowOverlayAnimation(context); setDefaultHomeAppAsync(context, SwitchConstants.PACKAGE_MOMENTS) .thenAccept( success -> { if (success) { startSwitchStateChangeActivity( context, true, shouldShowOverlayAnimation); callback.onSuccess(); } else { callback.onError( new Exception( "Failed to set default launcher")); } }) .exceptionally( throwable -> { callback.onError( new Exception( "Eror setting default launcher", throwable)); return null; }); } catch (Exception e) { callback.onError(e); } }); } private CompletableFuture<Boolean> setDefaultHomeAppAsync( Context context, String packageName) { CompletableFuture<Boolean> future = new CompletableFuture<>(); Log.d(TAG, "Setting default home app: " + packageName); try { RoleManager roleManager = getRoleManager(context); if (roleManager == null) { future.complete(false); return future; } int foregroundUser = ActivityManager.getCurrentUser(); roleManager.addRoleHolderAsUser( RoleManager.ROLE_HOME, packageName, 0, UserHandle.of(foregroundUser), ContextCompat.getMainExecutor(context), future::complete); } catch (Exception e) { Log.e(TAG, "Error setting " + packageName + " as default home app", e); future.complete(false); } return future; } private String getDefaultHomeAppPackageName(Context context) { RoleManager roleManager = getRoleManager(context); if (roleManager == null) { throw new IllegalStateException("RoleManager is not available"); } String packageName = roleManager.getDefaultApplication(RoleManager.ROLE_HOME); Log.d(TAG, "Default home app package name: " + packageName); if (packageName == null) { throw new IllegalStateException("Could not get default home app package name"); } if (SwitchConstants.PACKAGE_MOMENTS.equals(packageName)) { return SwitchConstants.PACKAGE_BLISS; } else { return packageName; } } /** Start switch state change activity in detox launcher to display overlay. */ private void startSwitchStateChangeActivity( Context context, boolean detoxEnabled, boolean showOverlay) { Intent intent = new Intent(SwitchConstants.ACTION_SHOW_SWITCH_BUTTON_HINT); intent.putExtra( SwitchConstants.EXTRA_SWITCH_BUTTON_STATE, detoxEnabled ? SwitchConstants.EXTRA_SWITCH_BUTTON_STATE_ENABLED : SwitchConstants.EXTRA_SWITCH_BUTTON_STATE_DISABLED); intent.putExtra(SwitchConstants.EXTRA_SHOW_OVERLAY, showOverlay); intent.setPackage(SwitchConstants.PACKAGE_MOMENTS); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); context.startActivityAsUser(intent, UserHandle.SYSTEM); } /** * Detox Launcher should display an overlay animation when user is interacting with any app, or * what is the same: it should NOT display an overlay animation when user is interacting with * stock launcher or spring launcher */ private boolean shouldShowOverlayAnimation(Context context) { ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); if (am == null) { return true; } try { // Get the foreground task List<ActivityManager.RunningTaskInfo> runningTasks = am.getRunningTasks(1); String defaultHomeAppPackage = getDefaultHomeAppPackageName(context); if (runningTasks != null && !runningTasks.isEmpty()) { ActivityManager.RunningTaskInfo task = runningTasks.get(0); if (task != null && task.topActivity != null) { String topPackageName = task.topActivity.getPackageName(); String topClassName = task.topActivity.getClassName(); Log.d(TAG, "Top activity package: " + topPackageName); Log.d(TAG, "Top activity name: " + topClassName); Log.d(TAG, "Default home app package: " + defaultHomeAppPackage); if (defaultHomeAppPackage.equals(topPackageName)) { return false; } if (SwitchConstants.PACKAGE_MOMENTS.equals(topPackageName) && SwitchConstants.FAIRPHONE_MOMENTS_HOME_ACTIVITY.equals( topClassName)) { return false; } return true; } } } catch (SecurityException e) { Log.e(TAG, "Permission needed for getRunningTasks", e); } catch (Exception e) { Log.e(TAG, "Error checking overlay animation requirement", e); } return true; } private RoleManager getRoleManager(Context context) { try { return context.getSystemService(RoleManager.class); } catch (Exception e) { Log.e(TAG, "Failed to get RoleManager", e); return null; } } private boolean isPackageAvailable(Context context, String packageName) { try { context.getPackageManager().getPackageInfo(packageName, 0); return true; } catch (PackageManager.NameNotFoundException e) { return false; } } private boolean isFairphoneMomentsAvailable(Context context) { return isPackageAvailable(context, SwitchConstants.PACKAGE_MOMENTS); } }