From da596484c1463eb913b5dd1c6ec7b056c3515812 Mon Sep 17 00:00:00 2001 From: Tofik Sonono Date: Sun, 24 Nov 2024 20:14:31 +0100 Subject: [PATCH 1/4] Initial support for Android 15 Private space This has the basic functionality for a useable Private Space interaction. What is missing is the "separate launcher container for apps installed in the private space", as well as the "an "Install Apps" button that launches an implicit intent to install apps into the user's private space". --- app/build.gradle | 4 +- app/src/main/AndroidManifest.xml | 3 + .../java/fr/neamar/kiss/MainActivity.java | 93 ++++++++++++++++++- app/src/main/res/menu/menu_main.xml | 4 + app/src/main/res/values/strings.xml | 1 + 5 files changed, 102 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index df92305c90..73c62f085b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,7 +14,7 @@ plugins { android { defaultConfig { - compileSdk 34 + compileSdk 35 } buildFeatures { buildConfig = true @@ -22,7 +22,7 @@ android { defaultConfig { applicationId 'fr.neamar.kiss' minSdkVersion 15 - targetSdkVersion 34 + targetSdkVersion 35 versionCode 211 versionName "3.21.3" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index aa3cc6d41c..ab8ba4686a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -17,6 +17,8 @@ --> + + + diff --git a/app/src/main/java/fr/neamar/kiss/MainActivity.java b/app/src/main/java/fr/neamar/kiss/MainActivity.java index 01cac3ed56..02b797884e 100644 --- a/app/src/main/java/fr/neamar/kiss/MainActivity.java +++ b/app/src/main/java/fr/neamar/kiss/MainActivity.java @@ -11,6 +11,8 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.LauncherApps; +import android.content.pm.LauncherUserInfo; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; @@ -19,6 +21,8 @@ import android.graphics.Rect; import android.os.Build; import android.os.Bundle; +import android.os.UserHandle; +import android.os.UserManager; import android.preference.PreferenceManager; import android.provider.Settings; import android.text.Editable; @@ -44,8 +48,11 @@ import android.widget.TextView.OnEditorActionListener; import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; import java.util.ArrayList; +import java.util.List; +import java.util.Objects; import fr.neamar.kiss.adapter.RecordAdapter; import fr.neamar.kiss.broadcast.IncomingCallHandler; @@ -213,6 +220,11 @@ public void onReceive(Context context, Intent intent) { // Run GC once to free all the garbage accumulated during provider initialization System.gc(); + } else if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { + if (intent.getAction().equalsIgnoreCase(Intent.ACTION_PROFILE_AVAILABLE) + || intent.getAction().equalsIgnoreCase(Intent.ACTION_PROFILE_UNAVAILABLE)) { + privateSpaceStateEvent(intent.getParcelableExtra(Intent.EXTRA_USER, UserHandle.class)); + } } // New provider might mean new favorites @@ -228,6 +240,12 @@ public void onReceive(Context context, Intent intent) { this.registerReceiver(mReceiver, intentFilterLoad, Context.RECEIVER_EXPORTED); this.registerReceiver(mReceiver, intentFilterLoadOver, Context.RECEIVER_EXPORTED); this.registerReceiver(mReceiver, intentFilterFullLoadOver, Context.RECEIVER_EXPORTED); + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { + IntentFilter intentFilterProfileAvailable = new IntentFilter(Intent.ACTION_PROFILE_AVAILABLE); + IntentFilter intentFilterProfileUnAvailable = new IntentFilter(Intent.ACTION_PROFILE_UNAVAILABLE); + this.registerReceiver(mReceiver, intentFilterProfileAvailable, Context.RECEIVER_EXPORTED); + this.registerReceiver(mReceiver, intentFilterProfileUnAvailable, Context.RECEIVER_EXPORTED); + } } else { this.registerReceiver(mReceiver, intentFilterLoad); @@ -402,6 +420,19 @@ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMen super.onCreateContextMenu(menu, v, menuInfo); MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.menu_main, menu); + + MenuItem privateSpaceItem = menu.findItem(R.id.private_space); + if (privateSpaceItem != null) { + if ((android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) + || (getPrivateUser() == null)) { + privateSpaceItem.setVisible(false); + } else if (isPrivateSpaceUnlocked()) { + privateSpaceItem.setTitle("Lock Private Space"); + } else { + privateSpaceItem.setTitle("Unlock Private Space"); + } + } + forwarderManager.onCreateContextMenu(menu); } @@ -557,6 +588,9 @@ public boolean onOptionsItemSelected(MenuItem item) { } else if (itemId == R.id.preferences) { startActivity(new Intent(this, SettingsActivity.class)); return true; + } else if (itemId == R.id.private_space) { + switchPrivateSpaceState(); + return true; } return super.onOptionsItemSelected(item); } @@ -566,7 +600,6 @@ public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.menu_main, menu); - return true; } @@ -689,6 +722,64 @@ public void onAnimationEnd(Animator animation) { } } + @RequiresApi(35) + private UserHandle getPrivateUser() { + final UserManager manager = (UserManager) this.getSystemService(Context.USER_SERVICE); + assert manager != null; + + final LauncherApps launcher = (LauncherApps) this.getSystemService(Context.LAUNCHER_APPS_SERVICE); + assert launcher != null; + + List users = launcher.getProfiles(); + + UserHandle privateUser = null; + for (UserHandle user : users) { + if (Objects.requireNonNull(launcher.getLauncherUserInfo(user)).getUserType().equalsIgnoreCase(UserManager.USER_TYPE_PROFILE_PRIVATE)) { + privateUser = user; + break; + } + } + return privateUser; + } + + private boolean isPrivateSpaceUnlocked() { + if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) { + return false; + } + + final UserManager manager = (UserManager) this.getSystemService(Context.USER_SERVICE); + assert manager != null; + + UserHandle user = getPrivateUser(); + return !manager.isQuietModeEnabled(user); + } + + @RequiresApi(35) + private void switchPrivateSpaceState() { + final UserManager manager = (UserManager) this.getSystemService(Context.USER_SERVICE); + assert manager != null; + + UserHandle user = getPrivateUser(); + manager.requestQuietModeEnabled(!manager.isQuietModeEnabled(user), user); + } + + @RequiresApi(35) + private void privateSpaceStateEvent(UserHandle handle) { + if (handle == null) { + return; + } + + final LauncherApps launcher = (LauncherApps) this.getSystemService(Context.LAUNCHER_APPS_SERVICE); + + LauncherUserInfo info = launcher.getLauncherUserInfo(handle); + if (info != null) { + if (info.getUserType().equalsIgnoreCase(UserManager.USER_TYPE_PROFILE_PRIVATE)) { + Log.d(TAG, "Private Space state changed"); + // TODO: Check if private space state changed and change app view accordingly + } + } + } + public void onFavoriteChange() { forwarderManager.onFavoriteChange(); } diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml index 43df5ecd4e..b868486cd2 100644 --- a/app/src/main/res/menu/menu_main.xml +++ b/app/src/main/res/menu/menu_main.xml @@ -17,5 +17,9 @@ android:id="@+id/settings" android:showAsAction="never" android:title="@string/menu_settings" /> + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d8316d8eed..f9ad500849 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -356,4 +356,5 @@ Tagged result sort mode Select how results should be sorted when shown by tag Tags + Private Space \ No newline at end of file From 3420b8f6399121a8aba3ac03b3efbc98021a5453 Mon Sep 17 00:00:00 2001 From: Tofik Sonono Date: Mon, 25 Nov 2024 18:18:16 +0100 Subject: [PATCH 2/4] Hide apps from private space when it is locked --- .../fr/neamar/kiss/loader/LoadAppPojos.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/fr/neamar/kiss/loader/LoadAppPojos.java b/app/src/main/java/fr/neamar/kiss/loader/LoadAppPojos.java index 2e26f40182..5c1466c2cc 100644 --- a/app/src/main/java/fr/neamar/kiss/loader/LoadAppPojos.java +++ b/app/src/main/java/fr/neamar/kiss/loader/LoadAppPojos.java @@ -5,6 +5,7 @@ import android.content.pm.ApplicationInfo; import android.content.pm.LauncherActivityInfo; import android.content.pm.LauncherApps; +import android.content.pm.LauncherUserInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.os.Build; @@ -55,6 +56,10 @@ protected List doInBackground(Void... params) { // Handle multi-profile support introduced in Android 5 (#542) for (android.os.UserHandle profile : manager.getUserProfiles()) { + LauncherUserInfo info = null; + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { + info = launcherApps.getLauncherUserInfo(profile); + } UserHandle user = new UserHandle(manager.getSerialNumberForUser(profile), profile); for (LauncherActivityInfo activityInfo : launcherApps.getActivityList(null, profile)) { if (isCancelled()) { @@ -63,7 +68,18 @@ protected List doInBackground(Void... params) { ApplicationInfo appInfo = activityInfo.getApplicationInfo(); boolean disabled = PackageManagerUtils.isAppSuspended(appInfo) || isQuietModeEnabled(manager, profile); final AppPojo app = createPojo(user, appInfo.packageName, activityInfo.getName(), activityInfo.getLabel(), disabled, excludedAppList, excludedFromHistoryAppList, excludedShortcutsAppList); - apps.add(app); + if ((android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) + && (info != null)) { + if (!info.getUserType().equalsIgnoreCase(UserManager.USER_TYPE_PROFILE_PRIVATE)) { + apps.add(app); + } else { + if (!isQuietModeEnabled(manager, profile)) { + apps.add(app); + } + } + } else { + apps.add(app); + } } } } else { From 386f7a6cea8d9410a4589a60625c4a57cafdb7aa Mon Sep 17 00:00:00 2001 From: Tofik Sonono Date: Mon, 25 Nov 2024 19:19:24 +0100 Subject: [PATCH 3/4] Ability to disable Private Space shortcuts They are disabled by default. --- .../java/fr/neamar/kiss/SettingsActivity.java | 2 + .../kiss/loader/LoadShortcutsPojos.java | 44 ++++++++++++++++++- app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/preferences.xml | 4 ++ 4 files changed, 50 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/fr/neamar/kiss/SettingsActivity.java b/app/src/main/java/fr/neamar/kiss/SettingsActivity.java index ea7b3bd10c..16727e6c59 100644 --- a/app/src/main/java/fr/neamar/kiss/SettingsActivity.java +++ b/app/src/main/java/fr/neamar/kiss/SettingsActivity.java @@ -672,6 +672,8 @@ public void onDenied() { } } else if ("selected-contact-mime-types".equals(key)) { getDataHandler().reloadContactsProvider(); + } else if ("disable-private-space-shortcuts".equals(key)) { + getDataHandler().reloadShortcuts(); } } diff --git a/app/src/main/java/fr/neamar/kiss/loader/LoadShortcutsPojos.java b/app/src/main/java/fr/neamar/kiss/loader/LoadShortcutsPojos.java index 27db84c761..e755c470cd 100644 --- a/app/src/main/java/fr/neamar/kiss/loader/LoadShortcutsPojos.java +++ b/app/src/main/java/fr/neamar/kiss/loader/LoadShortcutsPojos.java @@ -2,9 +2,15 @@ package fr.neamar.kiss.loader; import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.LauncherApps; +import android.content.pm.LauncherUserInfo; import android.content.pm.ShortcutInfo; import android.os.Build; import android.os.UserManager; +import android.preference.PreferenceManager; + +import androidx.annotation.RequiresApi; import java.util.ArrayList; import java.util.List; @@ -33,6 +39,8 @@ protected List doInBackground(Void... params) { return new ArrayList<>(); } + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + List records = DBHelper.getShortcuts(context); DataHandler dataHandler = KissApplication.getApplication(context).getDataHandler(); TagsHandler tagsHandler = dataHandler.getTagsHandler(); @@ -52,8 +60,13 @@ protected List doInBackground(Void... params) { // get all oreo shortcuts from system directly if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE); + LauncherApps launcherApps = (LauncherApps) context.getSystemService(Context.LAUNCHER_APPS_SERVICE); List shortcutInfos = ShortcutUtil.getAllShortcuts(context); for (ShortcutInfo shortcutInfo : shortcutInfos) { + LauncherUserInfo info = null; + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { + info = launcherApps.getLauncherUserInfo(shortcutInfo.getUserHandle()); + } if (isCancelled()) { break; } @@ -62,7 +75,16 @@ protected List doInBackground(Void... params) { if (shortcutRecord != null) { boolean disabled = PackageManagerUtils.isAppSuspended(context, shortcutInfo.getPackage(), new UserHandle(context, shortcutInfo.getUserHandle())) || userManager.isQuietModeEnabled(shortcutInfo.getUserHandle()); ShortcutPojo pojo = createPojo(shortcutRecord, tagsHandler, ShortcutUtil.getComponentName(context, shortcutInfo), shortcutInfo.isPinned(), shortcutInfo.isDynamic(), disabled); - pojos.add(pojo); + if ((android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) + && (info != null)) { + boolean privateSpaceShortcutsDisabled = prefs.getBoolean("disable-private-space-shortcuts", true); + if (shouldAddShortcut(userManager, info, shortcutInfo.getUserHandle(), + privateSpaceShortcutsDisabled)) { + pojos.add(pojo); + } + } else { + pojos.add(pojo); + } } } } @@ -71,10 +93,30 @@ protected List doInBackground(Void... params) { return pojos; } + @RequiresApi(35) + private boolean shouldAddShortcut(UserManager manager, LauncherUserInfo info, + android.os.UserHandle profile, boolean privateSpaceShortcutsDisabled) { + if (!info.getUserType().equalsIgnoreCase(UserManager.USER_TYPE_PROFILE_PRIVATE)) { + return true; + } else { + if (privateSpaceShortcutsDisabled) { + return false; + } + else return !isQuietModeEnabled(manager, profile); + } + } + private ShortcutPojo createPojo(ShortcutRecord shortcutRecord, TagsHandler tagsHandler, String componentName, boolean pinned, boolean dynamic, boolean disabled) { ShortcutPojo pojo = new ShortcutPojo(shortcutRecord, componentName, pinned, dynamic, disabled); pojo.setName(shortcutRecord.name); pojo.setTags(tagsHandler.getTags(pojo.id)); return pojo; } + + private boolean isQuietModeEnabled(UserManager manager, android.os.UserHandle profile) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return manager.isQuietModeEnabled(profile); + } + return false; + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f9ad500849..2e45353653 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -357,4 +357,5 @@ Select how results should be sorted when shown by tag Tags Private Space + Disable Private Space app shortcuts \ No newline at end of file diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 9ff1876266..1f02981044 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -408,6 +408,10 @@ android:defaultValue="false" android:key="call-contact-on-click" android:title="@string/contacts_call_on_click" /> + Date: Sat, 25 Jan 2025 10:17:42 +0100 Subject: [PATCH 4/4] Opt out of edge-to-edge Edge-to-edge is the default behavior in Android 15. It however breaks the app currently. --- app/src/main/res/values-v35/themes.xml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 app/src/main/res/values-v35/themes.xml diff --git a/app/src/main/res/values-v35/themes.xml b/app/src/main/res/values-v35/themes.xml new file mode 100644 index 0000000000..e84ae5d507 --- /dev/null +++ b/app/src/main/res/values-v35/themes.xml @@ -0,0 +1,15 @@ + + + + + + + + \ No newline at end of file