From 74df370c8893f0919214dfc32940f94e4c141f77 Mon Sep 17 00:00:00 2001 From: Kieron Quinn Date: Mon, 23 Dec 2024 20:56:19 +0000 Subject: [PATCH] - Experimental RemoteViews support. This allows any Target to make a custom layout, much like how widgets work. Google quietly added support for this to Native Smartspace in Android 15 QPR2 and Android 16 DP1, Smartspacer now supports it on Native Smartspace on those versions, and everywhere else on all versions (third party apps using the Smartspacer SDK may need updating) (#281) - New Widget Target, using RemoteViews support, allowing you to put 4x1 widgets in Smartspace. There are a number of limitations for this, see the app for more info. - New Clock Target, which is the same as the Clock Complication but as a Target. - Added option to hide add button on Expanded Smartspace when locked. - Fixed vertical widget on Android 16. - Support for adding widgets from work profile (requires widget's app to be installed on the main profile too due to system limitations). SDK version 1.1(.1): - Support for RemoteViews in client & dependency updates. --- app/build.gradle | 34 ++--- app/release/baselineProfiles/0/app-release.dm | Bin 3775 -> 12802 bytes app/release/baselineProfiles/1/app-release.dm | Bin 3689 -> 12710 bytes app/release/output-metadata.json | 4 +- .../repositories/AlarmRepositoryTests.kt | 1 + .../repositories/SmartspaceRepositoryTests.kt | 11 +- app/src/main/AndroidManifest.xml | 37 +++++ .../app/smartspacer/Smartspacer.kt | 11 +- .../components/notifications/Notifications.kt | 12 +- .../smartspace/BaseSmartspacerSession.kt | 5 +- .../smartspace/ClientSmartspacerSession.kt | 34 ++++- .../smartspace/ExpandedSmartspacerSession.kt | 17 ++- .../GlanceableHubSmartspacerSession.kt | 8 +- .../smartspace/MediaDataSmartspacerSession.kt | 8 +- .../NotificationSmartspacerSession.kt | 4 + .../smartspace/SystemSmartspacerSession.kt | 9 +- .../smartspace/WidgetSmartspacerSession.kt | 3 +- .../broadcasts/TimeChangedBroadcast.kt | 3 + .../smartspace/compat/TargetMerger.kt | 20 ++- .../smartspace/targets/TimeTarget.kt | 80 +++++++++++ .../smartspace/targets/WidgetTarget.kt | 129 ++++++++++++++++++ .../smartspace/widgets/WidgetWidget.kt | 41 ++++++ .../smartspacer/model/database/TargetData.kt | 2 +- .../receivers/WidgetListClickReceiver.kt | 46 +++++++ .../repositories/AlarmRepository.kt | 39 +++--- .../repositories/AppWidgetRepository.kt | 23 ++++ .../repositories/BaseSettingsRepository.kt | 55 ++++---- .../BatteryOptimisationRepository.kt | 3 +- .../repositories/CompatibilityRepository.kt | 13 ++ .../repositories/ExpandedRepository.kt | 6 +- .../repositories/SmartspaceRepository.kt | 12 +- .../SmartspacerSettingsRepository.kt | 1 + .../repositories/WidgetRepository.kt | 30 +++- .../service/SmartspacerShizukuService.kt | 7 +- .../service/SmartspacerSmartspaceService.kt | 6 + .../configuration/ConfigurationActivity.kt | 8 ++ .../WidgetTargetConfigurationFragment.kt | 122 +++++++++++++++++ .../WidgetTargetConfigurationViewModel.kt | 70 ++++++++++ .../setup/WidgetTargetSetupFragment.kt | 42 ++++++ .../setup/WidgetTargetSetupViewModel.kt | 50 +++++++ .../ui/screens/expanded/ExpandedFragment.kt | 4 +- .../ui/screens/expanded/ExpandedSession.kt | 7 +- ...BaseExpandedAddWidgetBottomSheetAdapter.kt | 10 +- .../ExpandedAddWidgetBottomSheetAdapter.kt | 3 + .../ExpandedAddWidgetBottomSheetFragment.kt | 11 +- ...dedAddWidgetBottomSheetViewPagerAdapter.kt | 3 + .../appwidget/HeadlessAppWidgetHostView.kt | 6 +- .../ui/views/smartspace/SmartspaceView.kt | 41 ++++-- .../features/BaseFeatureSmartspaceView.kt | 20 ++- .../features/WeatherFeatureSmartspaceView.kt | 3 +- .../remoteviews/RemoteViewsSmartspaceView.kt | 48 +++++++ .../templates/BaseTemplateSmartspaceView.kt | 5 +- .../WeatherTemplateSmartspaceView.kt | 3 +- .../extensions/Extensions+AppWidgetManager.kt | 10 ++ .../Extensions+AppWidgetProviderInfo.kt | 19 +++ .../utils/extensions/Extensions+Bitmap.kt | 2 +- .../utils/extensions/Extensions+Build.kt | 10 +- .../extensions/Extensions+ContentResolver.kt | 1 + .../extensions/Extensions+PackageManager.kt | 6 +- .../extensions/Extensions+RemoteResponse.kt | 8 +- .../extensions/Extensions+RemoteViews.kt | 128 +++++++++++++++-- .../extensions/Extensions+SmartspaceTarget.kt | 12 +- .../glide/SystemIconShapeTransformation.kt | 2 +- .../smartspacer/utils/gmail/GmailContract.kt | 14 +- .../remoteviews/FlagDisabledRemoteViews.kt | 14 +- .../utils/remoteviews/RemoteAdapter.kt | 4 +- .../RemoteCollectionItemsWrapper.kt | 4 +- .../drawable/remoteviews_rounded_corners.xml | 4 + ...gment_expanded_bottom_sheet_add_widget.xml | 8 +- .../remoteviews_wrapper_padding_disabled.xml | 8 ++ .../remoteviews_wrapper_padding_large.xml | 9 ++ .../remoteviews_wrapper_padding_medium.xml | 9 ++ .../remoteviews_wrapper_padding_none.xml | 8 ++ .../remoteviews_wrapper_padding_small.xml | 9 ++ .../layout/remoteviews_wrapper_rounded.xml | 11 ++ .../layout/smartspace_view_remoteviews.xml | 12 ++ .../nav_graph_configure_target_widget.xml | 11 ++ .../nav_graph_setup_target_widget.xml | 11 ++ app/src/main/res/values-af-rZA/strings.xml | 1 - app/src/main/res/values-ar-rSA/strings.xml | 1 - app/src/main/res/values-ca-rES/strings.xml | 1 - app/src/main/res/values-cs-rCZ/strings.xml | 1 - app/src/main/res/values-da-rDK/strings.xml | 1 - app/src/main/res/values-de-rDE/strings.xml | 1 - app/src/main/res/values-el-rGR/strings.xml | 1 - app/src/main/res/values-en-rGB/strings.xml | 1 - app/src/main/res/values-en-rUS/strings.xml | 1 - app/src/main/res/values-es-rES/strings.xml | 1 - app/src/main/res/values-fi-rFI/strings.xml | 1 - app/src/main/res/values-fr-rFR/strings.xml | 7 +- app/src/main/res/values-hu-rHU/strings.xml | 1 - app/src/main/res/values-in-rID/strings.xml | 1 - app/src/main/res/values-it-rIT/strings.xml | 11 +- app/src/main/res/values-iw-rIL/strings.xml | 1 - app/src/main/res/values-ja-rJP/strings.xml | 1 - app/src/main/res/values-ko-rKR/strings.xml | 1 - app/src/main/res/values-nl-rNL/strings.xml | 1 - app/src/main/res/values-no-rNO/strings.xml | 1 - app/src/main/res/values-pl-rPL/strings.xml | 1 - app/src/main/res/values-pt-rBR/strings.xml | 1 - app/src/main/res/values-pt-rPT/strings.xml | 1 - app/src/main/res/values-ro-rRO/strings.xml | 1 - app/src/main/res/values-ru-rRU/strings.xml | 1 - app/src/main/res/values-sr-rSP/strings.xml | 1 - app/src/main/res/values-sv-rSE/strings.xml | 1 - app/src/main/res/values-tr-rTR/strings.xml | 5 +- app/src/main/res/values-uk-rUA/strings.xml | 1 - app/src/main/res/values-vi-rVN/strings.xml | 1 - app/src/main/res/values-zh-rCN/strings.xml | 1 - app/src/main/res/values-zh-rTW/strings.xml | 1 - app/src/main/res/values/arrays.xml | 10 ++ app/src/main/res/values/strings.xml | 43 +++++- build.gradle | 10 +- gradle/wrapper/gradle-wrapper.properties | 4 +- sdk-client/build.gradle | 19 +-- .../sdk/client/SmartspacerClient.kt | 11 +- .../client/utils/Extensions+RemoteViews.kt | 16 +++ .../sdk/client/views/BcSmartspaceView.kt | 9 +- .../sdk/client/views/SmartspacerView.kt | 54 ++++---- .../SmartspacerRemoteViewsPageView.kt | 37 +++++ .../res/layout/remoteviews_wrapper_dark.xml | 9 ++ .../res/layout/remoteviews_wrapper_light.xml | 9 ++ .../layout/smartspace_page_remoteviews.xml | 11 ++ sdk-client/src/main/res/values/styles.xml | 8 ++ sdk-core/build.gradle | 6 +- .../smartspacer/sdk/model/SmartspaceConfig.kt | 14 +- .../smartspacer/sdk/model/SmartspaceTarget.kt | 18 ++- .../smartspacer/sdk/utils/TargetTemplate.kt | 23 ++++ sdk-plugin/build.gradle | 8 +- sdk-sample/build.gradle | 10 +- settings.gradle | 1 + .../android/widget/RemoteViewsHidden.java | 12 ++ 132 files changed, 1624 insertions(+), 274 deletions(-) create mode 100644 app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/targets/TimeTarget.kt create mode 100644 app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/targets/WidgetTarget.kt create mode 100644 app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/widgets/WidgetWidget.kt create mode 100644 app/src/main/java/com/kieronquinn/app/smartspacer/receivers/WidgetListClickReceiver.kt create mode 100644 app/src/main/java/com/kieronquinn/app/smartspacer/ui/screens/configuration/widgettarget/configuration/WidgetTargetConfigurationFragment.kt create mode 100644 app/src/main/java/com/kieronquinn/app/smartspacer/ui/screens/configuration/widgettarget/configuration/WidgetTargetConfigurationViewModel.kt create mode 100644 app/src/main/java/com/kieronquinn/app/smartspacer/ui/screens/configuration/widgettarget/setup/WidgetTargetSetupFragment.kt create mode 100644 app/src/main/java/com/kieronquinn/app/smartspacer/ui/screens/configuration/widgettarget/setup/WidgetTargetSetupViewModel.kt create mode 100644 app/src/main/java/com/kieronquinn/app/smartspacer/ui/views/smartspace/remoteviews/RemoteViewsSmartspaceView.kt create mode 100644 app/src/main/res/drawable/remoteviews_rounded_corners.xml create mode 100644 app/src/main/res/layout/remoteviews_wrapper_padding_disabled.xml create mode 100644 app/src/main/res/layout/remoteviews_wrapper_padding_large.xml create mode 100644 app/src/main/res/layout/remoteviews_wrapper_padding_medium.xml create mode 100644 app/src/main/res/layout/remoteviews_wrapper_padding_none.xml create mode 100644 app/src/main/res/layout/remoteviews_wrapper_padding_small.xml create mode 100644 app/src/main/res/layout/remoteviews_wrapper_rounded.xml create mode 100644 app/src/main/res/layout/smartspace_view_remoteviews.xml create mode 100644 app/src/main/res/navigation/nav_graph_configure_target_widget.xml create mode 100644 app/src/main/res/navigation/nav_graph_setup_target_widget.xml create mode 100644 sdk-client/src/main/java/com/kieronquinn/app/smartspacer/sdk/client/utils/Extensions+RemoteViews.kt create mode 100644 sdk-client/src/main/java/com/kieronquinn/app/smartspacer/sdk/client/views/remoteviews/SmartspacerRemoteViewsPageView.kt create mode 100644 sdk-client/src/main/res/layout/remoteviews_wrapper_dark.xml create mode 100644 sdk-client/src/main/res/layout/remoteviews_wrapper_light.xml create mode 100644 sdk-client/src/main/res/layout/smartspace_page_remoteviews.xml diff --git a/app/build.gradle b/app/build.gradle index c359eed0..28e62dc5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,11 +13,11 @@ plugins { apply plugin: 'com.google.android.gms.oss-licenses-plugin' -def tagName = '1.8.2' -def tagCode = 182 +def tagName = '1.9' +def tagCode = 190 android { - compileSdk 34 + compileSdk 35 defaultConfig { applicationId "com.kieronquinn.app.smartspacer" @@ -70,18 +70,18 @@ android { } dependencies { - implementation 'androidx.core:core-ktx:1.13.1' + implementation 'androidx.core:core-ktx:1.15.0' implementation 'androidx.appcompat:appcompat:1.7.0' - implementation 'com.google.android.material:material:1.13.0-alpha05' - implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - implementation "androidx.lifecycle:lifecycle-service:2.8.4" + implementation 'com.google.android.material:material:1.13.0-alpha09' + implementation 'androidx.constraintlayout:constraintlayout:2.2.0' + implementation "androidx.lifecycle:lifecycle-service:2.8.7" implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" implementation "androidx.navigation:navigation-ui-ktx:$nav_version" implementation 'androidx.core:core-splashscreen:1.0.1' implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" - implementation "androidx.fragment:fragment-ktx:1.8.2" - implementation "androidx.activity:activity-ktx:1.9.1" - implementation "androidx.work:work-runtime-ktx:2.9.1" + implementation "androidx.fragment:fragment-ktx:1.8.5" + implementation "androidx.activity:activity-ktx:1.9.3" + implementation "androidx.work:work-runtime-ktx:2.10.0" implementation "androidx.core:core-remoteviews:1.1.0" implementation "androidx.security:security-crypto:1.1.0-alpha06" implementation project(path: ':proto') @@ -91,7 +91,7 @@ dependencies { implementation "androidx.room:room-ktx:$room_version" ksp "androidx.room:room-compiler:$room_version" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1" implementation(project(path: ':sdk-plugin')) { exclude group: "com.kieronquinn.smartspacer", module: "sdk-core" } @@ -124,7 +124,7 @@ dependencies { //Dependency Injection implementation "io.insert-koin:koin-android:3.5.6" - implementation 'com.jakewharton:process-phoenix:2.1.2' + implementation 'com.jakewharton:process-phoenix:3.0.0' //Image Loading implementation 'com.github.bumptech.glide:glide:4.16.0' @@ -132,7 +132,7 @@ dependencies { implementation 'com.google.android.gms:play-services-location:21.3.0' implementation 'com.google.android.gms:play-services-maps:19.0.0' - implementation 'com.google.maps.android:android-maps-utils:3.8.0' + implementation 'com.google.maps.android:android-maps-utils:3.8.2' implementation 'me.saket:better-link-movement-method:2.2.0' implementation 'com.airbnb.android:lottie:6.4.1' @@ -142,7 +142,7 @@ dependencies { implementation "com.squareup.retrofit2:retrofit:2.11.0" implementation "com.squareup.retrofit2:converter-gson:2.11.0" implementation 'androidx.palette:palette-ktx:1.0.0' - implementation "org.jetbrains.kotlin:kotlin-reflect:1.9.22" + implementation "org.jetbrains.kotlin:kotlin-reflect:2.0.21" implementation "com.tbuonomo:dotsindicator:5.0" //Autostarter provides some OEM battery checks + starting settings for disabling them @@ -159,7 +159,7 @@ dependencies { implementation 'com.google.android.gms:play-services-oss-licenses:17.1.0' //Firebase Analytics + Crashlytics - implementation(platform("com.google.firebase:firebase-bom:33.2.0")) + implementation(platform("com.google.firebase:firebase-bom:33.7.0")) implementation("com.google.firebase:firebase-crashlytics-ktx") implementation("com.google.firebase:firebase-analytics-ktx") @@ -171,11 +171,11 @@ dependencies { androidTestImplementation 'junit:junit:4.13.2' androidTestImplementation 'io.mockk:mockk-android:1.13.8' androidTestImplementation 'app.cash.turbine:turbine:0.13.0' - androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3' + androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1' androidTestImplementation 'androidx.test.ext:junit:1.2.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' androidTestImplementation 'com.google.truth:truth:1.1.3' androidTestImplementation 'androidx.test:runner:1.6.2' - androidTestUtil 'androidx.test:orchestrator:1.5.0' + androidTestUtil 'androidx.test:orchestrator:1.5.1' androidTestCompileOnly project(path: ':systemstubs') } \ No newline at end of file diff --git a/app/release/baselineProfiles/0/app-release.dm b/app/release/baselineProfiles/0/app-release.dm index 4714d1940f005e179e0219049399a4e2b5028bea..694fe320b19ac2244f08ff462f36eda01537c17e 100644 GIT binary patch literal 12802 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 diff --git a/app/release/baselineProfiles/1/app-release.dm b/app/release/baselineProfiles/1/app-release.dm index 35a2ab01bfffbf2315f524fb89a7e3beb0b68e9d..cd9b3473a7a636159fb8833d9d72eb44f6694832 100644 GIT binary patch literal 12710 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 Ar~m)} delta 3604 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 { contextMock, notificationRepositoryMock, mock(), + mock(), dataRepositoryMock, scope ) diff --git a/app/src/androidTest/java/com/kieronquinn/app/smartspacer/repositories/SmartspaceRepositoryTests.kt b/app/src/androidTest/java/com/kieronquinn/app/smartspacer/repositories/SmartspaceRepositoryTests.kt index 586f1f19..ef102292 100644 --- a/app/src/androidTest/java/com/kieronquinn/app/smartspacer/repositories/SmartspaceRepositoryTests.kt +++ b/app/src/androidTest/java/com/kieronquinn/app/smartspacer/repositories/SmartspaceRepositoryTests.kt @@ -175,7 +175,7 @@ class SmartspaceRepositoryTests: BaseTest() { contextMock, shizukuServiceRepositoryMock, systemSmartspaceRepositoryMock, - targetsRepositoryMock, + mock(), scope, Dispatchers.Main ) @@ -274,7 +274,8 @@ class SmartspaceRepositoryTests: BaseTest() { UiSurface.HOMESCREEN, doesHaveSplitSmartspace = false, isNative = false, - actionsFirst = false + actionsFirst = false, + supportsRemoteViews = false ) val mockHomeTargets = listOfNotNull(targetOne.targets, targetTwo.targets).flatten() val mockHomeActions = listOfNotNull(actionOne.actions, actionTwo.actions).flatten() @@ -295,7 +296,8 @@ class SmartspaceRepositoryTests: BaseTest() { UiSurface.LOCKSCREEN, doesHaveSplitSmartspace = false, isNative = false, - actionsFirst = false + actionsFirst = false, + supportsRemoteViews = false ) val mockLockTargets = listOfNotNull(targetOne.targets).flatten() val mockLockActions = listOfNotNull(actionOne.actions).flatten() @@ -316,7 +318,8 @@ class SmartspaceRepositoryTests: BaseTest() { UiSurface.LOCKSCREEN, doesHaveSplitSmartspace = true, isNative = false, - actionsFirst = false + actionsFirst = false, + supportsRemoteViews = false ) val splitFirstPage = split.subList(0, 1) val splitRemainder = split.subList(1, split.size) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a8167af0..175b17c0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -281,6 +281,19 @@ android:label="@string/target_flashlight_settings_title" android:targetActivity=".ui.activities.configuration.ConfigurationActivity" /> + + + + + + + + + + + + + + + + + + + (createdAtStart = true) { AlarmRepositoryImpl(get(), get(), get(), get()) } + single(createdAtStart = true) { AlarmRepositoryImpl(get(), get(), get(), get(), get()) } single(createdAtStart = true) { NotificationRepositoryImpl(get(), get(), get(), get()) } single { WallpaperRepositoryImpl(get()) } single { PackageRepositoryImpl(get()) } @@ -526,6 +530,11 @@ class Smartspacer: Application(), Configuration.Provider { viewModel { FlashlightTargetConfigurationViewModelImpl(get(), get()) } viewModel { WidgetConfigurationViewModelImpl(get(), get(), get(), get(), get()) } viewModel { SettingsLanguageViewModelImpl(get()) } + viewModel { WidgetTargetSetupViewModelImpl(get(), it.get()) } + viewModel { WidgetTargetConfigurationViewModelImpl( + get(), + it.get() + ) } } override fun attachBaseContext(base: Context) { diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/components/notifications/Notifications.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/components/notifications/Notifications.kt index 953f118b..604e4c6e 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/components/notifications/Notifications.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/components/notifications/Notifications.kt @@ -21,6 +21,7 @@ fun Context.createNotification( channel.importance ).apply { description = getString(channel.descRes) + channel.options(this) } notificationManager.createNotificationChannel(notificationChannel) return NotificationCompat.Builder(this, channel.id).apply(builder).apply { @@ -33,13 +34,15 @@ enum class NotificationChannel( val id: String, val importance: Int, val titleRes: Int, - val descRes: Int + val descRes: Int, + val options: AndroidNotificationChannel.() -> Unit = {} ) { BACKGROUND_SERVICE( "background_service", NotificationManager.IMPORTANCE_DEFAULT, R.string.notification_channel_background_service_title, - R.string.notification_channel_background_service_subtitle + R.string.notification_channel_background_service_subtitle, + options = { setShowBadge(false) } ), SAFE_MODE( "safe_mode", @@ -93,7 +96,8 @@ enum class NotificationChannel( "widget_notification", NotificationManager.IMPORTANCE_MAX, R.string.notification_channel_widget_title, - R.string.notification_channel_widget_content + R.string.notification_channel_widget_content, + options = { setShowBadge(false) } ); fun isEnabled(context: Context): Boolean { @@ -137,5 +141,5 @@ enum class NotificationId { CLOCK_COMPLICATION, BLUETOOTH_REQUIRED, FLASHLIGHT, - WIDGET_LIST + CLOCK_TARGET } \ No newline at end of file diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/BaseSmartspacerSession.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/BaseSmartspacerSession.kt index 2d352d53..5fd7ed62 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/BaseSmartspacerSession.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/BaseSmartspacerSession.kt @@ -288,7 +288,8 @@ abstract class BaseSmartspacerSession( surface, doesHaveSplitSmartspace() && settings.useSplitSmartspace, this is SystemSmartspacerSession, - settings.actionsFirst + settings.actionsFirst, + supportsRemoteViews() ) } } @@ -302,6 +303,8 @@ abstract class BaseSmartspacerSession( return targets } + open suspend fun supportsRemoteViews() = false + open fun applyActionOverrides(targets: Flow>): Flow> { return combine( expandedOpenMode.filterNotNull(), diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/ClientSmartspacerSession.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/ClientSmartspacerSession.kt index 4b4391ee..08afe8b9 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/ClientSmartspacerSession.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/ClientSmartspacerSession.kt @@ -1,5 +1,7 @@ package com.kieronquinn.app.smartspacer.components.smartspace +import android.appwidget.AppWidgetHostView +import android.appwidget.AppWidgetProviderInfo import android.content.ComponentName import android.content.Context import androidx.lifecycle.lifecycleScope @@ -17,6 +19,8 @@ import com.kieronquinn.app.smartspacer.sdk.model.uitemplatedata.TapAction import com.kieronquinn.app.smartspacer.sdk.model.uitemplatedata.Text import com.kieronquinn.app.smartspacer.sdk.utils.TargetTemplate import com.kieronquinn.app.smartspacer.ui.activities.permission.client.SmartspacerClientPermissionActivity +import com.kieronquinn.app.smartspacer.utils.extensions.copyAsRoot +import com.kieronquinn.app.smartspacer.utils.extensions.createFakeWidgetProviderInfo import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted @@ -41,6 +45,7 @@ class ClientSmartspacerSession( var lastTargets: List? = null val owner = config.packageName + private val sdkVersion = config.sdkVersion private val permissions = databaseRepository.getGrants().map { it.firstOrNull { grant -> grant.packageName == config.packageName } @@ -62,13 +67,36 @@ class ClientSmartspacerSession( permissions.filterNotNull() ) { targets, grant -> if(grant.smartspace) { - targets.map { it.page } + targets.map { it.page.prepareRemoteViews() } }else{ listOf(createPermissionTarget(grant)) } }.flowOn(Dispatchers.IO) } + /** + * Prepares Target for displaying outside of Smartspacer. RemoteViews with bitmaps behave + * strangely outside of widgets or notifications, simply cloning them isn't sufficient to + * prevent them being marked as not-root, so we force the root flag to enabled and copy. + * + * [AppWidgetHostView] (used in the client SDK) requires an [AppWidgetProviderInfo] to get the + * content description, since this isn't actually a widget we don't have one. Smartspacer can + * create a fake one, but reflection is required so the client SDK may not be able to. As a + * result, we pass one in the Target, using the already existing (but unused) widget field. + * + * If RemoteViews are not supported by this client, don't send them at all in case it + * encounters problems. + */ + private suspend fun SmartspaceTarget.prepareRemoteViews(): SmartspaceTarget { + val remoteViews = remoteViews?.takeIf { supportsRemoteViews() }?.copyAsRoot() + return copy( + remoteViews = remoteViews, + widget = widget ?: if(remoteViews != null) { + context.createFakeWidgetProviderInfo() + }else null + ) + } + private fun createPermissionTarget(grant: Grant): SmartspaceTarget = TargetTemplate.Basic( id = "permission_request", componentName = ComponentName(BuildConfig.APPLICATION_ID, "permission_request"), @@ -82,6 +110,10 @@ class ClientSmartspacerSession( override fun toSmartspacerSessionId(id: SmartspaceSessionId) = id + override suspend fun supportsRemoteViews(): Boolean { + return sdkVersion >= 2 + } + init { lifecycleScope.launch { onCreate() diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/ExpandedSmartspacerSession.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/ExpandedSmartspacerSession.kt index 8826f6a7..b999cefa 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/ExpandedSmartspacerSession.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/ExpandedSmartspacerSession.kt @@ -37,6 +37,7 @@ import com.kieronquinn.app.smartspacer.sdk.model.UiSurface import com.kieronquinn.app.smartspacer.sdk.model.expanded.ExpandedState import com.kieronquinn.app.smartspacer.sdk.model.expanded.ExpandedState.BaseShortcut import com.kieronquinn.app.smartspacer.ui.screens.expanded.ExpandedSession.Complications.Complication +import com.kieronquinn.app.smartspacer.utils.extensions.createFakeWidgetProviderInfo import com.kieronquinn.app.smartspacer.utils.extensions.getBackgroundColour import com.kieronquinn.app.smartspacer.utils.extensions.isColorDark import com.kieronquinn.app.smartspacer.utils.extensions.isDarkMode @@ -61,7 +62,7 @@ import com.kieronquinn.app.smartspacer.ui.screens.expanded.ExpandedSession.Compl @Suppress("CloseTarget") class ExpandedSmartspacerSession( - context: Context, + private val context: Context, override val sessionId: SmartspaceSessionId, private val collectInto: suspend (List) -> Unit ): BaseSmartspacerSession( @@ -251,6 +252,13 @@ class ExpandedSmartspacerSession( }.flowOn(Dispatchers.IO) } + private fun SmartspaceTarget.addFakeWidgetIfNeeded() = apply { + remoteViews?.let { + if(widget != null) return@let + widget = context.createFakeWidgetProviderInfo() + } + } + override fun applyActionOverrides(targets: Flow>): Flow> { return targets //Don't override actions in expanded mode } @@ -304,7 +312,12 @@ class ExpandedSmartspacerSession( isDark, settings.useGoogleSans ) - list.add(Item.Target(it.page, it.target?.id, extras.isNotEmpty(), applyShadow, isDark)) + list.add(Item.Target( + it.page.addFakeWidgetIfNeeded(), + it.target?.id, extras.isNotEmpty(), + applyShadow, + isDark + )) //Force a new line if any extras are being shown alongside this Target if(extras.isNotEmpty()) { list.add(Item.Spacer(isDark)) diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/GlanceableHubSmartspacerSession.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/GlanceableHubSmartspacerSession.kt index 147ad466..990d0ae4 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/GlanceableHubSmartspacerSession.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/GlanceableHubSmartspacerSession.kt @@ -24,7 +24,7 @@ import android.app.smartspace.SmartspaceTarget as SystemSmartspaceTarget * config. */ class GlanceableHubSmartspacerSession( - context: Context, + private val context: Context, config: SmartspaceConfig, override val sessionId: SystemSmartspaceSessionId, private val collectInto: suspend (SystemSmartspaceSessionId, List) -> Unit @@ -43,7 +43,7 @@ class GlanceableHubSmartspacerSession( uiSurface: Flow ): Flow> { return combine(pages, uiSurface) { p, s -> - p.map { it.page.toSystemSmartspaceTarget(s) } + p.map { it.page.toSystemSmartspaceTarget(context, s) } }.flowOn(Dispatchers.IO) } @@ -66,7 +66,9 @@ class GlanceableHubSmartspacerSession( systemSmartspaceRepository.mediaTargets.collect { collectInto.invoke( sessionId, - it.map { target -> target.toSystemSmartspaceTarget(UiSurface.GLANCEABLE_HUB) } + it.map { target -> + target.toSystemSmartspaceTarget(context, UiSurface.GLANCEABLE_HUB) + } ) } } diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/MediaDataSmartspacerSession.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/MediaDataSmartspacerSession.kt index 5454d457..88c3a7ad 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/MediaDataSmartspacerSession.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/MediaDataSmartspacerSession.kt @@ -24,7 +24,7 @@ import android.app.smartspace.SmartspaceTarget as SystemSmartspaceTarget * config. */ class MediaDataSmartspacerSession( - context: Context, + private val context: Context, config: SmartspaceConfig, override val sessionId: SystemSmartspaceSessionId, private val collectInto: suspend (SystemSmartspaceSessionId, List) -> Unit @@ -43,7 +43,7 @@ class MediaDataSmartspacerSession( uiSurface: Flow ): Flow> { return combine(pages, uiSurface) { p, s -> - p.map { it.page.toSystemSmartspaceTarget(s) } + p.map { it.page.toSystemSmartspaceTarget(context, s) } }.flowOn(Dispatchers.IO) } @@ -66,7 +66,9 @@ class MediaDataSmartspacerSession( systemSmartspaceRepository.mediaTargets.collect { collectInto.invoke( sessionId, - it.map { target -> target.toSystemSmartspaceTarget(UiSurface.MEDIA_DATA_MANAGER) } + it.map { target -> + target.toSystemSmartspaceTarget(context, UiSurface.MEDIA_DATA_MANAGER) + } ) } } diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/NotificationSmartspacerSession.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/NotificationSmartspacerSession.kt index 166d856f..6006599b 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/NotificationSmartspacerSession.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/NotificationSmartspacerSession.kt @@ -13,4 +13,8 @@ class NotificationSmartspacerSession( override val includeBasic = true + override suspend fun supportsRemoteViews(): Boolean { + return true + } + } \ No newline at end of file diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/SystemSmartspacerSession.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/SystemSmartspacerSession.kt index 918c4d04..f0e71940 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/SystemSmartspacerSession.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/SystemSmartspacerSession.kt @@ -5,6 +5,7 @@ import androidx.core.os.BuildCompat import androidx.lifecycle.lifecycleScope import com.kieronquinn.app.smartspacer.model.smartspace.TargetHolder import com.kieronquinn.app.smartspacer.repositories.CompatibilityRepository +import com.kieronquinn.app.smartspacer.repositories.CompatibilityRepository.CompatibilityReport.Companion.areRemoteViewsSupported import com.kieronquinn.app.smartspacer.repositories.CompatibilityRepository.Feature import com.kieronquinn.app.smartspacer.repositories.CompatibilityRepository.Template import com.kieronquinn.app.smartspacer.repositories.SmartspaceRepository.SmartspacePageHolder @@ -31,7 +32,7 @@ import android.app.smartspace.SmartspaceSessionId as SystemSmartspaceSessionId import android.app.smartspace.SmartspaceTarget as SystemSmartspaceTarget class SystemSmartspacerSession( - context: Context, + private val context: Context, config: SmartspaceConfig, override val sessionId: SystemSmartspaceSessionId, private val collectInto: suspend (SystemSmartspaceSessionId, List) -> Unit @@ -73,7 +74,7 @@ class SystemSmartspacerSession( uiSurface: Flow ): Flow> { return combine(pages, uiSurface) { p, s -> - p.map { it.page.toSystemSmartspaceTarget(s) } + p.map { it.page.toSystemSmartspaceTarget(context, s) } }.flowOn(Dispatchers.IO) } @@ -139,6 +140,10 @@ class SystemSmartspacerSession( return isAtLeastU() } + override suspend fun supportsRemoteViews(): Boolean { + return compatibilityRepository.getCompatibilityReports().areRemoteViewsSupported() + } + /** * Returns if the current surface supports splitting and splitting is enabled */ diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/WidgetSmartspacerSession.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/WidgetSmartspacerSession.kt index 0aecc8e5..0e04d8ee 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/WidgetSmartspacerSession.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/WidgetSmartspacerSession.kt @@ -32,7 +32,7 @@ import java.util.UUID abstract class WidgetSmartspacerSession( context: Context, - widget: AppWidget, + private val widget: AppWidget, private val config: SmartspaceConfig = widget.getConfig(), private val collectInto: suspend (AppWidget) -> Unit ): BaseSmartspacerSession(context, config, widget) { @@ -86,6 +86,7 @@ abstract class WidgetSmartspacerSession( private fun SmartspacePageHolder.toSmartspaceViewState(): Flow { val template = page.templateData + val isList = widget.listMode val basic = if(includeBasic) { SmartspaceView.fromTarget(page, config.uiSurface, true) }else null diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/broadcasts/TimeChangedBroadcast.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/broadcasts/TimeChangedBroadcast.kt index b528e0b5..11c0d4a0 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/broadcasts/TimeChangedBroadcast.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/broadcasts/TimeChangedBroadcast.kt @@ -4,8 +4,10 @@ import android.content.Intent import android.content.IntentFilter import com.kieronquinn.app.smartspacer.BuildConfig import com.kieronquinn.app.smartspacer.components.smartspace.complications.TimeComplication +import com.kieronquinn.app.smartspacer.components.smartspace.targets.TimeTarget import com.kieronquinn.app.smartspacer.sdk.provider.SmartspacerBroadcastProvider import com.kieronquinn.app.smartspacer.sdk.provider.SmartspacerComplicationProvider +import com.kieronquinn.app.smartspacer.sdk.provider.SmartspacerTargetProvider class TimeChangedBroadcast: SmartspacerBroadcastProvider() { @@ -22,6 +24,7 @@ class TimeChangedBroadcast: SmartspacerBroadcastProvider() { override fun onReceive(intent: Intent) { SmartspacerComplicationProvider.notifyChange(provideContext(), TimeComplication::class.java) + SmartspacerTargetProvider.notifyChange(provideContext(), TimeTarget::class.java) } override fun getConfig(smartspacerId: String): Config { diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/compat/TargetMerger.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/compat/TargetMerger.kt index c5158367..39b0cced 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/compat/TargetMerger.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/compat/TargetMerger.kt @@ -75,7 +75,8 @@ abstract class TargetMerger { targets: List, actions: List, openMode: ExpandedOpenMode, - actionsFirst: Boolean + actionsFirst: Boolean, + supportsRemoteViews: Boolean ): List { val uniqueTargets = makeTargetsUnique(targets) val uniqueActions = makeActionsUnique(actions) @@ -88,7 +89,8 @@ abstract class TargetMerger { return prefixedTargets + getSplitTargets(actionQueue) + uniqueTargets.mapNotNull { val pageActions = ArrayList() var target = it.target - val headerAction = if(target.canTakeHeaderAction(actionQueue.peek())){ + val canTakeHeader = target.canTakeHeaderAction(actionQueue.peek(), supportsRemoteViews) + val headerAction = if(canTakeHeader){ actionQueue.pop() }else null if(headerAction != null){ @@ -100,7 +102,9 @@ abstract class TargetMerger { target.templateData?.subtitleItem = headerAction.action.subItemInfo pageActions.add(headerAction.parent) } - val baseAction = if(target.canTakeBaseAction(it.parent, actionQueue.peek())) { + val canTakeBase = target + .canTakeBaseAction(it.parent, actionQueue.peek(), supportsRemoteViews) + val baseAction = if(canTakeBase) { actionQueue.pop() }else null if(baseAction != null) { @@ -185,8 +189,12 @@ abstract class TargetMerger { }.flatten() } - private fun SmartspaceTarget.canTakeHeaderAction(holder: SmartspaceActionHolder?): Boolean { + private fun SmartspaceTarget.canTakeHeaderAction( + holder: SmartspaceActionHolder?, + supportsRemoteViews: Boolean + ): Boolean { if(holder == null) return false //End of list + if(supportsRemoteViews && remoteViews != null) return false //Never add with RemoteViews //Override specified by target if(canTakeTwoComplications && featureType == SmartspaceTarget.FEATURE_UNDEFINED) return true //Weather target @@ -198,9 +206,11 @@ abstract class TargetMerger { private fun SmartspaceTarget.canTakeBaseAction( target: Target, - holder: SmartspaceActionHolder? + holder: SmartspaceActionHolder?, + supportsRemoteViews: Boolean ): Boolean { val action = holder?.action ?: return false //End of list + if(supportsRemoteViews && remoteViews != null) return false //Never add with RemoteViews if(target.config.disableSubComplications) return false //Disabled by user //Override specified by target if(canTakeTwoComplications && featureType == SmartspaceTarget.FEATURE_UNDEFINED) return true diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/targets/TimeTarget.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/targets/TimeTarget.kt new file mode 100644 index 00000000..5aceb3a2 --- /dev/null +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/targets/TimeTarget.kt @@ -0,0 +1,80 @@ +package com.kieronquinn.app.smartspacer.components.smartspace.targets + +import android.app.PendingIntent +import android.content.ComponentName +import android.content.Intent +import android.provider.AlarmClock +import android.text.format.DateFormat +import com.kieronquinn.app.smartspacer.R +import com.kieronquinn.app.smartspacer.components.notifications.NotificationId +import com.kieronquinn.app.smartspacer.components.smartspace.broadcasts.TimeChangedBroadcast +import com.kieronquinn.app.smartspacer.sdk.model.SmartspaceTarget +import com.kieronquinn.app.smartspacer.sdk.model.uitemplatedata.Icon +import com.kieronquinn.app.smartspacer.sdk.model.uitemplatedata.TapAction +import com.kieronquinn.app.smartspacer.sdk.model.uitemplatedata.Text +import com.kieronquinn.app.smartspacer.sdk.provider.SmartspacerTargetProvider +import com.kieronquinn.app.smartspacer.sdk.utils.TargetTemplate +import java.util.Calendar +import android.graphics.drawable.Icon as AndroidIcon + +class TimeTarget: SmartspacerTargetProvider() { + + override fun getSmartspaceTargets(smartspacerId: String): List { + return listOf( + TargetTemplate.Basic( + id = "time_at_${System.currentTimeMillis()}", + componentName = ComponentName(provideContext(), TimeTarget::class.java), + icon = Icon( + AndroidIcon.createWithResource( + provideContext(), + R.drawable.ic_requirement_time_date + ) + ), + title = Text(getTime()), + subtitle = null, + onClick = getClickAction() + ).create().apply { + canTakeTwoComplications = true + canBeDismissed = false + } + ) + } + + private fun getClickAction(): TapAction { + val intent = Intent(AlarmClock.ACTION_SHOW_ALARMS).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + val pendingIntent = PendingIntent.getActivity( + provideContext(), + NotificationId.CLOCK_TARGET.ordinal, + intent, + PendingIntent.FLAG_IMMUTABLE + ) + return TapAction(pendingIntent = pendingIntent) + } + + private fun getTime(): String { + val format = DateFormat.getTimeFormat(provideContext()) + return format.format(Calendar.getInstance().time) + } + + override fun getConfig(smartspacerId: String?): Config { + val description = if(smartspacerId != null) { + resources.getText(R.string.target_clock_description_short) + }else{ + resources.getText(R.string.target_clock_description) + } + return Config( + resources.getString(R.string.target_clock_label), + description, + AndroidIcon.createWithResource(provideContext(), R.drawable.ic_requirement_time_date), + broadcastProvider = TimeChangedBroadcast.AUTHORITY, + allowAddingMoreThanOnce = true + ) + } + + override fun onDismiss(smartspacerId: String, targetId: String): Boolean { + return false + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/targets/WidgetTarget.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/targets/WidgetTarget.kt new file mode 100644 index 00000000..8e23bdfe --- /dev/null +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/targets/WidgetTarget.kt @@ -0,0 +1,129 @@ +package com.kieronquinn.app.smartspacer.components.smartspace.targets + +import android.content.ComponentName +import android.util.SizeF +import android.widget.RemoteViews +import androidx.annotation.LayoutRes +import androidx.annotation.StringRes +import com.google.gson.annotations.SerializedName +import com.kieronquinn.app.smartspacer.R +import com.kieronquinn.app.smartspacer.components.smartspace.targets.WidgetTarget.TargetData.Padding +import com.kieronquinn.app.smartspacer.components.smartspace.widgets.WidgetWidget +import com.kieronquinn.app.smartspacer.repositories.DataRepository +import com.kieronquinn.app.smartspacer.repositories.WidgetRepository +import com.kieronquinn.app.smartspacer.sdk.model.SmartspaceTarget +import com.kieronquinn.app.smartspacer.sdk.model.uitemplatedata.Icon +import com.kieronquinn.app.smartspacer.sdk.model.uitemplatedata.Text +import com.kieronquinn.app.smartspacer.sdk.provider.SmartspacerTargetProvider +import com.kieronquinn.app.smartspacer.sdk.utils.TargetTemplate +import com.kieronquinn.app.smartspacer.ui.activities.configuration.ConfigurationActivity +import com.kieronquinn.app.smartspacer.utils.extensions.checkCompatibility +import com.kieronquinn.app.smartspacer.utils.extensions.dp +import com.kieronquinn.app.smartspacer.utils.extensions.getBestRemoteViews +import com.kieronquinn.app.smartspacer.utils.extensions.getDisplayPortraitWidth +import org.koin.android.ext.android.inject +import android.graphics.drawable.Icon as AndroidIcon + +class WidgetTarget: SmartspacerTargetProvider() { + + private val widgetRepository by inject() + private val dataRepository by inject() + + override fun getSmartspaceTargets(smartspacerId: String): List { + val remoteViews = widgetRepository.getWidgetTargetWidget(smartspacerId) + ?: return emptyList() + val data = dataRepository.getTargetData(smartspacerId, TargetData::class.java) + val padding = data?.padding ?: Padding.NONE + val size = SizeF( + provideContext().getDisplayPortraitWidth().toFloat(), + padding.height.dp.toFloat() + ) + val bestRemoteViews = remoteViews.getBestRemoteViews(provideContext(), size) + val fallback = { title: Int, content: Int -> + TargetTemplate.Basic( + id = "widget_${smartspacerId}_at_${System.currentTimeMillis()}", + componentName = ComponentName(provideContext(), WidgetTarget::class.java), + title = Text(resources.getString(title)), + subtitle = Text(resources.getString(content)), + icon = Icon(AndroidIcon.createWithResource(provideContext(), R.drawable.ic_widgets)) + ) + } + if(!remoteViews.checkCompatibility()) { + return listOf( + fallback( + R.string.target_widget_not_compatible_title, + R.string.target_widget_not_compatible_content + ).create() + ) + } + val roundedRemoteViews = if(data?.rounded == true) { + RemoteViews( + provideContext().packageName, R.layout.remoteviews_wrapper_rounded + ).apply { + removeAllViews(R.id.remoteviews_rounded_wrapper) + addView(R.id.remoteviews_rounded_wrapper, bestRemoteViews) + } + } else remoteViews + val wrappedRemoteViews = RemoteViews(provideContext().packageName, padding.layout).apply { + removeAllViews(R.id.remoteviews_padding_wrapper) + addView(R.id.remoteviews_padding_wrapper, roundedRemoteViews) + } + return listOf( + TargetTemplate.RemoteViews( + wrappedRemoteViews, + fallback( + R.string.target_widget_not_supported_title, + R.string.target_widget_not_supported_content + ) + ).create() + ) + } + + override fun onDismiss(smartspacerId: String, targetId: String): Boolean { + return false + } + + override fun getConfig(smartspacerId: String?): Config { + val data = smartspacerId?.let { dataRepository.getTargetData(it, TargetData::class.java) } + val description = if(data != null) { + resources.getString(R.string.target_widget_description_set, data.label, data.appName) + }else{ + resources.getText(R.string.target_widget_description) + } + return Config( + label = resources.getString(R.string.target_widget_title), + description = description, + icon = AndroidIcon.createWithResource(provideContext(), R.drawable.ic_widgets), + allowAddingMoreThanOnce = true, + configActivity = ConfigurationActivity.createIntent( + provideContext(), ConfigurationActivity.NavGraphMapping.TARGET_WIDGET + ), + setupActivity = ConfigurationActivity.createIntent( + provideContext(), ConfigurationActivity.NavGraphMapping.TARGET_WIDGET_SETUP + ), + widgetProvider = WidgetWidget.AUTHORITY + ) + } + + data class TargetData( + @SerializedName("provider") + val provider: String, + @SerializedName("name") + val label: String, + @SerializedName("app_name") + val appName: String, + @SerializedName("rounded") + val rounded: Boolean = true, + @SerializedName("padding") + val padding: Padding = Padding.NONE + ) { + enum class Padding(@StringRes val label: Int, @LayoutRes val layout: Int, val height: Int) { + NONE(R.string.target_widget_padding_none, R.layout.remoteviews_wrapper_padding_none, 96), + SMALL(R.string.target_widget_padding_small, R.layout.remoteviews_wrapper_padding_small, 92), + MEDIUM(R.string.target_widget_padding_medium, R.layout.remoteviews_wrapper_padding_medium, 88), + LARGE(R.string.target_widget_padding_large, R.layout.remoteviews_wrapper_padding_large, 80), + DISABLED(R.string.target_widget_padding_disable, R.layout.remoteviews_wrapper_padding_disabled, 96), + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/widgets/WidgetWidget.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/widgets/WidgetWidget.kt new file mode 100644 index 00000000..1131c91b --- /dev/null +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/components/smartspace/widgets/WidgetWidget.kt @@ -0,0 +1,41 @@ +package com.kieronquinn.app.smartspacer.components.smartspace.widgets + +import android.appwidget.AppWidgetProviderInfo +import android.content.ComponentName +import android.widget.RemoteViews +import com.kieronquinn.app.smartspacer.BuildConfig +import com.kieronquinn.app.smartspacer.components.smartspace.targets.WidgetTarget +import com.kieronquinn.app.smartspacer.repositories.DataRepository +import com.kieronquinn.app.smartspacer.repositories.WidgetRepository +import com.kieronquinn.app.smartspacer.sdk.provider.SmartspacerWidgetProvider +import com.kieronquinn.app.smartspacer.utils.extensions.dp +import org.koin.android.ext.android.inject + +class WidgetWidget: SmartspacerWidgetProvider() { + + companion object { + const val AUTHORITY = "${BuildConfig.APPLICATION_ID}.widget.widget" + } + + private val widgetRepository by inject() + private val dataRepository by inject() + + override fun onWidgetChanged(smartspacerId: String, remoteViews: RemoteViews?) { + widgetRepository.setWidgetTargetWidget(smartspacerId, remoteViews ?: return) + } + + override fun getConfig(smartspacerId: String): Config { + val widget = dataRepository.getTargetData(smartspacerId, WidgetTarget.TargetData::class.java) + ?: return Config() + return Config( + height = widget.padding.height.dp + ) + } + + override fun getAppWidgetProviderInfo(smartspacerId: String): AppWidgetProviderInfo? { + val widget = dataRepository.getTargetData(smartspacerId, WidgetTarget.TargetData::class.java) + ?.let { ComponentName.unflattenFromString(it.provider) } ?: return null + return widgetRepository.getProviders().firstOrNull { it.provider == widget } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/model/database/TargetData.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/model/database/TargetData.kt index 24513b5f..24ea1233 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/model/database/TargetData.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/model/database/TargetData.kt @@ -19,5 +19,5 @@ class TargetData( ) : Parcelable, BaseData enum class TargetDataType { - MUSIC, NOTIFICATION, CALENDAR, DEFAULT, GREETING, BLANK, DATE, FLASHLIGHT + MUSIC, NOTIFICATION, CALENDAR, DEFAULT, GREETING, BLANK, DATE, FLASHLIGHT, WIDGET } \ No newline at end of file diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/receivers/WidgetListClickReceiver.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/receivers/WidgetListClickReceiver.kt new file mode 100644 index 00000000..f5efcd63 --- /dev/null +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/receivers/WidgetListClickReceiver.kt @@ -0,0 +1,46 @@ +package com.kieronquinn.app.smartspacer.receivers + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.widget.RemoteViews.RemoteResponse +import com.kieronquinn.app.smartspacer.sdk.model.RemoteOnClickResponse +import com.kieronquinn.app.smartspacer.sdk.utils.sendSafely +import com.kieronquinn.app.smartspacer.utils.extensions.getParcelableExtraCompat +import com.kieronquinn.app.smartspacer.utils.extensions.toRemoteResponse +import com.kieronquinn.app.smartspacer.utils.extensions.verifySecurity + +class WidgetListClickReceiver: BroadcastReceiver() { + + companion object { + private const val EXTRA_INTENT = "intent" + private const val EXTRA_REMOTE_RESPONSE = "remote_response" + + fun getIntent(extra: Intent): Intent { + return Intent().apply { + putExtra(EXTRA_INTENT, extra) + } + } + + fun getIntent(extra: RemoteResponse): Intent { + return Intent().apply { + putExtra(EXTRA_REMOTE_RESPONSE, extra.toRemoteResponse().toBundle()) + } + } + } + + override fun onReceive(context: Context, intent: Intent) { + intent.verifySecurity() + val extraIntent = intent.getParcelableExtraCompat(EXTRA_INTENT, Intent::class.java) + val remoteResponse = intent + .getParcelableExtraCompat(EXTRA_REMOTE_RESPONSE, Bundle::class.java)?.let { + RemoteOnClickResponse.RemoteResponse(it) + } + when { + extraIntent != null -> context.sendBroadcast(extraIntent) + remoteResponse != null -> remoteResponse.pendingIntent?.sendSafely() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/repositories/AlarmRepository.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/repositories/AlarmRepository.kt index d0bd22f2..8ff896fb 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/repositories/AlarmRepository.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/repositories/AlarmRepository.kt @@ -106,6 +106,7 @@ class AlarmRepositoryImpl( private val context: Context, private val notificationRepository: NotificationRepository, private val calendarRepository: CalendarRepository, + private val batteryOptimisationRepository: BatteryOptimisationRepository, dataRepository: DataRepository, private val scope: CoroutineScope = MainScope() ): AlarmRepository { @@ -303,24 +304,26 @@ class AlarmRepositoryImpl( } override fun enqueueDailyUpdateReceiver() { - if(!canScheduleExactAlarm()) { - showBatteryOptimisationNotification() - return - }else{ - notificationRepository.cancelNotification(NotificationId.BATTERY_OPTIMISATION) - } - val intent = DailyUpdateAlarmReceiver.createIntent(context) - val time = LocalDate.now().atStartOfDay().plusDays(1).atZone(ZoneId.systemDefault()) - alarmManager.setExactAndAllowWhileIdle( - AlarmManager.RTC, - time.toInstant().toEpochMilli(), - PendingIntent.getBroadcast( - context, - NotificationId.DAILY_UPDATE_ALARM.ordinal, - intent, - PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + scope.launch { + if(!canScheduleExactAlarm()) { + showBatteryOptimisationNotification() + return@launch + }else{ + notificationRepository.cancelNotification(NotificationId.BATTERY_OPTIMISATION) + } + val intent = DailyUpdateAlarmReceiver.createIntent(context) + val time = LocalDate.now().atStartOfDay().plusDays(1).atZone(ZoneId.systemDefault()) + alarmManager.setExactAndAllowWhileIdle( + AlarmManager.RTC, + time.toInstant().toEpochMilli(), + PendingIntent.getBroadcast( + context, + NotificationId.DAILY_UPDATE_ALARM.ordinal, + intent, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) ) - ) + } } override fun onRequirementChanged() { @@ -378,7 +381,7 @@ class AlarmRepositoryImpl( override fun canScheduleExactAlarm(): Boolean { //Left open for if Google change their minds again and require it separate from battery - return powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID) + return batteryOptimisationRepository.areBatteryOptimisationsDisabled() } @VisibleForTesting diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/repositories/AppWidgetRepository.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/repositories/AppWidgetRepository.kt index efd8a5b9..7c9e860a 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/repositories/AppWidgetRepository.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/repositories/AppWidgetRepository.kt @@ -8,6 +8,7 @@ import android.content.Intent import android.content.res.ColorStateList import android.graphics.Color import android.graphics.Rect +import android.os.Build import android.os.DeadSystemException import android.util.TypedValue import android.view.LayoutInflater @@ -27,10 +28,12 @@ import com.kieronquinn.app.smartspacer.components.smartspace.PagedWidgetSmartspa import com.kieronquinn.app.smartspacer.components.smartspace.PagedWidgetSmartspacerSessionState import com.kieronquinn.app.smartspacer.components.smartspace.WidgetSmartspacerSession import com.kieronquinn.app.smartspacer.model.database.AppWidget +import com.kieronquinn.app.smartspacer.receivers.WidgetListClickReceiver import com.kieronquinn.app.smartspacer.receivers.WidgetPageChangeReceiver import com.kieronquinn.app.smartspacer.repositories.SmartspacerSettingsRepository.TintColour import com.kieronquinn.app.smartspacer.sdk.model.SmartspaceTargetEvent import com.kieronquinn.app.smartspacer.sdk.model.UiSurface +import com.kieronquinn.app.smartspacer.sdk.utils.applySecurity import com.kieronquinn.app.smartspacer.service.SmartspacerAccessibiltyService import com.kieronquinn.app.smartspacer.service.SmartspacerListWidgetRemoteViewsService import com.kieronquinn.app.smartspacer.ui.activities.WidgetOptionsMenuActivity @@ -41,6 +44,7 @@ import com.kieronquinn.app.smartspacer.utils.extensions.PendingIntent_MUTABLE_FL import com.kieronquinn.app.smartspacer.utils.extensions.dip import com.kieronquinn.app.smartspacer.utils.extensions.dp import com.kieronquinn.app.smartspacer.utils.extensions.firstNotNull +import com.kieronquinn.app.smartspacer.utils.extensions.isAtLeastBaklava import com.kieronquinn.app.smartspacer.utils.extensions.launch import com.kieronquinn.app.smartspacer.utils.extensions.lockscreenShowing import com.kieronquinn.app.smartspacer.utils.extensions.screenOff @@ -490,6 +494,21 @@ class AppWidgetRepositoryImpl( val remoteViews = listContainer(widget) { RemoteViews(packageName, R.layout.widget_smartspacer_list) } + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + val pendingIntentCode = listOf(widget.appWidgetId, widget.ownerPackage).hashCode() + val pendingIntent = PendingIntent.getBroadcast( + context, + pendingIntentCode, + Intent(context, WidgetListClickReceiver::class.java).apply { + applySecurity(context) + }, + PendingIntent.FLAG_MUTABLE + ) + remoteViews.setPendingIntentTemplate( + R.id.widget_list, + pendingIntent + ) + } remoteViews.setRemoteAdapter( R.id.widget_list, SmartspacerListWidgetRemoteViewsService.createIntent( @@ -578,6 +597,10 @@ class AppWidgetRepositoryImpl( } } } + if(isList && isAtLeastBaklava()) { + //Orientation is no longer supported, might be an Android bug? + return portrait + } val landscape = container { view.inflate( this, diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/repositories/BaseSettingsRepository.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/repositories/BaseSettingsRepository.kt index 705f6cbb..784c2f13 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/repositories/BaseSettingsRepository.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/repositories/BaseSettingsRepository.kt @@ -20,12 +20,17 @@ import com.kieronquinn.app.smartspacer.repositories.BaseSettingsRepositoryImpl.S import com.kieronquinn.app.smartspacer.utils.extensions.toColorOrNull import com.kieronquinn.app.smartspacer.utils.extensions.toHexString import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.MainScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onSubscription +import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import kotlin.properties.ReadWriteProperty @@ -136,6 +141,18 @@ interface BaseSettingsRepository { abstract class BaseSettingsRepositoryImpl: BaseSettingsRepository { + private val scope = MainScope() + + private val onPrefsChange = callbackFlow { + val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> + trySend(key) + } + sharedPreferences.registerOnSharedPreferenceChangeListener(listener) + awaitClose { + sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener) + } + }.shareIn(scope, SharingStarted.Eagerly) + fun boolean(key: String, default: Boolean, onChanged: MutableSharedFlow? = null) = SmartspacerSettingImpl( Boolean::class.java, @@ -392,31 +409,21 @@ abstract class BaseSettingsRepositoryImpl: BaseSettingsRepository { } } - override fun asFlow() = callbackFlow { - val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> - if(key == this@SmartspacerSettingImpl.key) { - trySend(rawSetting ?: default) - } - } - sharedPreferences.registerOnSharedPreferenceChangeListener(listener) - trySend(rawSetting ?: default) - awaitClose { - sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener) - } - }.flowOn(Dispatchers.IO) - - override fun asFlowNullable() = callbackFlow { - val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> - if(key == this@SmartspacerSettingImpl.key) { - trySend(rawSetting) - } - } - sharedPreferences.registerOnSharedPreferenceChangeListener(listener) - if(existsSync()) trySend(rawSetting) - awaitClose { - sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener) + override fun asFlow() = onPrefsChange + .onSubscription { emit(key) } + .mapNotNull { + if(it == key) { + rawSetting ?: default + }else null + }.flowOn(Dispatchers.IO) + + override fun asFlowNullable() = onPrefsChange + .onSubscription { if(exists()) emit(key) } + .mapNotNull { + if(it == key) { + rawSetting ?: default + }else null } - }.flowOn(Dispatchers.IO) override fun key(): String { return key diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/repositories/BatteryOptimisationRepository.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/repositories/BatteryOptimisationRepository.kt index b8d078a4..cf90bbaf 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/repositories/BatteryOptimisationRepository.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/repositories/BatteryOptimisationRepository.kt @@ -12,6 +12,7 @@ import com.kieronquinn.app.smartspacer.utils.extensions.getIgnoreBatteryOptimisa interface BatteryOptimisationRepository { + fun areBatteryOptimisationsDisabled(): Boolean fun getDisableBatteryOptimisationsIntent(): Intent? fun areOemOptimisationsAvailable(context: Context): Boolean fun startOemOptimisationSettings(context: Context) @@ -25,7 +26,7 @@ class BatteryOptimisationRepositoryImpl( private val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager private val autoStarter = AutoStartPermissionHelper.getInstance() - private fun areBatteryOptimisationsDisabled(): Boolean { + override fun areBatteryOptimisationsDisabled(): Boolean { return powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID) } diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/repositories/CompatibilityRepository.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/repositories/CompatibilityRepository.kt index 40179e68..af85e6d9 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/repositories/CompatibilityRepository.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/repositories/CompatibilityRepository.kt @@ -102,6 +102,14 @@ interface CompatibilityRepository { //Compatible apps must have at least one compatible Template or Feature available. return any { it.compatibility.any { compatible -> compatible.compatible } } } + + fun List.areRemoteViewsSupported(): Boolean { + //No compatible apps found + if(isEmpty()) return false + return any { it.compatibility.any { compatible -> + compatible.compatible && compatible.item == Feature.REMOTE_VIEWS + }} + } } } @@ -165,6 +173,11 @@ interface CompatibilityRepository { R.string.compatibility_feature_loyalty_content, SmartspaceTarget.FEATURE_LOYALTY_CARD ), + REMOTE_VIEWS( + "BcSmartspaceRemoteViewsCard", + R.string.compatibility_feature_remoteviews_title, + R.string.compatibility_feature_remoteviews_content + ), SHOPPING_LIST( "BcSmartspaceCardShoppingList", R.string.compatibility_feature_shopping_list_title, diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/repositories/ExpandedRepository.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/repositories/ExpandedRepository.kt index 11b62c4a..430c35e3 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/repositories/ExpandedRepository.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/repositories/ExpandedRepository.kt @@ -414,7 +414,11 @@ class ExpandedRepositoryImpl( } createWidgetPredictorSession(listener, getWidgetsForPredictor()) awaitClose { - destroyWidgetPredictorSession() + try { + destroyWidgetPredictorSession() + }catch (e: IllegalStateException) { + //Already destroyed + } } } diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/repositories/SmartspaceRepository.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/repositories/SmartspaceRepository.kt index 16610795..235ccc77 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/repositories/SmartspaceRepository.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/repositories/SmartspaceRepository.kt @@ -67,7 +67,8 @@ interface SmartspaceRepository { surface: UiSurface, doesHaveSplitSmartspace: Boolean, isNative: Boolean, - actionsFirst: Boolean + actionsFirst: Boolean, + supportsRemoteViews: Boolean ): List /** @@ -413,7 +414,8 @@ class SmartspaceRepositoryImpl( surface: UiSurface, doesHaveSplitSmartspace: Boolean, isNative: Boolean, - actionsFirst: Boolean + actionsFirst: Boolean, + supportsRemoteViews: Boolean ): List { return when { doesHaveSplitSmartspace && surface == UiSurface.LOCKSCREEN -> { @@ -421,7 +423,8 @@ class SmartspaceRepositoryImpl( targets, actions, openMode, - actionsFirst + actionsFirst, + supportsRemoteViews ) } else -> { @@ -429,7 +432,8 @@ class SmartspaceRepositoryImpl( targets, actions, openMode, - actionsFirst + actionsFirst, + supportsRemoteViews ) } } diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/repositories/SmartspacerSettingsRepository.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/repositories/SmartspacerSettingsRepository.kt index b44e02bd..a5be4c58 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/repositories/SmartspacerSettingsRepository.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/repositories/SmartspacerSettingsRepository.kt @@ -336,6 +336,7 @@ interface SmartspacerSettingsRepository { enum class ExpandedHideAddButton(@StringRes val label: Int) { NEVER(R.string.expanded_settings_hide_add_button_never), OVERLAY_ONLY(R.string.expanded_settings_hide_add_button_overlay_only), + WHEN_LOCKED(R.string.expanded_settings_hide_add_button_when_locked), ALWAYS(R.string.expanded_settings_hide_add_button_always), } diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/repositories/WidgetRepository.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/repositories/WidgetRepository.kt index e888c0a7..e1c30662 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/repositories/WidgetRepository.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/repositories/WidgetRepository.kt @@ -8,12 +8,16 @@ import android.content.Context import android.content.Intent import android.content.IntentSender import android.os.Build +import android.widget.RemoteViews import androidx.annotation.RequiresApi +import com.kieronquinn.app.smartspacer.components.smartspace.targets.WidgetTarget import com.kieronquinn.app.smartspacer.model.smartspace.Widget +import com.kieronquinn.app.smartspacer.sdk.provider.SmartspacerTargetProvider import com.kieronquinn.app.smartspacer.sdk.utils.RemoteAdapter import com.kieronquinn.app.smartspacer.sdk.utils.getClickPendingIntent import com.kieronquinn.app.smartspacer.ui.views.appwidget.HeadlessAppWidgetHostView import com.kieronquinn.app.smartspacer.ui.views.appwidget.HeadlessAppWidgetHostView.HeadlessWidgetEvent +import com.kieronquinn.app.smartspacer.utils.extensions.getAllInstalledProviders import com.kieronquinn.app.smartspacer.utils.extensions.getDisplayPortraitHeight import com.kieronquinn.app.smartspacer.utils.extensions.getDisplayPortraitWidth import com.kieronquinn.app.smartspacer.utils.extensions.replaceUriActionsWithProxy @@ -61,6 +65,9 @@ interface WidgetRepository { fun loadAdapter(smartspacerId: String, appWidgetId: Int, identifier: String?, id: Int?) fun launchFillInIntent(pendingIntent: PendingIntent, fillInIntent: Intent) + fun setWidgetTargetWidget(smartspacerId: String, remoteViews: RemoteViews) + fun getWidgetTargetWidget(smartspacerId: String): RemoteViews? + } class WidgetRepositoryImpl( @@ -98,6 +105,7 @@ class WidgetRepositoryImpl( private val widgetContext = WidgetContextWrapper(context) private val displayPortraitWidth = context.getDisplayPortraitWidth() private val displayPortraitHeight = context.getDisplayPortraitHeight() + private val widgetTargetWidgets = HashMap() @VisibleForTesting val appWidgetHostPool = mutableListOf() @@ -110,8 +118,12 @@ class WidgetRepositoryImpl( } override val providers = providersChanged.mapLatest { - appWidgetManager.installedProviders.filterIncompatible() - }.stateIn(scope, SharingStarted.Eagerly, appWidgetManager.installedProviders.filterIncompatible()) + appWidgetManager.getAllInstalledProviders(context).filterIncompatible() + }.stateIn( + scope, + SharingStarted.Eagerly, + appWidgetManager.getAllInstalledProviders(context).filterIncompatible() + ) override val widgets = combine( databaseWidgets, @@ -337,6 +349,20 @@ class WidgetRepositoryImpl( ) } + override fun setWidgetTargetWidget(smartspacerId: String, remoteViews: RemoteViews) { + synchronized(widgetTargetWidgets) { + widgetTargetWidgets[smartspacerId] = remoteViews + }.also { + SmartspacerTargetProvider.notifyChange(context, WidgetTarget::class.java) + } + } + + override fun getWidgetTargetWidget(smartspacerId: String): RemoteViews? { + return synchronized(widgetTargetWidgets) { + widgetTargetWidgets[smartspacerId] + } + } + private fun List.filterIncompatible(): List { return filterNot { provider -> val packageName = provider.provider.packageName diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/service/SmartspacerShizukuService.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/service/SmartspacerShizukuService.kt index 229c883e..bc17cdb7 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/service/SmartspacerShizukuService.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/service/SmartspacerShizukuService.kt @@ -499,15 +499,16 @@ class SmartspacerShizukuService: ISmartspacerShizukuService.Stub() { //Allow restricted settings automatically to help users who are using enhanced mode grantRestrictedSettings() //Hide the stop button in the Task Manager to prevent users from killing the service by mistake - hideStopButtonInTaskManager() + undoHideStopButtonInTaskManager() } override fun grantRestrictedSettings() { runCommand("cmd appops set ${BuildConfig.APPLICATION_ID} ACCESS_RESTRICTED_SETTINGS allow") } - private fun hideStopButtonInTaskManager() { - runCommand("cmd appops set ${BuildConfig.APPLICATION_ID} SYSTEM_EXEMPT_FROM_POWER_RESTRICTIONS allow") + private fun undoHideStopButtonInTaskManager() { + //Breaks battery optimisation toggle on some devices, undo earlier change if needed + runCommand("cmd appops set ${BuildConfig.APPLICATION_ID} SYSTEM_EXEMPT_FROM_POWER_RESTRICTIONS deny") } override fun enableBluetooth() { diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/service/SmartspacerSmartspaceService.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/service/SmartspacerSmartspaceService.kt index c8f01490..6a82d017 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/service/SmartspacerSmartspaceService.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/service/SmartspacerSmartspaceService.kt @@ -7,6 +7,7 @@ import android.content.ComponentName import android.content.Intent import android.content.IntentFilter import android.provider.Settings +import android.util.Log import androidx.lifecycle.lifecycleScope import com.kieronquinn.app.smartspacer.BuildConfig import com.kieronquinn.app.smartspacer.ISmartspacerCrashListener @@ -29,6 +30,7 @@ import com.kieronquinn.app.smartspacer.sdk.utils.applySecurity import com.kieronquinn.app.smartspacer.utils.extensions.broadcastReceiverAsFlow import com.kieronquinn.app.smartspacer.utils.extensions.getDarkMode import com.kieronquinn.app.smartspacer.utils.extensions.getDefaultSmartspaceComponent +import com.kieronquinn.app.smartspacer.utils.extensions.observerAsFlow import com.kieronquinn.app.smartspacer.utils.extensions.startForeground import com.kieronquinn.app.smartspacer.utils.extensions.toSmartspaceConfig import com.kieronquinn.app.smartspacer.utils.extensions.toSystemSmartspaceTargetEvent @@ -425,8 +427,12 @@ class SmartspacerSmartspaceService: LifecycleSmartspaceService() { System.currentTimeMillis() }.stateIn(lifecycleScope, SharingStarted.Eagerly, System.currentTimeMillis()) } + val materialTheme = contentResolver.observerAsFlow( + Settings.Secure.getUriFor("theme_customization_overlay_packages") + ) combine( broadcasts, + materialTheme, getDarkMode(lifecycleScope) ) { _ -> sessions.forEach { it.value.forceReload() } diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/ui/activities/configuration/ConfigurationActivity.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/ui/activities/configuration/ConfigurationActivity.kt index 11dbef6e..7a8759cf 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/ui/activities/configuration/ConfigurationActivity.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/ui/activities/configuration/ConfigurationActivity.kt @@ -87,6 +87,14 @@ class ConfigurationActivity: MonetCompatActivity() { ".ui.activities.configuration.target.flashlight.FlashlightTargetConfigurationActivity", R.navigation.nav_graph_configure_target_flashlight ), + TARGET_WIDGET( + ".ui.activities.configuration.target.widget.WidgetTargetConfigurationActivity", + R.navigation.nav_graph_configure_target_widget + ), + TARGET_WIDGET_SETUP( + ".ui.activities.configuration.target.widget.WidgetTargetSetupActivity", + R.navigation.nav_graph_setup_target_widget + ), COMPLICATION_GMAIL( ".ui.activities.configuration.complication.GmailComplicationConfigurationActivity", R.navigation.nav_graph_configure_complication_gmail diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/ui/screens/configuration/widgettarget/configuration/WidgetTargetConfigurationFragment.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/ui/screens/configuration/widgettarget/configuration/WidgetTargetConfigurationFragment.kt new file mode 100644 index 00000000..ae8e90f8 --- /dev/null +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/ui/screens/configuration/widgettarget/configuration/WidgetTargetConfigurationFragment.kt @@ -0,0 +1,122 @@ +package com.kieronquinn.app.smartspacer.ui.screens.configuration.widgettarget.configuration + +import android.os.Build +import android.os.Bundle +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.style.BulletSpan +import android.view.View +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import com.kieronquinn.app.smartspacer.R +import com.kieronquinn.app.smartspacer.components.smartspace.targets.WidgetTarget.TargetData.Padding +import com.kieronquinn.app.smartspacer.model.settings.BaseSettingsItem +import com.kieronquinn.app.smartspacer.model.settings.GenericSettingsItem +import com.kieronquinn.app.smartspacer.sdk.SmartspacerConstants.EXTRA_SMARTSPACER_ID +import com.kieronquinn.app.smartspacer.ui.base.BackAvailable +import com.kieronquinn.app.smartspacer.ui.base.settings.BaseSettingsAdapter +import com.kieronquinn.app.smartspacer.ui.base.settings.BaseSettingsFragment +import com.kieronquinn.app.smartspacer.ui.screens.configuration.widgettarget.configuration.WidgetTargetConfigurationViewModel.State +import com.kieronquinn.app.smartspacer.utils.extensions.whenResumed +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koin.core.parameter.parametersOf + +class WidgetTargetConfigurationFragment: BaseSettingsFragment(), BackAvailable { + + override val additionalPadding by lazy { + resources.getDimension(R.dimen.margin_8) + } + + private val viewModel by viewModel { + parametersOf(requireActivity().intent.getStringExtra(EXTRA_SMARTSPACER_ID)) + } + + override val adapter by lazy { + Adapter() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupState() + } + + private fun setupState() { + handleState(viewModel.state.value) + whenResumed { + viewModel.state.collect { + handleState(it) + } + } + } + + private fun handleState(state: State) = with(binding) { + when(state) { + is State.Loading -> { + settingsBaseRecyclerView.isVisible = false + settingsBaseLoading.isVisible = true + } + is State.Loaded -> { + settingsBaseRecyclerView.isVisible = true + settingsBaseLoading.isVisible = false + adapter.update(state.loadItems(), settingsBaseRecyclerView) + } + } + } + + private fun State.Loaded.loadItems(): List = listOfNotNull( + GenericSettingsItem.Card( + ContextCompat.getDrawable(requireContext(), R.drawable.ic_info), + createInfoText() + ), + GenericSettingsItem.Setting( + getString(R.string.target_widget_settings_widget_title), + getString(R.string.target_widget_settings_widget_content, data.label, data.appName), + ContextCompat.getDrawable(requireContext(), R.drawable.ic_widgets), + isEnabled = false, + onClick = {} + ), + GenericSettingsItem.SwitchSetting( + data.rounded, + getString(R.string.target_widget_settings_rounded_title), + getString(R.string.target_widget_settings_rounded_content), + ContextCompat.getDrawable(requireContext(), R.drawable.ic_expanded_custom_widget_options_round_corners), + onChanged = viewModel::onRoundChanged + ).takeIf { Build.VERSION.SDK_INT >= Build.VERSION_CODES.S }, + GenericSettingsItem.Dropdown( + getString(R.string.target_widget_settings_padding_title), + getString(R.string.target_widget_settings_padding_content, getString(data.padding.label)), + ContextCompat.getDrawable(requireContext(), R.drawable.ic_widget_configuration_padding), + data.padding, + viewModel::onPaddingChanged, + Padding.entries + ) { + it.label + } + ) + + private fun createInfoText() = SpannableStringBuilder().apply { + val footerItems = resources.getStringArray(R.array.target_widget_settings_limitations) + appendLine(getString(R.string.target_widget_settings_limitations_content)) + footerItems.forEachIndexed { i, item -> + appendBullet() + append(item) + if(i < footerItems.size - 1) { + appendLine() + } + } + appendLine() + appendLine() + append(getText(R.string.target_widget_settings_limitations_footer)) + } + + private fun SpannableStringBuilder.appendBullet() { + append( + " ", + BulletSpan(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + + inner class Adapter: BaseSettingsAdapter(binding.settingsBaseRecyclerView, emptyList()) + +} \ No newline at end of file diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/ui/screens/configuration/widgettarget/configuration/WidgetTargetConfigurationViewModel.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/ui/screens/configuration/widgettarget/configuration/WidgetTargetConfigurationViewModel.kt new file mode 100644 index 00000000..e011d933 --- /dev/null +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/ui/screens/configuration/widgettarget/configuration/WidgetTargetConfigurationViewModel.kt @@ -0,0 +1,70 @@ +package com.kieronquinn.app.smartspacer.ui.screens.configuration.widgettarget.configuration + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.kieronquinn.app.smartspacer.components.smartspace.targets.WidgetTarget +import com.kieronquinn.app.smartspacer.components.smartspace.targets.WidgetTarget.TargetData +import com.kieronquinn.app.smartspacer.components.smartspace.targets.WidgetTarget.TargetData.Padding +import com.kieronquinn.app.smartspacer.model.database.TargetDataType +import com.kieronquinn.app.smartspacer.repositories.DataRepository +import com.kieronquinn.app.smartspacer.sdk.provider.SmartspacerTargetProvider +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.stateIn + +abstract class WidgetTargetConfigurationViewModel: ViewModel() { + + abstract val state: StateFlow + + abstract fun onPaddingChanged(padding: Padding) + abstract fun onRoundChanged(enabled: Boolean) + + sealed class State { + data object Loading: State() + data class Loaded( + val data: TargetData + ): State() + } + +} + +class WidgetTargetConfigurationViewModelImpl( + private val dataRepository: DataRepository, + private val smartspacerId: String +): WidgetTargetConfigurationViewModel() { + + override val state = dataRepository.getTargetDataFlow(smartspacerId, TargetData::class.java) + .mapNotNull { State.Loaded(it ?: return@mapNotNull null) } + .stateIn(viewModelScope, SharingStarted.Eagerly, State.Loading) + + override fun onPaddingChanged(padding: Padding) { + val current = (state.value as? State.Loaded)?.data ?: return + dataRepository.updateTargetData( + smartspacerId, + TargetData::class.java, + TargetDataType.WIDGET, + ::onUpdate + ) { + current.copy(padding = padding) + } + } + + override fun onRoundChanged(enabled: Boolean) { + val current = (state.value as? State.Loaded)?.data ?: return + dataRepository.updateTargetData( + smartspacerId, + TargetData::class.java, + TargetDataType.WIDGET, + ::onUpdate + ) { + current.copy(rounded = enabled) + } + } + + private fun onUpdate(context: Context, smartspacerId: String) { + SmartspacerTargetProvider.notifyChange(context, WidgetTarget::class.java, smartspacerId) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/ui/screens/configuration/widgettarget/setup/WidgetTargetSetupFragment.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/ui/screens/configuration/widgettarget/setup/WidgetTargetSetupFragment.kt new file mode 100644 index 00000000..48be82ca --- /dev/null +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/ui/screens/configuration/widgettarget/setup/WidgetTargetSetupFragment.kt @@ -0,0 +1,42 @@ +package com.kieronquinn.app.smartspacer.ui.screens.configuration.widgettarget.setup + +import android.app.Activity +import android.os.Bundle +import android.view.View +import com.kieronquinn.app.smartspacer.R +import com.kieronquinn.app.smartspacer.sdk.SmartspacerConstants.EXTRA_SMARTSPACER_ID +import com.kieronquinn.app.smartspacer.ui.screens.expanded.addwidget.ExpandedAddWidgetBottomSheetFragment +import com.kieronquinn.app.smartspacer.ui.screens.expanded.addwidget.ExpandedAddWidgetBottomSheetViewModel.Item.Widget +import com.kieronquinn.app.smartspacer.utils.extensions.whenResumed +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koin.core.parameter.parametersOf + +class WidgetTargetSetupFragment: ExpandedAddWidgetBottomSheetFragment() { + + override val titleRes = R.string.target_widget_setup_title + + private val viewModel by viewModel { + parametersOf(requireActivity().intent.getStringExtra(EXTRA_SMARTSPACER_ID)) + } + + override fun onWidgetClicked( + item: Widget, + spanX: Int, + spanY: Int + ) { + viewModel.onWidgetClicked(item) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupDismiss() + } + + private fun setupDismiss() = whenResumed { + viewModel.dismissBus.collect { + requireActivity().setResult(Activity.RESULT_OK) + requireActivity().finish() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/ui/screens/configuration/widgettarget/setup/WidgetTargetSetupViewModel.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/ui/screens/configuration/widgettarget/setup/WidgetTargetSetupViewModel.kt new file mode 100644 index 00000000..072aae19 --- /dev/null +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/ui/screens/configuration/widgettarget/setup/WidgetTargetSetupViewModel.kt @@ -0,0 +1,50 @@ +package com.kieronquinn.app.smartspacer.ui.screens.configuration.widgettarget.setup + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.kieronquinn.app.smartspacer.components.smartspace.targets.WidgetTarget.TargetData +import com.kieronquinn.app.smartspacer.model.database.TargetDataType +import com.kieronquinn.app.smartspacer.repositories.DataRepository +import com.kieronquinn.app.smartspacer.ui.screens.expanded.addwidget.ExpandedAddWidgetBottomSheetViewModel.Item.Widget +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch + +abstract class WidgetTargetSetupViewModel: ViewModel() { + + abstract val dismissBus: Flow + + abstract fun onWidgetClicked(widget: Widget) + +} + +class WidgetTargetSetupViewModelImpl( + private val dataRepository: DataRepository, + private val smartspacerId: String +): WidgetTargetSetupViewModel() { + + override val dismissBus = MutableSharedFlow() + + override fun onWidgetClicked(widget: Widget) { + dataRepository.updateTargetData( + smartspacerId, + TargetData::class.java, + TargetDataType.WIDGET, + ::onDataAdded + ) { + TargetData( + widget.info.provider.flattenToString(), + widget.label.toString(), + widget.parent.label.toString() + ) + } + } + + private fun onDataAdded(context: Context, smartspacerId: String) { + viewModelScope.launch { + dismissBus.emit(Unit) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/ui/screens/expanded/ExpandedFragment.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/ui/screens/expanded/ExpandedFragment.kt index d4d35623..9c8db19c 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/ui/screens/expanded/ExpandedFragment.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/ui/screens/expanded/ExpandedFragment.kt @@ -580,7 +580,7 @@ class ExpandedFragment: BoundFragment( id: String?, config: CustomExpandedAppWidgetConfig? ) { - if(isOverlay){ + if(isOverlay || isMinusOne){ launchOverlayAction(OpenFromOverlayAction.ConfigureWidget(info, id, config, getScroll())) }else{ unlockAndInvoke { @@ -596,7 +596,7 @@ class ExpandedFragment: BoundFragment( } override fun onAddWidgetClicked() { - if(isOverlay){ + if(isOverlay || isMinusOne){ launchOverlayAction(OpenFromOverlayAction.AddWidget(getScroll())) }else{ unlockAndInvoke { diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/ui/screens/expanded/ExpandedSession.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/ui/screens/expanded/ExpandedSession.kt index be785c5e..5026bc72 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/ui/screens/expanded/ExpandedSession.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/ui/screens/expanded/ExpandedSession.kt @@ -3,7 +3,6 @@ package com.kieronquinn.app.smartspacer.ui.screens.expanded import android.content.Context import android.os.Process import android.provider.Settings -import android.util.Log import com.kieronquinn.app.smartspacer.components.smartspace.ExpandedSmartspacerSession import com.kieronquinn.app.smartspacer.repositories.SmartspacerSettingsRepository import com.kieronquinn.app.smartspacer.sdk.model.SmartspaceAction @@ -148,11 +147,13 @@ class ExpandedSessionImpl( private val hideAddButton = combine( isOverlay.filterNotNull(), - settingsRepository.expandedHideAddButton.asFlow() - ) { overlay, hideAdd -> + settingsRepository.expandedHideAddButton.asFlow(), + isLocked + ) { overlay, hideAdd, locked -> when(hideAdd) { SmartspacerSettingsRepository.ExpandedHideAddButton.NEVER -> false SmartspacerSettingsRepository.ExpandedHideAddButton.ALWAYS -> true + SmartspacerSettingsRepository.ExpandedHideAddButton.WHEN_LOCKED -> locked SmartspacerSettingsRepository.ExpandedHideAddButton.OVERLAY_ONLY -> overlay } } diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/ui/screens/expanded/addwidget/BaseExpandedAddWidgetBottomSheetAdapter.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/ui/screens/expanded/addwidget/BaseExpandedAddWidgetBottomSheetAdapter.kt index 07b51bdb..f27dd0ea 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/ui/screens/expanded/addwidget/BaseExpandedAddWidgetBottomSheetAdapter.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/ui/screens/expanded/addwidget/BaseExpandedAddWidgetBottomSheetAdapter.kt @@ -2,6 +2,8 @@ package com.kieronquinn.app.smartspacer.ui.screens.expanded.addwidget import android.appwidget.AppWidgetProviderInfo import android.content.Context +import android.os.Process +import android.os.UserHandle import android.util.SizeF import android.view.ViewGroup import android.widget.ImageView @@ -14,6 +16,7 @@ import com.kieronquinn.app.smartspacer.R import com.kieronquinn.app.smartspacer.model.glide.Widget import com.kieronquinn.app.smartspacer.ui.views.appwidget.PreviewAppWidgetHostView import com.kieronquinn.app.smartspacer.utils.extensions.getBestRemoteViews +import com.kieronquinn.app.smartspacer.utils.extensions.getIdentifier import com.kieronquinn.app.smartspacer.utils.extensions.loadPreview interface BaseExpandedAddWidgetBottomSheetAdapter { @@ -21,6 +24,7 @@ interface BaseExpandedAddWidgetBottomSheetAdapter { fun setupWidget( widgetContext: Context, glide: RequestManager, + profile: UserHandle, info: AppWidgetProviderInfo, spanX: Int, spanY: Int, @@ -71,11 +75,15 @@ interface BaseExpandedAddWidgetBottomSheetAdapter { .into(imageImageView) .waitForLayout() } + val icon = if(profile != info.profile) { + " 💼" + }else "" nameTextView.text = root.context.getString( R.string.expanded_add_widget_widget_label, label, spanX, - spanY + spanY, + icon ) descriptionTextView.text = description descriptionTextView.isVisible = description != null diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/ui/screens/expanded/addwidget/ExpandedAddWidgetBottomSheetAdapter.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/ui/screens/expanded/addwidget/ExpandedAddWidgetBottomSheetAdapter.kt index aeac9b2a..7b62a3d3 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/ui/screens/expanded/addwidget/ExpandedAddWidgetBottomSheetAdapter.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/ui/screens/expanded/addwidget/ExpandedAddWidgetBottomSheetAdapter.kt @@ -1,5 +1,6 @@ package com.kieronquinn.app.smartspacer.ui.screens.expanded.addwidget +import android.os.Process import android.view.LayoutInflater import android.view.View.OnClickListener import android.view.ViewGroup @@ -73,6 +74,7 @@ class ExpandedAddWidgetBottomSheetAdapter( private val context = recyclerView.context private val widgetContext = ContextThemeWrapper(recyclerView.context.applicationContext, theme) + private val handle = Process.myUserHandle() private val layoutInflater = LayoutInflater.from(context) private val glide = Glide.with(recyclerView.context) @@ -185,6 +187,7 @@ class ExpandedAddWidgetBottomSheetAdapter( setupWidget( widgetContext, glide, + handle, item.info, spanX, spanY, diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/ui/screens/expanded/addwidget/ExpandedAddWidgetBottomSheetFragment.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/ui/screens/expanded/addwidget/ExpandedAddWidgetBottomSheetFragment.kt index 4e99777b..25e1b5f8 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/ui/screens/expanded/addwidget/ExpandedAddWidgetBottomSheetFragment.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/ui/screens/expanded/addwidget/ExpandedAddWidgetBottomSheetFragment.kt @@ -18,6 +18,7 @@ import com.kieronquinn.app.smartspacer.R import com.kieronquinn.app.smartspacer.databinding.FragmentExpandedBottomSheetAddWidgetBinding import com.kieronquinn.app.smartspacer.ui.base.BaseBottomSheetFragment import com.kieronquinn.app.smartspacer.ui.screens.expanded.addwidget.ExpandedAddWidgetBottomSheetViewModel.AddState +import com.kieronquinn.app.smartspacer.ui.screens.expanded.addwidget.ExpandedAddWidgetBottomSheetViewModel.Item import com.kieronquinn.app.smartspacer.ui.screens.expanded.addwidget.ExpandedAddWidgetBottomSheetViewModel.State import com.kieronquinn.app.smartspacer.utils.extensions.allowBackground import com.kieronquinn.app.smartspacer.utils.extensions.onApplyInsets @@ -30,9 +31,10 @@ import kotlinx.coroutines.flow.drop import kotlinx.coroutines.launch import org.koin.androidx.viewmodel.ext.android.viewModel -class ExpandedAddWidgetBottomSheetFragment: BaseBottomSheetFragment(FragmentExpandedBottomSheetAddWidgetBinding::inflate) { +open class ExpandedAddWidgetBottomSheetFragment: BaseBottomSheetFragment(FragmentExpandedBottomSheetAddWidgetBinding::inflate) { private val viewModel by viewModel() + open val titleRes: Int = R.string.expanded_add_widget_title private val widgetBindResult = registerForActivityResult( ActivityResultContracts.StartActivityForResult() @@ -53,12 +55,13 @@ class ExpandedAddWidgetBottomSheetFragment: BaseBottomSheetFragment Unit)? = null private val layoutInflater = LayoutInflater.from(context) + private val handle = Process.myUserHandle() override fun getItemCount(): Int = items.size @@ -53,6 +55,7 @@ class ExpandedAddWidgetBottomSheetViewPagerAdapter( setupWidget( widgetContext, glide, + handle, item.info, spanX, spanY, diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/ui/views/appwidget/HeadlessAppWidgetHostView.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/ui/views/appwidget/HeadlessAppWidgetHostView.kt index 331fec51..3c61fcd0 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/ui/views/appwidget/HeadlessAppWidgetHostView.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/ui/views/appwidget/HeadlessAppWidgetHostView.kt @@ -16,7 +16,7 @@ import com.kieronquinn.app.smartspacer.ui.views.appwidget.HeadlessAppWidgetHostV import com.kieronquinn.app.smartspacer.utils.extensions.createPackageContextOrNull import com.kieronquinn.app.smartspacer.utils.extensions.extractAdapterIntent import com.kieronquinn.app.smartspacer.utils.extensions.extractRemoteCollectionItems -import com.kieronquinn.app.smartspacer.utils.extensions.getActionsIncludingNested +import com.kieronquinn.app.smartspacer.utils.extensions.getActionsIncludingSized import com.kieronquinn.app.smartspacer.utils.extensions.isRemoteCollectionItemListAdapter import com.kieronquinn.app.smartspacer.utils.extensions.isRemoteViewsAdapterIntent import com.kieronquinn.app.smartspacer.utils.remoteviews.RemoteViewsFactoryWrapper @@ -162,14 +162,14 @@ class HeadlessAppWidgetHostView(context: Context): RoundedCornersEnforcingAppWid } private fun RemoteViews.findRemoteViewsAdapters(): List> { - return getActionsIncludingNested().filter { it.isRemoteViewsAdapterIntent() }.map { + return getActionsIncludingSized().filter { it.isRemoteViewsAdapterIntent() }.map { it.extractAdapterIntent() } } @RequiresApi(Build.VERSION_CODES.S) private fun RemoteViews.findRemoteViewsCollectionListAdapters(): List> { - return getActionsIncludingNested().filter { + return getActionsIncludingSized().filter { it.isRemoteCollectionItemListAdapter() }.mapNotNull { it.extractRemoteCollectionItems() diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/ui/views/smartspace/SmartspaceView.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/ui/views/smartspace/SmartspaceView.kt index 85224393..c0c42c45 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/ui/views/smartspace/SmartspaceView.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/ui/views/smartspace/SmartspaceView.kt @@ -5,18 +5,21 @@ import android.content.Context import android.content.Intent import android.content.res.ColorStateList import android.graphics.drawable.Icon +import android.os.Build import android.view.View import android.widget.RemoteViews import androidx.annotation.IdRes import androidx.core.graphics.drawable.toBitmap import com.kieronquinn.app.smartspacer.R import com.kieronquinn.app.smartspacer.receivers.SmartspacerWidgetClickReceiver +import com.kieronquinn.app.smartspacer.receivers.WidgetListClickReceiver import com.kieronquinn.app.smartspacer.repositories.AppWidgetRepository import com.kieronquinn.app.smartspacer.sdk.client.utils.getEnabledDrawableOrNull import com.kieronquinn.app.smartspacer.sdk.model.SmartspaceTarget import com.kieronquinn.app.smartspacer.sdk.model.UiSurface import com.kieronquinn.app.smartspacer.sdk.model.uitemplatedata.TapAction import com.kieronquinn.app.smartspacer.ui.views.smartspace.features.BaseFeatureSmartspaceView +import com.kieronquinn.app.smartspacer.ui.views.smartspace.remoteviews.RemoteViewsSmartspaceView import com.kieronquinn.app.smartspacer.ui.views.smartspace.templates.BaseTemplateSmartspaceView import com.kieronquinn.app.smartspacer.utils.extensions.setImageViewImageTintListCompat import org.koin.core.component.KoinComponent @@ -33,7 +36,9 @@ abstract class SmartspaceView( surface: UiSurface, forceBasic: Boolean ): SmartspaceView { - return target.templateData?.let { template -> + return target.remoteViews?.let { + RemoteViewsSmartspaceView(target, surface) + } ?: target.templateData?.let { template -> BaseTemplateSmartspaceView.create( target.smartspaceTargetId, target, template, surface, forceBasic ) @@ -123,28 +128,34 @@ abstract class SmartspaceView( id: Int, targetId: String, surface: UiSurface, - action: TapAction? + action: TapAction?, + isList: Boolean ) { val intent = SmartspacerWidgetClickReceiver.createIntent( context, targetId, surface, tapAction = action ) - setOnClickIntent(context, targetId, id, intent) + setOnClickIntent(context, targetId, id, intent, isList) } protected fun RemoteViews.setOnClickIntent( context: Context, targetId: String, id: Int, - intent: Intent + intent: Intent, + isList: Boolean ) { - val pendingIntentCode = listOf(targetId, id).hashCode() - val pendingIntent = PendingIntent.getBroadcast( - context, - pendingIntentCode, - intent, - PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT - ) - setOnClickPendingIntent(id, pendingIntent) + if(isList && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + setOnClickFillInIntent(id, WidgetListClickReceiver.getIntent(intent)) + }else{ + val pendingIntentCode = listOf(targetId, id).hashCode() + val pendingIntent = PendingIntent.getBroadcast( + context, + pendingIntentCode, + intent, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) + setOnClickPendingIntent(id, pendingIntent) + } } protected fun RemoteViews.setupOverflow( @@ -171,7 +182,8 @@ abstract class SmartspaceView( R.id.widget_smartspacer_list_item_overflow, target.smartspaceTargetId, surface, - tapAction + tapAction, + true ) } @@ -204,7 +216,8 @@ abstract class SmartspaceView( FEATURE_UNDEFINED, FEATURE_WEATHER, FEATURE_COMMUTE_TIME, - FEATURE_DOORBELL + FEATURE_DOORBELL, + REMOTE_VIEWS } } \ No newline at end of file diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/ui/views/smartspace/features/BaseFeatureSmartspaceView.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/ui/views/smartspace/features/BaseFeatureSmartspaceView.kt index 4b65a6e1..d8722b2a 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/ui/views/smartspace/features/BaseFeatureSmartspaceView.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/ui/views/smartspace/features/BaseFeatureSmartspaceView.kt @@ -119,12 +119,14 @@ abstract class BaseFeatureSmartspaceView( remoteViews.setOnClickAction( context, R.id.smartspace_view_root, - target.headerAction + target.headerAction, + isList ) remoteViews.setOnClickAction( context, R.id.smartspace_view_title, - target.headerAction + target.headerAction, + isList ) val action = target.baseAction if(supportsSubAction && (action?.subtitle?.isNotEmpty() == true || action?.icon != null)){ @@ -138,7 +140,8 @@ abstract class BaseFeatureSmartspaceView( remoteViews.setOnClickAction( context, R.id.smartspace_view_action_icon, - action + action, + isList ) remoteViews.setViewVisibility(R.id.smartspace_view_action_icon, View.VISIBLE) }else{ @@ -152,12 +155,14 @@ abstract class BaseFeatureSmartspaceView( remoteViews.setOnClickAction( context, R.id.smartspace_view_action_text, - action + action, + isList ) remoteViews.setOnClickAction( context, R.id.smartspace_view_action_text, - target.baseAction + target.baseAction, + isList ) remoteViews.setTextViewTextSize( R.id.smartspace_view_action_text, TypedValue.COMPLEX_UNIT_PX, subtitleSize @@ -179,12 +184,13 @@ abstract class BaseFeatureSmartspaceView( private fun RemoteViews.setOnClickAction( context: Context, id: Int, - action: SmartspaceAction? + action: SmartspaceAction?, + isList: Boolean ) { val intent = SmartspacerWidgetClickReceiver.createIntent( context, targetId, surface, smartspaceAction = action?.stripData() ) - setOnClickIntent(context, targetId, id, intent) + setOnClickIntent(context, targetId, id, intent, isList) } /** diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/ui/views/smartspace/features/WeatherFeatureSmartspaceView.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/ui/views/smartspace/features/WeatherFeatureSmartspaceView.kt index b6f1b431..e3aeee22 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/ui/views/smartspace/features/WeatherFeatureSmartspaceView.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/ui/views/smartspace/features/WeatherFeatureSmartspaceView.kt @@ -67,7 +67,8 @@ class WeatherFeatureSmartspaceView( R.id.smartspace_view_title, targetId, surface, - tapAction + tapAction, + isList ) } diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/ui/views/smartspace/remoteviews/RemoteViewsSmartspaceView.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/ui/views/smartspace/remoteviews/RemoteViewsSmartspaceView.kt new file mode 100644 index 00000000..bd2199ea --- /dev/null +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/ui/views/smartspace/remoteviews/RemoteViewsSmartspaceView.kt @@ -0,0 +1,48 @@ +package com.kieronquinn.app.smartspacer.ui.views.smartspace.remoteviews + +import android.content.Context +import android.content.Intent +import android.graphics.Color +import android.widget.RemoteViews +import com.kieronquinn.app.smartspacer.R +import com.kieronquinn.app.smartspacer.sdk.client.utils.wrap +import com.kieronquinn.app.smartspacer.sdk.model.SmartspaceTarget +import com.kieronquinn.app.smartspacer.sdk.model.UiSurface +import com.kieronquinn.app.smartspacer.sdk.utils.copy +import com.kieronquinn.app.smartspacer.ui.views.smartspace.SmartspaceView +import com.kieronquinn.app.smartspacer.utils.extensions.replaceClickWithFillIntent + +class RemoteViewsSmartspaceView( + override val target: SmartspaceTarget, + override val surface: UiSurface +): SmartspaceView(target, surface) { + + override val viewType = ViewType.REMOTE_VIEWS + override val layoutRes = R.layout.smartspace_view_remoteviews + + override fun apply( + context: Context, + textColour: Int, + shadowEnabled: Boolean, + remoteViews: RemoteViews, + width: Int, + titleSize: Float, + subtitleSize: Float, + featureSize: Float, + isList: Boolean, + overflowIntent: Intent? + ) { + remoteViews.removeAllViews(R.id.smartspace_view_root) + val remoteViewsToApply = target.remoteViews + ?.wrap(context, textColour == Color.BLACK) + ?.copy() + ?.let { + if(isList) it.replaceClickWithFillIntent() else it + } + return remoteViews.addView( + R.id.smartspace_view_root, + remoteViewsToApply + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/ui/views/smartspace/templates/BaseTemplateSmartspaceView.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/ui/views/smartspace/templates/BaseTemplateSmartspaceView.kt index cb5b270f..7d6522bf 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/ui/views/smartspace/templates/BaseTemplateSmartspaceView.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/ui/views/smartspace/templates/BaseTemplateSmartspaceView.kt @@ -105,6 +105,9 @@ abstract class BaseTemplateSmartspaceView( remoteViews.setOnClickAction( context, R.id.smartspace_view_root, isList, template.primaryItem?.tapAction ) + remoteViews.setOnClickAction( + context, R.id.smartspace_view_title, isList, template.primaryItem?.tapAction + ) val enforcedHeightVisibility = if(isList) View.VISIBLE else View.GONE remoteViews.setViewVisibility(R.id.smartspace_view_enforced_height, enforcedHeightVisibility) template.subtitleItem?.text?.let { @@ -224,7 +227,7 @@ abstract class BaseTemplateSmartspaceView( isList: Boolean, action: TapAction? ) { - setOnClickAction(context, id, targetId, surface, action) + setOnClickAction(context, id, targetId, surface, action, isList) } } \ No newline at end of file diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/ui/views/smartspace/templates/WeatherTemplateSmartspaceView.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/ui/views/smartspace/templates/WeatherTemplateSmartspaceView.kt index 86f56216..946e4208 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/ui/views/smartspace/templates/WeatherTemplateSmartspaceView.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/ui/views/smartspace/templates/WeatherTemplateSmartspaceView.kt @@ -73,7 +73,8 @@ class WeatherTemplateSmartspaceView( R.id.smartspace_view_title, targetId, surface, - tapAction + tapAction, + isList ) } diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/utils/extensions/Extensions+AppWidgetManager.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/utils/extensions/Extensions+AppWidgetManager.kt index aaa7c27c..a18d439b 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/utils/extensions/Extensions+AppWidgetManager.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/utils/extensions/Extensions+AppWidgetManager.kt @@ -2,11 +2,14 @@ package com.kieronquinn.app.smartspacer.utils.extensions import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetManagerHidden +import android.appwidget.AppWidgetProviderInfo import android.content.Context import android.content.Intent import android.content.ServiceConnection import android.os.Build import android.os.Handler +import android.os.UserManager +import android.util.Log import dev.rikka.tools.refine.Refine fun AppWidgetManager.bindRemoteViewsService( @@ -32,4 +35,11 @@ fun AppWidgetManager.noteAppWidgetTappedCompat(appWidgetId: Int) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { noteAppWidgetTapped(appWidgetId) } +} + +fun AppWidgetManager.getAllInstalledProviders(context: Context): List { + val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager + return userManager.userProfiles.flatMap { + getInstalledProvidersForProfile(it) + } } \ No newline at end of file diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/utils/extensions/Extensions+AppWidgetProviderInfo.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/utils/extensions/Extensions+AppWidgetProviderInfo.kt index 95be64ea..6bbe0f76 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/utils/extensions/Extensions+AppWidgetProviderInfo.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/utils/extensions/Extensions+AppWidgetProviderInfo.kt @@ -3,13 +3,16 @@ package com.kieronquinn.app.smartspacer.utils.extensions import android.annotation.SuppressLint import android.appwidget.AppWidgetProviderInfo import android.appwidget.AppWidgetProviderInfoHidden +import android.content.ComponentName import android.content.Context import android.content.pm.ActivityInfo import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager import android.os.Build import android.widget.RemoteViews import androidx.annotation.StringRes import com.kieronquinn.app.smartspacer.R +import com.kieronquinn.app.smartspacer.ui.activities.MainActivity import dev.rikka.tools.refine.Refine import org.koin.java.KoinJavaComponent.inject import kotlin.math.floor @@ -178,4 +181,20 @@ private fun getSpan(minSize: Int, spanSize: Int): Int { } } return 1 +} + +fun Context.createFakeWidgetProviderInfo(): AppWidgetProviderInfo { + val activityInfo = packageManager.getActivityInfo( + ComponentName(this, MainActivity::class.java) + ) + return AppWidgetProviderInfo().apply { + //Used to load widget description, has to be a real activity info + providerInfo = activityInfo + } +} + +class FakeActivityInfo: ActivityInfo() { + override fun loadLabel(pm: PackageManager): CharSequence { + return "" + } } \ No newline at end of file diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/utils/extensions/Extensions+Bitmap.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/utils/extensions/Extensions+Bitmap.kt index ef7ea63f..e435d553 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/utils/extensions/Extensions+Bitmap.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/utils/extensions/Extensions+Bitmap.kt @@ -8,7 +8,7 @@ import java.io.IOException fun Bitmap.setTint(colour: Int): Bitmap { val paint = Paint() paint.colorFilter = PorterDuffColorFilter(colour, PorterDuff.Mode.SRC_IN) - val bitmap = Bitmap.createBitmap(width, height, config) + val bitmap = Bitmap.createBitmap(width, height, config ?: Bitmap.Config.ARGB_8888) val canvas = Canvas(bitmap) canvas.drawBitmap(this, 0f, 0f, paint) return bitmap diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/utils/extensions/Extensions+Build.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/utils/extensions/Extensions+Build.kt index 4dfb136b..b81e693d 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/utils/extensions/Extensions+Build.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/utils/extensions/Extensions+Build.kt @@ -1,7 +1,15 @@ package com.kieronquinn.app.smartspacer.utils.extensions +import android.annotation.SuppressLint import android.os.Build +import androidx.core.os.BuildCompat.isAtLeastPreReleaseCodename fun isAtLeastU(): Boolean { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE -} \ No newline at end of file +} + +@SuppressLint("RestrictedApi") +fun isAtLeastBaklava(): Boolean = + Build.VERSION.SDK_INT >= 36 || + (Build.VERSION.SDK_INT >= 35 && + isAtLeastPreReleaseCodename("Baklava", Build.VERSION.CODENAME)) \ No newline at end of file diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/utils/extensions/Extensions+ContentResolver.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/utils/extensions/Extensions+ContentResolver.kt index eff17456..29d4c3bd 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/utils/extensions/Extensions+ContentResolver.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/utils/extensions/Extensions+ContentResolver.kt @@ -8,6 +8,7 @@ import android.os.Bundle import android.os.DeadObjectException import android.os.Handler import android.os.Looper +import android.provider.Settings import com.kieronquinn.app.smartspacer.utils.test.TestUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.awaitClose diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/utils/extensions/Extensions+PackageManager.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/utils/extensions/Extensions+PackageManager.kt index f1e8d4c1..d367a739 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/utils/extensions/Extensions+PackageManager.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/utils/extensions/Extensions+PackageManager.kt @@ -166,8 +166,10 @@ fun PackageManager.resolveContentProvider(authority: String): ProviderInfo? { fun PackageManager.packageHasPermission(packageName: String, permission: String): Boolean { return try { val info = getPackageInfoCompat(packageName, PackageManager.GET_PERMISSIONS) - val permissions = info.requestedPermissions.zip(info.requestedPermissionsFlags.toTypedArray()) - permissions.any { it.first == permission && it.second and REQUESTED_PERMISSION_GRANTED != 0 } + val flags = info.requestedPermissionsFlags?.toTypedArray() ?: return false + val permissions = info.requestedPermissions?.zip(flags) + permissions?.any { it.first == permission && it.second and REQUESTED_PERMISSION_GRANTED != 0 } + ?: return false }catch (e: NameNotFoundException){ false } diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/utils/extensions/Extensions+RemoteResponse.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/utils/extensions/Extensions+RemoteResponse.kt index 0755c67e..e84532ca 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/utils/extensions/Extensions+RemoteResponse.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/utils/extensions/Extensions+RemoteResponse.kt @@ -53,4 +53,10 @@ fun SystemRemoteResponse.toRemoteResponse(): RemoteResponse { getViewIds()?.toList() ?: ArrayList(), getElementNames() ?: ArrayList() ) -} \ No newline at end of file +} + +@SuppressLint("BlockedPrivateApi") +fun SystemRemoteResponse.setInteractionType(type: Int) = apply { + SystemRemoteResponse::class.java.getDeclaredMethod("setInteractionType", Integer.TYPE) + .apply { isAccessible = true }.invoke(this, type) +} diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/utils/extensions/Extensions+RemoteViews.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/utils/extensions/Extensions+RemoteViews.kt index 31af4852..64c896da 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/utils/extensions/Extensions+RemoteViews.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/utils/extensions/Extensions+RemoteViews.kt @@ -23,7 +23,10 @@ import androidx.annotation.RequiresApi import androidx.core.widget.RemoteViewsCompat.setImageViewColorFilter import androidx.core.widget.RemoteViewsCompat.setImageViewImageTintList import com.kieronquinn.app.smartspacer.providers.SmartspacerWidgetProxyContentProvider.Companion.createSmartspacerWidgetProxyUri +import com.kieronquinn.app.smartspacer.receivers.WidgetListClickReceiver import com.kieronquinn.app.smartspacer.sdk.client.views.base.SmartspacerBasePageView.SmartspaceTargetInteractionListener +import com.kieronquinn.app.smartspacer.sdk.model.RemoteOnClickResponse.RemoteResponse.Companion.INTERACTION_TYPE_CHECKED_CHANGE +import com.kieronquinn.app.smartspacer.sdk.utils.copy import com.kieronquinn.app.smartspacer.ui.activities.OverlayTrampolineActivity import dev.rikka.tools.refine.Refine import java.lang.reflect.Field @@ -31,24 +34,41 @@ import java.util.concurrent.CompletableFuture import kotlin.Pair import android.util.Pair as AndroidPair -@Suppress("UNCHECKED_CAST") @SuppressLint("DiscouragedPrivateApi") -private fun RemoteViews.getActions(): ArrayList? { - return RemoteViews::class.java.getDeclaredField("mActions").apply { - isAccessible = true - }.get(this) as? ArrayList +private val mActions = RemoteViews::class.java.getDeclaredField("mActions").apply { + isAccessible = true } +@Suppress("UNCHECKED_CAST") +private var RemoteViews.actions: ArrayList? + get() = mActions.get(this) as? ArrayList + set(value) = mActions.set(this, value) + @Suppress("UNCHECKED_CAST") @SuppressLint("DiscouragedPrivateApi") -fun RemoteViews.getActionsIncludingNested(): List { - val myActions = getActions()?.toList() ?: emptyList() +fun RemoteViews.getActionsIncludingSized(): List { + val myActions = actions?.toList() ?: emptyList() val sizedActions = getSizedRemoteViews().mapNotNull { - it.getActions() + it.actions }.flatten() return myActions + sizedActions } +fun RemoteViews.getActionsIncludingNested(): List { + val actions = getActionsIncludingSized() + val actionsIncludingNested = ArrayList(actions) + actions.forEach { + when(it.javaClass.simpleName) { + "ViewGroupActionAdd" -> { + it.getViewGroupActionAddRemoteViews().getActionsIncludingNested().let { actions -> + actionsIncludingNested.addAll(actions) + } + } + } + } + return actionsIncludingNested.distinct() +} + private val setRemoteViewsAdapterIntentClass by lazy { Class.forName("${RemoteViews::class.java.name}\$SetRemoteViewsAdapterIntent") } @@ -145,7 +165,7 @@ var Any.reflectionActionValue: Any? set(value) = reflectionActionValueField.set(this, value) fun RemoteViews.removeActionsForId(id: Int) { - val actions = getActions() ?: return + val actions = actions ?: return actions.removeAll { it.getId() == id } getSizedRemoteViews().forEach { it.removeActionsForId(id) @@ -156,7 +176,7 @@ fun RemoteViews.replaceUriActionsWithProxy( context: Context, pluginPackageName: String ): RemoteViews = apply { - val uriActions = getActionsIncludingNested().filter { + val uriActions = getActionsIncludingSized().filter { it::class.java == reflectionActionClass && it.reflectionActionValue is Uri }.map { Pair(it, it.reflectionActionValue as Uri) @@ -290,4 +310,90 @@ private fun Class<*>.getDeclaredField(vararg options: String): Field { null } } ?: throw lastException -} \ No newline at end of file +} + +/** + * Replaces click actions with fill in proxy actions, running via [WidgetListClickReceiver] + */ +fun RemoteViews.replaceClickWithFillIntent(): RemoteViews { + //Not required on Android < 13 since we disable this requirement + if(Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return this + actions = actions?.map { + when(it.javaClass.simpleName) { + "ViewGroupActionAdd" -> it.replaceClickWithFillIntent() + "SetOnClickResponse" -> it.convertOnClickResponse() + "SetOnCheckedChangeResponse" -> it.convertOnCheckedChangeResponse() + else -> it + } + } as ArrayList? + return this +} + +@SuppressLint("PrivateApi", "SoonBlockedPrivateApi") +private fun Any.replaceClickWithFillIntent() = apply { + val action = Class.forName("android.widget.RemoteViews\$ViewGroupActionAdd") + val mNestedViews = action.getDeclaredField("mNestedViews").apply { + isAccessible = true + } + val nestedViews = mNestedViews.get(this) as RemoteViews + mNestedViews.set(this, nestedViews.replaceClickWithFillIntent()) +} + +@SuppressLint("PrivateApi", "SoonBlockedPrivateApi") +private fun Any.getViewGroupActionAddRemoteViews(): RemoteViews { + val action = Class.forName("android.widget.RemoteViews\$ViewGroupActionAdd") + val mNestedViews = action.getDeclaredField("mNestedViews").apply { + isAccessible = true + } + return mNestedViews.get(this) as RemoteViews +} + +@SuppressLint("PrivateApi") +private fun Any.convertOnClickResponse() = apply { + val action = Class.forName("android.widget.RemoteViews\$SetOnClickResponse") + val mResponse = action.getDeclaredField("mResponse").apply { + isAccessible = true + } + val remoteResponse = mResponse.get(this) as RemoteResponse + val newResponse = RemoteResponse + .fromFillInIntent(WidgetListClickReceiver.getIntent(remoteResponse)) + mResponse.set(this, newResponse) +} + +@SuppressLint("PrivateApi") +private fun Any.convertOnCheckedChangeResponse() = apply { + val action = Class.forName("android.widget.RemoteViews\$SetOnCheckedChangeResponse") + val mResponse = action.getDeclaredField("mResponse").apply { + isAccessible = true + } + val remoteResponse = mResponse.get(this) as RemoteResponse + val newResponse = RemoteResponse + .fromFillInIntent(WidgetListClickReceiver.getIntent(remoteResponse)) + .setInteractionType(INTERACTION_TYPE_CHECKED_CHANGE) + mResponse.set(this, newResponse) +} + +private val INCOMPATIBLE_ACTIONS = setOf( + "SetRemoteCollectionItemListAdapterAction", + "SetRemoteViewsAdapterIntent" +) + +fun RemoteViews.checkCompatibility(): Boolean { + return getActionsIncludingSized().none { + INCOMPATIBLE_ACTIONS.contains(it.javaClass.simpleName) + } +} + +@SuppressLint("SoonBlockedPrivateApi") +private val mIsRoot = RemoteViews::class.java.getDeclaredField("mIsRoot").apply { + isAccessible = true +} + +fun RemoteViews.copyAsRoot(): RemoteViews { + isRoot = true + return copy() +} + +private var RemoteViews.isRoot: Boolean + get() = mIsRoot.getBoolean(this) + set(value) = mIsRoot.setBoolean(this, value) \ No newline at end of file diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/utils/extensions/Extensions+SmartspaceTarget.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/utils/extensions/Extensions+SmartspaceTarget.kt index 3beada40..7e968cd6 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/utils/extensions/Extensions+SmartspaceTarget.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/utils/extensions/Extensions+SmartspaceTarget.kt @@ -6,6 +6,7 @@ import android.os.Build import com.kieronquinn.app.smartspacer.components.smartspace.targets.DefaultTarget import com.kieronquinn.app.smartspacer.model.smartspace.Target import com.kieronquinn.app.smartspacer.repositories.WallpaperRepository +import com.kieronquinn.app.smartspacer.sdk.client.utils.wrap import com.kieronquinn.app.smartspacer.sdk.model.SmartspaceTarget import com.kieronquinn.app.smartspacer.sdk.model.UiSurface import org.koin.java.KoinJavaComponent @@ -19,7 +20,10 @@ private fun T?.clone(to: (T) -> SystemSmartspaceTarget.Builder) { } @Synchronized -fun SmartspaceTarget.toSystemSmartspaceTarget(uiSurface: UiSurface): SystemSmartspaceTarget { +fun SmartspaceTarget.toSystemSmartspaceTarget( + context: Context, + uiSurface: UiSurface +): SystemSmartspaceTarget { val from = this val wallpaperRepository by KoinJavaComponent.inject(WallpaperRepository::class.java) val dark = when(uiSurface){ @@ -48,6 +52,9 @@ fun SmartspaceTarget.toSystemSmartspaceTarget(uiSurface: UiSurface): SystemSmart from.associatedSmartspaceTargetId.clone(::setAssociatedSmartspaceTargetId) from.sliceUri.clone(::setSliceUri) from.widget.clone(::setWidget) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + from.remoteViews?.wrap(context, dark)?.copyAsRoot().clone(::setRemoteViews) + } if(supportsUiTemplate()){ from.templateData ?.toSystemBaseTemplateData(tintColour) @@ -76,6 +83,9 @@ fun SystemSmartspaceTarget.toSmartspaceTarget(): SmartspaceTarget { associatedSmartspaceTargetId = associatedSmartspaceTargetId, sliceUri = sliceUri, widget = widget, + remoteViews = if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + remoteViews + } else null, templateData = if(supportsUiTemplate()) templateData?.toBaseTemplateData() else null, expandedState = null, canBeDismissed = featureType != SmartspaceTarget.FEATURE_WEATHER diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/utils/glide/SystemIconShapeTransformation.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/utils/glide/SystemIconShapeTransformation.kt index 6c7d1cf9..9a74ffa3 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/utils/glide/SystemIconShapeTransformation.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/utils/glide/SystemIconShapeTransformation.kt @@ -37,7 +37,7 @@ class SystemIconShapeTransformation: BitmapTransformation() { } it.transform(scaleMatrix) val bitmap = Bitmap.createBitmap( - toTransform.width, toTransform.height, toTransform.config + toTransform.width, toTransform.height, toTransform.config ?: Bitmap.Config.ARGB_8888 ) val canvas = Canvas(bitmap) canvas.clipPath(it) diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/utils/gmail/GmailContract.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/utils/gmail/GmailContract.kt index b045b45a..a40bf0e7 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/utils/gmail/GmailContract.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/utils/gmail/GmailContract.kt @@ -79,11 +79,12 @@ object GmailContract { PackageManager.GET_PROVIDERS or PackageManager.GET_PERMISSIONS ) var allowRead = false - if (info.permissions != null) { + val permissions = info.permissions + if (permissions != null) { var i = 0 - val len = info.permissions.size + val len = permissions.size while (i < len) { - val perm = info.permissions[i] + val perm = permissions[i] if (PERMISSION == perm.name && perm.protectionLevel < PermissionInfo.PROTECTION_SIGNATURE) { allowRead = true break @@ -91,11 +92,12 @@ object GmailContract { i++ } } - if (allowRead && info.providers != null) { + val providers = info.providers + if (allowRead && providers != null) { var i = 0 - val len = info.providers.size + val len = providers.size while (i < len) { - val provider = info.providers[i] + val provider = providers[i] if (AUTHORITY == provider.authority && TextUtils.equals(PERMISSION, provider.readPermission) ) { diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/utils/remoteviews/FlagDisabledRemoteViews.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/utils/remoteviews/FlagDisabledRemoteViews.kt index 74442171..7e81acef 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/utils/remoteviews/FlagDisabledRemoteViews.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/utils/remoteviews/FlagDisabledRemoteViews.kt @@ -1,5 +1,6 @@ package com.kieronquinn.app.smartspacer.utils.remoteviews +import android.os.Build import android.widget.RemoteViews import android.widget.RemoteViewsHidden @@ -11,17 +12,22 @@ import android.widget.RemoteViewsHidden * * This should only be used as the root level node for the list adapter, since it is not required * anywhere else. + * + * Patched in Android 16, so now only used on Android < 13. Android >= 13 uses the recommended + * method. */ -class FlagDisabledRemoteViews( - landscape: RemoteViews, portrait: RemoteViews -) : RemoteViewsHidden(landscape, portrait) { +class FlagDisabledRemoteViews: RemoteViewsHidden { + + constructor(landscape: RemoteViews, portrait: RemoteViews): super(landscape, portrait) + constructor(remoteViews: RemoteViews): super(remoteViews) companion object { private const val FLAG_WIDGET_IS_COLLECTION_CHILD = 2 } override fun addFlags(flags: Int) { - if(flags == FLAG_WIDGET_IS_COLLECTION_CHILD) { + if(flags == FLAG_WIDGET_IS_COLLECTION_CHILD && + Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { return } super.addFlags(flags) diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/utils/remoteviews/RemoteAdapter.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/utils/remoteviews/RemoteAdapter.kt index edca2464..d305712a 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/utils/remoteviews/RemoteAdapter.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/utils/remoteviews/RemoteAdapter.kt @@ -7,7 +7,7 @@ import com.kieronquinn.app.smartspacer.sdk.IRemoteAdapter import com.kieronquinn.app.smartspacer.sdk.model.RemoteAdapterItem import com.kieronquinn.app.smartspacer.sdk.model.RemoteOnClickResponse import com.kieronquinn.app.smartspacer.utils.extensions.extractOnClickResponse -import com.kieronquinn.app.smartspacer.utils.extensions.getActionsIncludingNested +import com.kieronquinn.app.smartspacer.utils.extensions.getActionsIncludingSized import com.kieronquinn.app.smartspacer.utils.extensions.getResourceNameOrNull import com.kieronquinn.app.smartspacer.utils.extensions.isOnClickResponse import com.kieronquinn.app.smartspacer.utils.extensions.toRemoteResponse @@ -19,7 +19,7 @@ class RemoteAdapter( override fun getViewAt(index: Int): Bundle? { val remoteViews = factoryWrapper.getViewAt(index) ?: return null - val actions = remoteViews.getActionsIncludingNested() + val actions = remoteViews.getActionsIncludingSized() .filter { it.isOnClickResponse() }.map { it.extractOnClickResponse() }.mapNotNull { if(it.first == 0) return@mapNotNull null val viewId = context.resources.getResourceNameOrNull(it.first) diff --git a/app/src/main/java/com/kieronquinn/app/smartspacer/utils/remoteviews/RemoteCollectionItemsWrapper.kt b/app/src/main/java/com/kieronquinn/app/smartspacer/utils/remoteviews/RemoteCollectionItemsWrapper.kt index 40a1a3cc..2490aa98 100644 --- a/app/src/main/java/com/kieronquinn/app/smartspacer/utils/remoteviews/RemoteCollectionItemsWrapper.kt +++ b/app/src/main/java/com/kieronquinn/app/smartspacer/utils/remoteviews/RemoteCollectionItemsWrapper.kt @@ -10,7 +10,7 @@ import com.kieronquinn.app.smartspacer.sdk.IRemoteAdapter import com.kieronquinn.app.smartspacer.sdk.model.RemoteAdapterItem import com.kieronquinn.app.smartspacer.sdk.model.RemoteOnClickResponse import com.kieronquinn.app.smartspacer.utils.extensions.extractOnClickResponse -import com.kieronquinn.app.smartspacer.utils.extensions.getActionsIncludingNested +import com.kieronquinn.app.smartspacer.utils.extensions.getActionsIncludingSized import com.kieronquinn.app.smartspacer.utils.extensions.getResourceNameOrNull import com.kieronquinn.app.smartspacer.utils.extensions.isOnClickResponse import com.kieronquinn.app.smartspacer.utils.extensions.toRemoteResponse @@ -27,7 +27,7 @@ class RemoteCollectionItemsWrapper( override fun getViewAt(index: Int): Bundle { val remoteViews = items.getItemView(index) - val actions = remoteViews.getActionsIncludingNested() + val actions = remoteViews.getActionsIncludingSized() .filter { it.isOnClickResponse() }.map { it.extractOnClickResponse() }.mapNotNull { if(it.first == 0) return@mapNotNull null val viewId = context.resources.getResourceNameOrNull(it.first) diff --git a/app/src/main/res/drawable/remoteviews_rounded_corners.xml b/app/src/main/res/drawable/remoteviews_rounded_corners.xml new file mode 100644 index 00000000..613225ab --- /dev/null +++ b/app/src/main/res/drawable/remoteviews_rounded_corners.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_expanded_bottom_sheet_add_widget.xml b/app/src/main/res/layout/fragment_expanded_bottom_sheet_add_widget.xml index 4030e516..2ea27478 100644 --- a/app/src/main/res/layout/fragment_expanded_bottom_sheet_add_widget.xml +++ b/app/src/main/res/layout/fragment_expanded_bottom_sheet_add_widget.xml @@ -4,8 +4,8 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - android:orientation="vertical" android:clipToPadding="false" + android:orientation="vertical" tools:background="@drawable/bottom_sheet_rounded_bg_dark" tools:layout_gravity="bottom"> @@ -15,9 +15,9 @@ android:layout_height="wrap_content" android:gravity="center" android:padding="@dimen/margin_16" - android:text="@string/expanded_add_widget_title" android:textAppearance="@style/TextAppearance.AppCompat.Large.Smartspacer" - app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toTopOf="parent" + tools:text="@string/expanded_add_widget_title" /> + \ No newline at end of file diff --git a/app/src/main/res/layout/remoteviews_wrapper_padding_large.xml b/app/src/main/res/layout/remoteviews_wrapper_padding_large.xml new file mode 100644 index 00000000..fe533458 --- /dev/null +++ b/app/src/main/res/layout/remoteviews_wrapper_padding_large.xml @@ -0,0 +1,9 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/remoteviews_wrapper_padding_medium.xml b/app/src/main/res/layout/remoteviews_wrapper_padding_medium.xml new file mode 100644 index 00000000..46af0a84 --- /dev/null +++ b/app/src/main/res/layout/remoteviews_wrapper_padding_medium.xml @@ -0,0 +1,9 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/remoteviews_wrapper_padding_none.xml b/app/src/main/res/layout/remoteviews_wrapper_padding_none.xml new file mode 100644 index 00000000..dbb88062 --- /dev/null +++ b/app/src/main/res/layout/remoteviews_wrapper_padding_none.xml @@ -0,0 +1,8 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/remoteviews_wrapper_padding_small.xml b/app/src/main/res/layout/remoteviews_wrapper_padding_small.xml new file mode 100644 index 00000000..ea9c1dfe --- /dev/null +++ b/app/src/main/res/layout/remoteviews_wrapper_padding_small.xml @@ -0,0 +1,9 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/remoteviews_wrapper_rounded.xml b/app/src/main/res/layout/remoteviews_wrapper_rounded.xml new file mode 100644 index 00000000..fcfb2259 --- /dev/null +++ b/app/src/main/res/layout/remoteviews_wrapper_rounded.xml @@ -0,0 +1,11 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/smartspace_view_remoteviews.xml b/app/src/main/res/layout/smartspace_view_remoteviews.xml new file mode 100644 index 00000000..3011a145 --- /dev/null +++ b/app/src/main/res/layout/smartspace_view_remoteviews.xml @@ -0,0 +1,12 @@ + + \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_graph_configure_target_widget.xml b/app/src/main/res/navigation/nav_graph_configure_target_widget.xml new file mode 100644 index 00000000..5f65a7e0 --- /dev/null +++ b/app/src/main/res/navigation/nav_graph_configure_target_widget.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_graph_setup_target_widget.xml b/app/src/main/res/navigation/nav_graph_setup_target_widget.xml new file mode 100644 index 00000000..978e2984 --- /dev/null +++ b/app/src/main/res/navigation/nav_graph_setup_target_widget.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-af-rZA/strings.xml b/app/src/main/res/values-af-rZA/strings.xml index 62dbc5a4..5aab33ed 100644 --- a/app/src/main/res/values-af-rZA/strings.xml +++ b/app/src/main/res/values-af-rZA/strings.xml @@ -710,7 +710,6 @@ Drag to rearrange Reset Add Widget - %1s (%1d x %1d) %1d widget %1d widgets Battery diff --git a/app/src/main/res/values-ar-rSA/strings.xml b/app/src/main/res/values-ar-rSA/strings.xml index b2ec97c8..b954178c 100644 --- a/app/src/main/res/values-ar-rSA/strings.xml +++ b/app/src/main/res/values-ar-rSA/strings.xml @@ -710,7 +710,6 @@ Drag to rearrange Reset Add Widget - %1s (%1d x %1d) %1d widget %1d widgets Battery diff --git a/app/src/main/res/values-ca-rES/strings.xml b/app/src/main/res/values-ca-rES/strings.xml index 24f60d35..2edc4099 100644 --- a/app/src/main/res/values-ca-rES/strings.xml +++ b/app/src/main/res/values-ca-rES/strings.xml @@ -710,7 +710,6 @@ Drag to rearrange Reset Add Widget - %1s (%1d x %1d) %1d widget %1d widgets Battery diff --git a/app/src/main/res/values-cs-rCZ/strings.xml b/app/src/main/res/values-cs-rCZ/strings.xml index 62dbc5a4..5aab33ed 100644 --- a/app/src/main/res/values-cs-rCZ/strings.xml +++ b/app/src/main/res/values-cs-rCZ/strings.xml @@ -710,7 +710,6 @@ Drag to rearrange Reset Add Widget - %1s (%1d x %1d) %1d widget %1d widgets Battery diff --git a/app/src/main/res/values-da-rDK/strings.xml b/app/src/main/res/values-da-rDK/strings.xml index 99980f29..043d7024 100644 --- a/app/src/main/res/values-da-rDK/strings.xml +++ b/app/src/main/res/values-da-rDK/strings.xml @@ -710,7 +710,6 @@ Drag to rearrange Reset Add Widget - %1s (%1d x %1d) %1d widget %1d widgets Battery diff --git a/app/src/main/res/values-de-rDE/strings.xml b/app/src/main/res/values-de-rDE/strings.xml index 8d45e7bf..133626f4 100644 --- a/app/src/main/res/values-de-rDE/strings.xml +++ b/app/src/main/res/values-de-rDE/strings.xml @@ -711,7 +711,6 @@ Möglicherweise muss auch GPS aktiviert sein, damit Geräte und Verbindungen im Ziehen zum Anordnen Zurücksetzen Widget hinzufügen - %1s (%1d x %1d) %1d Widget %1d Widgets Akku diff --git a/app/src/main/res/values-el-rGR/strings.xml b/app/src/main/res/values-el-rGR/strings.xml index b6385b22..a49303b3 100644 --- a/app/src/main/res/values-el-rGR/strings.xml +++ b/app/src/main/res/values-el-rGR/strings.xml @@ -710,7 +710,6 @@ Drag to rearrange Reset Add Widget - %1s (%1d x %1d) %1d widget %1d widgets Battery diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml index 62dbc5a4..5aab33ed 100644 --- a/app/src/main/res/values-en-rGB/strings.xml +++ b/app/src/main/res/values-en-rGB/strings.xml @@ -710,7 +710,6 @@ Drag to rearrange Reset Add Widget - %1s (%1d x %1d) %1d widget %1d widgets Battery diff --git a/app/src/main/res/values-en-rUS/strings.xml b/app/src/main/res/values-en-rUS/strings.xml index ea76958b..df5bf4d5 100644 --- a/app/src/main/res/values-en-rUS/strings.xml +++ b/app/src/main/res/values-en-rUS/strings.xml @@ -710,7 +710,6 @@ Drag to rearrange Reset Add Widget - %1s (%1d x %1d) %1d widget %1d widgets Battery diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index 13c7d053..435ae6d6 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -710,7 +710,6 @@ Arrastra para reordenar Restablecer Agregar widget - %1s (%1d x %1d) %1d widget %1d widgets Batería diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml index 62dbc5a4..5aab33ed 100644 --- a/app/src/main/res/values-fi-rFI/strings.xml +++ b/app/src/main/res/values-fi-rFI/strings.xml @@ -710,7 +710,6 @@ Drag to rearrange Reset Add Widget - %1s (%1d x %1d) %1d widget %1d widgets Battery diff --git a/app/src/main/res/values-fr-rFR/strings.xml b/app/src/main/res/values-fr-rFR/strings.xml index 16daaba4..91106728 100644 --- a/app/src/main/res/values-fr-rFR/strings.xml +++ b/app/src/main/res/values-fr-rFR/strings.xml @@ -18,9 +18,9 @@ Paramètres Autorisez les paramètres restreints en ouvrant le menu « ... » Depuis Android 13, Google interdit par défaut l\'accès aux services d\'accessibilité aux applications téléchargées en dehors du Play Store en forçant une vérification de sécurité dans les paramètres.\n\nSi vous n\'arrivez pas à activer le service d\'accessibilité, vous devez ouvrir les Infos sur l\'appli et autoriser les paramètres restreints. -
Si un message vous indique que les paramètres sont restreints, vous devez alors désinstaller Smartspacer et le réinstaller à l'aider d'une application utilisant l'installation par session comme Split APKs Installer (SAI) ou l'application APKMirror. Vous DEVEZ désinstaller l'application AVANT de la réinstaller.

Un autre moyen de procéder est d'activer le mode amélioré (nécessite Shizuku ou un accès root).]]>
+
Si un message vous indique que les paramètres sont restreints, vous devez alors désinstaller Smartspacer et le réinstaller à l\'aider d\'une application utilisant l\'installation par session comme Split APKs Installer (SAI) ou l\'application APKMirror. Vous DEVEZ désinstaller l\'application AVANT de la réinstaller.

Un autre moyen de procéder est d\'activer le mode amélioré (nécessite Shizuku ou un accès root).]]>
Depuis Android 13, Google interdit par défaut l\'accès aux notifications aux applications téléchargées en dehors du Play Store en forçant une vérification de sécurité dans les paramètres.\n\nSi vous n\'arrivez pas à accorder l\'accès aux notifications, vous devez ouvrir les Infos sur l\'appli et autoriser les paramètres restreints. -
Si un message vous indique que les paramètres sont restreints, vous devez alors désinstaller Smartspacer et le réinstaller à l'aider d'une application utilisant l'installation par session comme Split APKs Installer (SAI) ou l'application APKMirror. Vous DEVEZ désinstaller l'application AVANT de la réinstaller.

Un autre moyen de procéder est d'activer le mode amélioré (nécessite Shizuku ou un accès root).]]>
+
Si un message vous indique que les paramètres sont restreints, vous devez alors désinstaller Smartspacer et le réinstaller à l\'aider d\'une application utilisant l\'installation par session comme Split APKs Installer (SAI) ou l\'application APKMirror. Vous DEVEZ désinstaller l\'application AVANT de la réinstaller.

Un autre moyen de procéder est d\'activer le mode amélioré (nécessite Shizuku ou un accès root).]]>
Ouvrir les Infos sur l\'appli Accorder l\'autorisation avec Shizuku Contenus masqués @@ -452,7 +452,7 @@ Collecte de données Données et rapports de plantage - Pour aider à la correction des bugs, Smartspacer peut envoyer les rapports de plantage à Firebase Crashlytics, avec des données statistiques de base (sur les lancements de l'application).
Toutes ces données sont anonymes et leur collecte est optionnelle. Plus de détails et règles de confidentialité ici]]>
+ Pour aider à la correction des bugs, Smartspacer peut envoyer les rapports de plantage à Firebase Crashlytics, avec des données statistiques de base (sur les lancements de l\'application).
Toutes ces données sont anonymes et leur collecte est optionnelle. Plus de détails et règles de confidentialité ici]]>
Autoriser Interdire @@ -710,7 +710,6 @@ Faites glisser pour réorganiser Réinitialiser Ajouter un widget - %1s (%1d x %1d) %1d widget %1d widgets Batterie diff --git a/app/src/main/res/values-hu-rHU/strings.xml b/app/src/main/res/values-hu-rHU/strings.xml index f4d3e451..f7e84c6f 100644 --- a/app/src/main/res/values-hu-rHU/strings.xml +++ b/app/src/main/res/values-hu-rHU/strings.xml @@ -710,7 +710,6 @@ Drag to rearrange Reset Add Widget - %1s (%1d x %1d) %1d widget %1d widgets Battery diff --git a/app/src/main/res/values-in-rID/strings.xml b/app/src/main/res/values-in-rID/strings.xml index 0d400fc1..896969b4 100644 --- a/app/src/main/res/values-in-rID/strings.xml +++ b/app/src/main/res/values-in-rID/strings.xml @@ -710,7 +710,6 @@ Drag to rearrange Reset Add Widget - %1s (%1d x %1d) %1d widget %1d widgets Battery diff --git a/app/src/main/res/values-it-rIT/strings.xml b/app/src/main/res/values-it-rIT/strings.xml index 49735683..4eb8ffa6 100644 --- a/app/src/main/res/values-it-rIT/strings.xml +++ b/app/src/main/res/values-it-rIT/strings.xml @@ -18,9 +18,9 @@ Impostazioni Consenti impostazioni limitate nel menu di overflow Da Android 13, Google ha aggiunto un controllo di sicurezza alle impostazioni, impedendo alle app che sono state installate tramite sideload di utilizzare i servizi d\'accessibilità per impostazione predefinita.\n\nSe vedi un messaggio di impostazioni limitate quando cerchi di abilitare il servizio d\'accessibilità, è necessario aprire Informazioni App per consentire le impostazioni limitate. -
Se vedi un messaggio riguardo a impostazioni limitate, è necessario disinstallare Smartspacer e reinstallarlo utilizzando un'app basata su installer a sessione come Split APKs Installer (SAI) o l'app APKMirror. Aggiornare l'app (senza disinstallarla) NON è sufficiente.

In alternativa, con la Modalità Avanzata abilitata, Smartspacer aggirerà questo requisito (è necessario Shizuku o root).]]>
+
Se vedi un messaggio riguardo a impostazioni limitate, è necessario disinstallare Smartspacer e reinstallarlo utilizzando un\'app basata su installer a sessione come Split APKs Installer (SAI) o l\'app APKMirror. Aggiornare l\'app (senza disinstallarla) NON è sufficiente.

In alternativa, con la Modalità Avanzata abilitata, Smartspacer aggirerà questo requisito (è necessario Shizuku o root).]]>
Da Android 13, Google ha aggiunto un controllo di sicurezza alle impostazioni, impedendo alle app che sono state installate tramite sideload di inviare notifiche per impostazione predefinita.\n\nSe vedi un messaggio di impostazioni limitate quando cerchi di abilitare le notifiche, è necessario aprire Informazioni App per consentire le impostazioni limitate. -
Se vedi un messaggio riguardo a impostazioni limitate, è necessario disinstallare Smartspacer e reinstallarlo utilizzando un'app basata su installer di sessione come Split APKs Installer (SAI) o l'app APKMirror. Aggiornare l'app (senza disinstallarla) NON è sufficiente.

In alternativa, con la Modalità Avanzata abilitata, Smartspacer aggirerà questo requisito (è necessario Shizuku o root).]]>
+
Se vedi un messaggio riguardo a impostazioni limitate, è necessario disinstallare Smartspacer e reinstallarlo utilizzando un\'app basata su installer di sessione come Split APKs Installer (SAI) o l\'app APKMirror. Aggiornare l\'app (senza disinstallarla) NON è sufficiente.

In alternativa, con la Modalità Avanzata abilitata, Smartspacer aggirerà questo requisito (è necessario Shizuku o root).]]>
Apri le informazioni dell\'app Concedi il permesso con Shizuku Contenuto nascosto @@ -452,7 +452,7 @@ Analytics Analitiche e Report dei Crash - Per agevolare la risoluzione di bug, Smartspacer può inviare crash report a Firebase Crashlytics, insieme ad altre informazioni analitiche di base (lanci dell'app).
Questi dati sono tutti anonimi, e sono assolutamente facoltativi, maggiori dettagli e informativa sulla privacy qui]]>
+ Per agevolare la risoluzione di bug, Smartspacer può inviare crash report a Firebase Crashlytics, insieme ad altre informazioni analitiche di base (lanci dell\'app).
Questi dati sono tutti anonimi, e sono assolutamente facoltativi, maggiori dettagli e informativa sulla privacy qui]]>
Consenti Nega @@ -495,7 +495,7 @@ Aggiungi widget Aggiungi Smartspacer alla Schermata Home - Lock Screen Widgets per aggiungere il widget.]]> + Lock Screen Widgets per aggiungere il widget.]]> Requisiti I Requisiti consentono di personalizzare quando un Target o una Complicazione devono essere visualizzati in Smartspace. Con essi è possibile creare scenari in cui le informazioni utili vengono mostrate in Smartspace. @@ -710,7 +710,6 @@ Trascina per riorganizzare Resetta Aggiungi Widget - %1s (%1d × %1d) %1d widget %1d widget Batteria @@ -890,7 +889,7 @@ Icone Material Icons (Licenza Apache 2.0)
Material Design Icons (Licenza Pictogrammers Free)
Success di Rohith R Krishnan (Licenza Lottie Simple)]]>
SDK - Licenza Apache 2.0]]> + Licenza Apache 2.0]]> Traduttori Selettore del Colore di Sfondo diff --git a/app/src/main/res/values-iw-rIL/strings.xml b/app/src/main/res/values-iw-rIL/strings.xml index 62dbc5a4..5aab33ed 100644 --- a/app/src/main/res/values-iw-rIL/strings.xml +++ b/app/src/main/res/values-iw-rIL/strings.xml @@ -710,7 +710,6 @@ Drag to rearrange Reset Add Widget - %1s (%1d x %1d) %1d widget %1d widgets Battery diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml index f8516879..19dcbf91 100644 --- a/app/src/main/res/values-ja-rJP/strings.xml +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -710,7 +710,6 @@ ドラッグで並べ替え リセット ウィジェットを追加 - %1s (%1d x %1d) %1d 個のウィジェット %1d 個のウィジェット バッテリー diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml index 64740acf..68eb3b1c 100644 --- a/app/src/main/res/values-ko-rKR/strings.xml +++ b/app/src/main/res/values-ko-rKR/strings.xml @@ -710,7 +710,6 @@ Drag to rearrange 초기화 위젯 추가 - %1s (%1d x %1d) %1d widget %1d widgets Battery diff --git a/app/src/main/res/values-nl-rNL/strings.xml b/app/src/main/res/values-nl-rNL/strings.xml index 62dbc5a4..5aab33ed 100644 --- a/app/src/main/res/values-nl-rNL/strings.xml +++ b/app/src/main/res/values-nl-rNL/strings.xml @@ -710,7 +710,6 @@ Drag to rearrange Reset Add Widget - %1s (%1d x %1d) %1d widget %1d widgets Battery diff --git a/app/src/main/res/values-no-rNO/strings.xml b/app/src/main/res/values-no-rNO/strings.xml index 1005f9f7..2d2877b6 100644 --- a/app/src/main/res/values-no-rNO/strings.xml +++ b/app/src/main/res/values-no-rNO/strings.xml @@ -710,7 +710,6 @@ Dra for å endre rekkefølge Tilbakestill Legg til modul - %1s (%1d × %1d) %1d modul %1d moduler Batteri diff --git a/app/src/main/res/values-pl-rPL/strings.xml b/app/src/main/res/values-pl-rPL/strings.xml index 78bfc552..ebb72f22 100644 --- a/app/src/main/res/values-pl-rPL/strings.xml +++ b/app/src/main/res/values-pl-rPL/strings.xml @@ -710,7 +710,6 @@ Drag to rearrange Reset Dodaj widżet - %1s (%1d x %1d) %1d widget %1d widgets Battery diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index ca05142f..080962a8 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -710,7 +710,6 @@ Arraste para reorganizar Redefinir Adicionar Widget - %1s (%1d x %1d) %1d widget %1d widgets Bateria diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index 62dbc5a4..5aab33ed 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -710,7 +710,6 @@ Drag to rearrange Reset Add Widget - %1s (%1d x %1d) %1d widget %1d widgets Battery diff --git a/app/src/main/res/values-ro-rRO/strings.xml b/app/src/main/res/values-ro-rRO/strings.xml index 62c25758..31130845 100644 --- a/app/src/main/res/values-ro-rRO/strings.xml +++ b/app/src/main/res/values-ro-rRO/strings.xml @@ -710,7 +710,6 @@ Drag to rearrange Reset Add Widget - %1s (%1d x %1d) %1d widget %1d widgets Battery diff --git a/app/src/main/res/values-ru-rRU/strings.xml b/app/src/main/res/values-ru-rRU/strings.xml index bfc411f2..e7ebebfe 100644 --- a/app/src/main/res/values-ru-rRU/strings.xml +++ b/app/src/main/res/values-ru-rRU/strings.xml @@ -710,7 +710,6 @@ Перетащите для изменения порядка Сброс Добавить виджет - %1s (%1d x %1d) %1d виджет %1d виджетов Battery diff --git a/app/src/main/res/values-sr-rSP/strings.xml b/app/src/main/res/values-sr-rSP/strings.xml index 62dbc5a4..5aab33ed 100644 --- a/app/src/main/res/values-sr-rSP/strings.xml +++ b/app/src/main/res/values-sr-rSP/strings.xml @@ -710,7 +710,6 @@ Drag to rearrange Reset Add Widget - %1s (%1d x %1d) %1d widget %1d widgets Battery diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index 62dbc5a4..5aab33ed 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -710,7 +710,6 @@ Drag to rearrange Reset Add Widget - %1s (%1d x %1d) %1d widget %1d widgets Battery diff --git a/app/src/main/res/values-tr-rTR/strings.xml b/app/src/main/res/values-tr-rTR/strings.xml index 32b81d5e..8b06c25f 100644 --- a/app/src/main/res/values-tr-rTR/strings.xml +++ b/app/src/main/res/values-tr-rTR/strings.xml @@ -18,9 +18,9 @@ Ayarlar Taşan menüsünde kısıtlı ayarlara izin ver Android 13\'ten beri Google, ayarlara bir güvenlik kontrolü ekleyerek yandan yüklenen uygulamaların varsayılan olarak Erişilebilirlik Hizmetlerini kullanmasına izin verilmesini engelliyor.\n\nErişilebilirlik hizmetini etkinleştirmeye çalışırken kısıtlanmış bir ayarla ilgili bir mesaj görürseniz, kısıtlanmış ayarlara izin veren Uygulama Bilgisi\'ni açmanız gerekir. -
Eğer kısıtlanmış ayarlarla ilgili bir mesaj görürseniz Smartspacer'i silip Split APKs Installer (SAI) ya da APKMirror gibi bir oturum yükleyici-tabanlı uygulama ile tekrar yüklemelisiniz. Üstüne yükleme(yeniden yüklemesiz) yeterli DEĞİLDİR.

Alternatif olarak, Gelişmiş Mod açıkken Smartspacer bu gereksinimi atlayacaktır(Shizuku veya root ile yapılamaz).]]>
+
Eğer kısıtlanmış ayarlarla ilgili bir mesaj görürseniz Smartspacer\'i silip Split APKs Installer (SAI) ya da APKMirror gibi bir oturum yükleyici-tabanlı uygulama ile tekrar yüklemelisiniz. Üstüne yükleme(yeniden yüklemesiz) yeterli DEĞİLDİR.

Alternatif olarak, Gelişmiş Mod açıkken Smartspacer bu gereksinimi atlayacaktır(Shizuku veya root ile yapılamaz).]]>
Android 13\'ten beri Google, ayarlara bir güvenlik kontrolü ekleyerek yandan yüklenen uygulamaların varsayılan olarak Bildirim Erişiminin kullanmasına izin verilmesini engelliyor.\n\nBildirim erişimini verdikten sonra kısitlanmış erişim ile ilgili bir mesaj görürseniz, Uygulama Bilgileri\'ni açıp kısıtlanmış ayarları etkinleştirmeniz gerekir. -
Eğer kısıtlanmış ayarlarla ilgili bir mesaj görürseniz Smartspacer'i silip Split APKs Installer (SAI) ya da APKMirror gibi bir oturum yükleyici-tabanlı uygulama ile tekrar yüklemelisiniz. Üstüne yükleme(yeniden yüklemesiz) yeterli DEĞİLDİR.

Alternatif olarak, Gelişmiş Mod açıkken Smartspacer bu gereksinimi atlayacaktır(Shizuku veya root ile yapılamaz).]]>
+
Eğer kısıtlanmış ayarlarla ilgili bir mesaj görürseniz Smartspacer\'i silip Split APKs Installer (SAI) ya da APKMirror gibi bir oturum yükleyici-tabanlı uygulama ile tekrar yüklemelisiniz. Üstüne yükleme(yeniden yüklemesiz) yeterli DEĞİLDİR.

Alternatif olarak, Gelişmiş Mod açıkken Smartspacer bu gereksinimi atlayacaktır(Shizuku veya root ile yapılamaz).]]>
Uygulama Bilgilerini Aç Shizuku ile izin ver İçerikler gizli @@ -710,7 +710,6 @@ Drag to rearrange Reset Add Widget - %1s (%1d x %1d) %1d widget %1d widgets Battery diff --git a/app/src/main/res/values-uk-rUA/strings.xml b/app/src/main/res/values-uk-rUA/strings.xml index c363cca6..a841c370 100644 --- a/app/src/main/res/values-uk-rUA/strings.xml +++ b/app/src/main/res/values-uk-rUA/strings.xml @@ -710,7 +710,6 @@ Drag to rearrange Reset Add Widget - %1s (%1d x %1d) %1d widget %1d widgets Battery diff --git a/app/src/main/res/values-vi-rVN/strings.xml b/app/src/main/res/values-vi-rVN/strings.xml index ad46283f..187737ef 100644 --- a/app/src/main/res/values-vi-rVN/strings.xml +++ b/app/src/main/res/values-vi-rVN/strings.xml @@ -710,7 +710,6 @@ Drag to rearrange Reset Add Widget - %1s (%1d x %1d) %1d widget %1d widgets Battery diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 35aac54f..b5043001 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -710,7 +710,6 @@ 拖拽以重排 重置 添加小组件 - %1s(%1d x %1d) %1d 个小组件 %1d 个小组件 Battery diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 62ac3c2b..e64c7d36 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -710,7 +710,6 @@ 拖曳以重新排列 重置 新增小工具 - %1s(%1d x %1d) %1d 個小工具 %1d 個小工具 Battery diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index cab1c852..655c9810 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -44,4 +44,14 @@ com.google.android.apps.pixel.familyspace/com.google.android.apps.pixel.familyspace.widget.FamilySpaceWidgetProvider + + @string/target_widget_settings_limitations_bull1 + @string/target_widget_settings_limitations_bull2 + @string/target_widget_settings_limitations_bull3 + @string/target_widget_settings_limitations_bull4 + @string/target_widget_settings_limitations_bull5 + @string/target_widget_settings_limitations_bull6 + @string/target_widget_settings_limitations_bull7 + @string/target_widget_settings_limitations_bull8 + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cd5b5133..b75c31c4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -494,6 +494,8 @@ Allows showing a button in addition to the title & subtitle. List Allows displaying a short list in addition to the title & subtitle + Custom Content + Allows displaying custom content not conforming to a template (RemoteViews) Sports Allows displaying a score for two teams in addition to the title & subtitle @@ -765,6 +767,7 @@ Hide the button to add custom widgets: %1s Never When replacing Google Discover + When locked Always Enable Multi-Column On larger width devices, display Smartspacer Targets, extra content and custom widgets horizontally as well as vertically, where space is available @@ -807,7 +810,7 @@ Drag to rearrange Reset Add Widget - %1s (%1d x %1d) + %1s (%1d x %1d)%1s %1d widget %1d widgets Battery @@ -1020,6 +1023,11 @@ Shows the current time.\nWarning: This Complication may cause battery drain. Shows the current time + + Clock + Shows the current time.\nWarning: This Target may cause battery drain. + Shows the current time + Calendar Shows Calendar events from selected calendars @@ -1127,6 +1135,39 @@ This option is not compatible with your device Grant the Camera permission + + Widget + Experimental: Show a 4x1 widget + Shows %1s from %1s + Widgets not Supported + Widgets are not supported here + Widget not compatible + Sorry, this widget is not compatible + Select Widget + Settings + Configure widget options + Due to system limitations, there are some restrictions on widgets in Smartspace: + Widgets are not supported in Native Smartspace on Android versions older than 16 or 15 QPR2 + Widgets will be sized to fit Smartspace - 4x1 widgets work best + Widgets containing lists are not supported. + Widgets will be displayed on the Always on Display if shown in Native Smartspace - which may cause burn in. + Some interactivity may be limited or non-functional. + When the device is locked, you may need to unlock manually to complete interactions with some widgets. + Some widgets may not look right. + Some widgets may not work at all and will show \"Couldn\'t add widget\", or nothing at all. + These restrictions cannot be worked around, please do not report them as bugs. + Widget + %1s from %1s\n\nSelected widget cannot be changed after adding, remove this Target and re-add to change widget + Padding + The amount of padding above & below the widget: %1s + Round Corners + Apply rounded corners to this widget. May not work with all widgets. + Minimum + Small + Medium + Large + Disabled + Gmail Unread Show the number of unread Gmail emails from selected labels diff --git a/build.gradle b/build.gradle index 0b5fa1ee..85c41aaa 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ import com.vanniktech.maven.publish.SonatypeHost buildscript { - ext.nav_version = "2.7.7" + ext.nav_version = "2.8.5" ext.protobuf_version = '0.9.0' ext.refine_version = '4.4.0' dependencies { @@ -13,12 +13,12 @@ buildscript { // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '8.5.2' apply false - id 'com.android.library' version '8.5.2' apply false - id 'org.jetbrains.kotlin.android' version '1.9.10' apply false + id 'com.android.application' version '8.7.3' apply false + id 'com.android.library' version '8.7.3' apply false + id 'org.jetbrains.kotlin.android' version '1.9.24' apply false id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin' version '2.0.1' apply false id 'dev.rikka.tools.refine' version "$refine_version" apply false - id 'com.google.devtools.ksp' version '1.9.10-1.0.13' apply false + id 'com.google.devtools.ksp' version '1.9.24-1.0.20' apply false id "com.google.gms.google-services" version "4.4.2" apply false id "com.google.firebase.crashlytics" version "3.0.2" apply false id "com.vanniktech.maven.publish" version "0.29.0" apply false diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3f528970..0f4b05c4 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Aug 27 04:42:21 BST 2024 +#Sun Dec 22 22:35:27 GMT 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/sdk-client/build.gradle b/sdk-client/build.gradle index dd104e6c..dc9f0457 100644 --- a/sdk-client/build.gradle +++ b/sdk-client/build.gradle @@ -7,7 +7,7 @@ plugins { android { namespace 'com.kieronquinn.app.smartspacer.sdk.client' - compileSdk 34 + compileSdk 35 defaultConfig { minSdk 29 @@ -15,6 +15,7 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" + buildConfigField "int", "SDK_VERSION", "2" } buildFeatures { @@ -38,18 +39,18 @@ android { } dependencies { - api('com.kieronquinn.smartspacer:sdk-core:1.0.5') - implementation('androidx.core:core-ktx:1.13.1') - implementation('androidx.fragment:fragment-ktx:1.8.2') - implementation('androidx.constraintlayout:constraintlayout:2.1.4') - implementation('androidx.lifecycle:lifecycle-service:2.8.4') - implementation('androidx.lifecycle:lifecycle-runtime-ktx:2.8.4') - implementation('org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3') + api('com.kieronquinn.smartspacer:sdk-core:1.1.1') + implementation('androidx.core:core-ktx:1.15.0') + implementation('androidx.fragment:fragment-ktx:1.8.5') + implementation('androidx.constraintlayout:constraintlayout:2.2.0') + implementation('androidx.lifecycle:lifecycle-service:2.8.7') + implementation('androidx.lifecycle:lifecycle-runtime-ktx:2.8.7') + implementation('org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1') implementation('com.github.skydoves:balloon:1.4.7') } mavenPublishing { - coordinates("com.kieronquinn.smartspacer", "sdk-client", "1.0.11") + coordinates("com.kieronquinn.smartspacer", "sdk-client", "1.1") pom { name = "Smartspacer Client SDK" diff --git a/sdk-client/src/main/java/com/kieronquinn/app/smartspacer/sdk/client/SmartspacerClient.kt b/sdk-client/src/main/java/com/kieronquinn/app/smartspacer/sdk/client/SmartspacerClient.kt index 67158602..3f249276 100644 --- a/sdk-client/src/main/java/com/kieronquinn/app/smartspacer/sdk/client/SmartspacerClient.kt +++ b/sdk-client/src/main/java/com/kieronquinn/app/smartspacer/sdk/client/SmartspacerClient.kt @@ -200,9 +200,14 @@ class SmartspacerClient(context: Context) { service = null } } - val success = applicationContext.bindService( - getServiceIntent(), serviceConnection, Context.BIND_AUTO_CREATE - ) + val success = try { + applicationContext.bindService( + getServiceIntent(), serviceConnection, Context.BIND_AUTO_CREATE + ) + }catch (e: IllegalStateException) { + //Rare case where Android thinks there's too many bind requests, despite locking + false + } if(!success){ hasResumed = true it.resume(null) diff --git a/sdk-client/src/main/java/com/kieronquinn/app/smartspacer/sdk/client/utils/Extensions+RemoteViews.kt b/sdk-client/src/main/java/com/kieronquinn/app/smartspacer/sdk/client/utils/Extensions+RemoteViews.kt new file mode 100644 index 00000000..07774b97 --- /dev/null +++ b/sdk-client/src/main/java/com/kieronquinn/app/smartspacer/sdk/client/utils/Extensions+RemoteViews.kt @@ -0,0 +1,16 @@ +package com.kieronquinn.app.smartspacer.sdk.client.utils + +import android.content.Context +import android.widget.RemoteViews +import com.kieronquinn.app.smartspacer.sdk.client.R + +fun RemoteViews.wrap(context: Context, darkText: Boolean): RemoteViews { + return if(darkText) { + RemoteViews(context.packageName, R.layout.remoteviews_wrapper_light) + }else{ + RemoteViews(context.packageName, R.layout.remoteviews_wrapper_dark) + }.apply { + removeAllViews(R.id.remoteviews_wrapper) + addView(R.id.remoteviews_wrapper, this@wrap) + } +} \ No newline at end of file diff --git a/sdk-client/src/main/java/com/kieronquinn/app/smartspacer/sdk/client/views/BcSmartspaceView.kt b/sdk-client/src/main/java/com/kieronquinn/app/smartspacer/sdk/client/views/BcSmartspaceView.kt index 4a645adc..3fdaf9c5 100644 --- a/sdk-client/src/main/java/com/kieronquinn/app/smartspacer/sdk/client/views/BcSmartspaceView.kt +++ b/sdk-client/src/main/java/com/kieronquinn/app/smartspacer/sdk/client/views/BcSmartspaceView.kt @@ -5,6 +5,7 @@ import android.animation.AnimatorListenerAdapter import android.animation.AnimatorSet import android.animation.ObjectAnimator import android.annotation.SuppressLint +import android.appwidget.AppWidgetHostView import android.content.Context import android.content.Intent import android.graphics.Color @@ -15,8 +16,10 @@ import android.view.View.MeasureSpec.makeMeasureSpec import android.view.ViewGroup import android.widget.FrameLayout import android.widget.Toast +import androidx.core.view.allViews import androidx.viewpager.widget.ViewPager import com.kieronquinn.app.smartspacer.sdk.SmartspacerConstants.SMARTSPACER_PACKAGE_NAME +import com.kieronquinn.app.smartspacer.sdk.client.BuildConfig import com.kieronquinn.app.smartspacer.sdk.client.R import com.kieronquinn.app.smartspacer.sdk.client.SmartspacerClient import com.kieronquinn.app.smartspacer.sdk.client.helper.SmartspacerHelper @@ -47,7 +50,7 @@ open class BcSmartspaceView @JvmOverloads constructor( private val client = SmartspacerClient.getInstance(context) private val provider by lazy { - SmartspacerHelper(client, config) + SmartspacerHelper(client, config.copy(sdkVersion = BuildConfig.SDK_VERSION)) } private lateinit var viewPager: ViewPager @@ -214,7 +217,9 @@ open class BcSmartspaceView @JvmOverloads constructor( } private fun animateSmartspaceUpdate(oldCard: View) { - if (runningAnimation != null || oldCard.parent != null) return + if (runningAnimation != null || oldCard.parent != null || + //Don't animate widget updates + oldCard.allViews.any { it is AppWidgetHostView }) return val animParent = viewPager.parent as ViewGroup oldCard.measure(makeMeasureSpec(viewPager.width, EXACTLY), makeMeasureSpec(viewPager.height, EXACTLY)) diff --git a/sdk-client/src/main/java/com/kieronquinn/app/smartspacer/sdk/client/views/SmartspacerView.kt b/sdk-client/src/main/java/com/kieronquinn/app/smartspacer/sdk/client/views/SmartspacerView.kt index e57106e1..04342936 100644 --- a/sdk-client/src/main/java/com/kieronquinn/app/smartspacer/sdk/client/views/SmartspacerView.kt +++ b/sdk-client/src/main/java/com/kieronquinn/app/smartspacer/sdk/client/views/SmartspacerView.kt @@ -12,6 +12,7 @@ import com.kieronquinn.app.smartspacer.sdk.client.views.features.SmartspacerComm import com.kieronquinn.app.smartspacer.sdk.client.views.features.SmartspacerDoorbellFeaturePageView import com.kieronquinn.app.smartspacer.sdk.client.views.features.SmartspacerUndefinedFeaturePageView import com.kieronquinn.app.smartspacer.sdk.client.views.features.SmartspacerWeatherFeaturePageView +import com.kieronquinn.app.smartspacer.sdk.client.views.remoteviews.SmartspacerRemoteViewsPageView import com.kieronquinn.app.smartspacer.sdk.client.views.templates.SmartspacerBasicTemplatePageView import com.kieronquinn.app.smartspacer.sdk.client.views.templates.SmartspacerCardImagesPageView import com.kieronquinn.app.smartspacer.sdk.client.views.templates.SmartspacerCardTemplatePageView @@ -143,33 +144,40 @@ open class SmartspacerView: FrameLayout { applyShadowIfRequired: Boolean ): SmartspacerBasePageView<*>? { return try { - val clazz = if(target.templateData != null){ - when(target.templateData){ - is CarouselTemplateData -> SmartspacerCarouselTemplatePageView::class.java - is HeadToHeadTemplateData -> SmartspacerHeadToHeadTemplatePageView::class.java - is SubCardTemplateData -> SmartspacerCardTemplatePageView::class.java - is SubListTemplateData -> SmartspacerListTemplatePageView::class.java - is SubImageTemplateData -> SmartspacerCardImagesPageView::class.java - else -> { - if(target.featureType == FEATURE_WEATHER){ - SmartspacerWeatherTemplatePageView::class.java - }else { - SmartspacerBasicTemplatePageView::class.java + val clazz = when { + target.remoteViews != null -> SmartspacerRemoteViewsPageView::class.java + target.templateData != null -> { + when(target.templateData){ + is CarouselTemplateData -> SmartspacerCarouselTemplatePageView::class.java + is HeadToHeadTemplateData -> SmartspacerHeadToHeadTemplatePageView::class.java + is SubCardTemplateData -> SmartspacerCardTemplatePageView::class.java + is SubListTemplateData -> SmartspacerListTemplatePageView::class.java + is SubImageTemplateData -> SmartspacerCardImagesPageView::class.java + else -> { + if(target.featureType == FEATURE_WEATHER){ + SmartspacerWeatherTemplatePageView::class.java + }else { + SmartspacerBasicTemplatePageView::class.java + } } } } - }else{ - when{ - FEATURE_ALLOWLIST_DOORBELL.contains(target.featureType) -> { - SmartspacerDoorbellFeaturePageView::class.java - } - FEATURE_ALLOWLIST_IMAGE.contains(target.featureType) -> { - SmartspacerCommuteTimeFeaturePageView::class.java - } - target.featureType == FEATURE_WEATHER -> { - SmartspacerWeatherFeaturePageView::class.java + else -> { + when{ + FEATURE_ALLOWLIST_DOORBELL.contains(target.featureType) -> { + SmartspacerDoorbellFeaturePageView::class.java + } + + FEATURE_ALLOWLIST_IMAGE.contains(target.featureType) -> { + SmartspacerCommuteTimeFeaturePageView::class.java + } + + target.featureType == FEATURE_WEATHER -> { + SmartspacerWeatherFeaturePageView::class.java + } + + else -> SmartspacerUndefinedFeaturePageView::class.java } - else -> SmartspacerUndefinedFeaturePageView::class.java } } SmartspacerBasePageView.createInstance( diff --git a/sdk-client/src/main/java/com/kieronquinn/app/smartspacer/sdk/client/views/remoteviews/SmartspacerRemoteViewsPageView.kt b/sdk-client/src/main/java/com/kieronquinn/app/smartspacer/sdk/client/views/remoteviews/SmartspacerRemoteViewsPageView.kt new file mode 100644 index 00000000..aac50eb5 --- /dev/null +++ b/sdk-client/src/main/java/com/kieronquinn/app/smartspacer/sdk/client/views/remoteviews/SmartspacerRemoteViewsPageView.kt @@ -0,0 +1,37 @@ +package com.kieronquinn.app.smartspacer.sdk.client.views.remoteviews + +import android.appwidget.AppWidgetHostView +import android.content.Context +import android.graphics.Color +import com.kieronquinn.app.smartspacer.sdk.client.databinding.SmartspacePageRemoteviewsBinding +import com.kieronquinn.app.smartspacer.sdk.client.utils.wrap +import com.kieronquinn.app.smartspacer.sdk.client.views.base.SmartspacerBasePageView +import com.kieronquinn.app.smartspacer.sdk.model.SmartspaceTarget +import com.kieronquinn.app.smartspacer.sdk.utils.copy + +class SmartspacerRemoteViewsPageView( + context: Context +): SmartspacerBasePageView( + context, + SmartspacePageRemoteviewsBinding::inflate +) { + + override suspend fun setTarget( + target: SmartspaceTarget, + interactionListener: SmartspaceTargetInteractionListener?, + tintColour: Int, + applyShadow: Boolean + ) = with(binding.root) { + removeAllViews() + val widgetContext = context.applicationContext + val host = AppWidgetHostView(widgetContext).apply { + setAppWidget(-1, target.widget) + updateAppWidget( + target.remoteViews?.wrap(widgetContext, tintColour == Color.BLACK)?.copy() + ) + layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) + } + addView(host) + } + +} \ No newline at end of file diff --git a/sdk-client/src/main/res/layout/remoteviews_wrapper_dark.xml b/sdk-client/src/main/res/layout/remoteviews_wrapper_dark.xml new file mode 100644 index 00000000..55bf2b57 --- /dev/null +++ b/sdk-client/src/main/res/layout/remoteviews_wrapper_dark.xml @@ -0,0 +1,9 @@ + + \ No newline at end of file diff --git a/sdk-client/src/main/res/layout/remoteviews_wrapper_light.xml b/sdk-client/src/main/res/layout/remoteviews_wrapper_light.xml new file mode 100644 index 00000000..796aa83b --- /dev/null +++ b/sdk-client/src/main/res/layout/remoteviews_wrapper_light.xml @@ -0,0 +1,9 @@ + + \ No newline at end of file diff --git a/sdk-client/src/main/res/layout/smartspace_page_remoteviews.xml b/sdk-client/src/main/res/layout/smartspace_page_remoteviews.xml new file mode 100644 index 00000000..9ec35aed --- /dev/null +++ b/sdk-client/src/main/res/layout/smartspace_page_remoteviews.xml @@ -0,0 +1,11 @@ + + \ No newline at end of file diff --git a/sdk-client/src/main/res/values/styles.xml b/sdk-client/src/main/res/values/styles.xml index cb219c52..f523fdb6 100644 --- a/sdk-client/src/main/res/values/styles.xml +++ b/sdk-client/src/main/res/values/styles.xml @@ -53,5 +53,13 @@ 1 1 + + \ No newline at end of file diff --git a/sdk-core/build.gradle b/sdk-core/build.gradle index 28f9e520..6baac6d6 100644 --- a/sdk-core/build.gradle +++ b/sdk-core/build.gradle @@ -8,7 +8,7 @@ plugins { android { namespace 'com.kieronquinn.app.smartspacer.sdk' - compileSdk 34 + compileSdk 35 defaultConfig { minSdk 29 @@ -39,11 +39,11 @@ android { } dependencies { - implementation "androidx.core:core-ktx:1.13.1" + implementation "androidx.core:core-ktx:1.15.0" } mavenPublishing { - coordinates("com.kieronquinn.smartspacer", "sdk-core", "1.0.5") + coordinates("com.kieronquinn.smartspacer", "sdk-core", "1.1.1") pom { name = "Smartspacer Core SDK" diff --git a/sdk-core/src/main/java/com/kieronquinn/app/smartspacer/sdk/model/SmartspaceConfig.kt b/sdk-core/src/main/java/com/kieronquinn/app/smartspacer/sdk/model/SmartspaceConfig.kt index 9b49229c..1fdf3671 100644 --- a/sdk-core/src/main/java/com/kieronquinn/app/smartspacer/sdk/model/SmartspaceConfig.kt +++ b/sdk-core/src/main/java/com/kieronquinn/app/smartspacer/sdk/model/SmartspaceConfig.kt @@ -31,7 +31,12 @@ data class SmartspaceConfig( * - Request to get periodic updates * - Request to support multiple clients for the same UISurface. */ - val extras: Bundle? = null + val extras: Bundle? = null, + /** + * The SDK version code, if this is using the Smartspacer SDK. SDK version 1 does not provide + * this value, so the default value is `1` + */ + val sdkVersion: Int = 1 ): Parcelable { companion object { @@ -39,6 +44,7 @@ data class SmartspaceConfig( private const val KEY_UI_SURFACE ="ui_surface" private const val KEY_PACKAGE_NAME = "package_name" private const val KEY_EXTRAS = "extras" + private const val KEY_SDK_VERSION = "sdk_version" } @RestrictTo(RestrictTo.Scope.LIBRARY) @@ -46,7 +52,8 @@ data class SmartspaceConfig( bundle.getInt(KEY_SMARTSPACE_TARGET_COUNT), UiSurface.from(bundle.getString(KEY_UI_SURFACE)!!), bundle.getString(KEY_PACKAGE_NAME)!!, - bundle.getBundle(KEY_EXTRAS) + bundle.getBundle(KEY_EXTRAS), + bundle.getInt(KEY_SDK_VERSION, 1) ) @RestrictTo(RestrictTo.Scope.LIBRARY) @@ -55,7 +62,8 @@ data class SmartspaceConfig( KEY_SMARTSPACE_TARGET_COUNT to smartspaceTargetCount, KEY_UI_SURFACE to uiSurface.surface, KEY_PACKAGE_NAME to packageName, - KEY_EXTRAS to extras + KEY_EXTRAS to extras, + KEY_SDK_VERSION to sdkVersion ) } diff --git a/sdk-core/src/main/java/com/kieronquinn/app/smartspacer/sdk/model/SmartspaceTarget.kt b/sdk-core/src/main/java/com/kieronquinn/app/smartspacer/sdk/model/SmartspaceTarget.kt index b008b533..73c54d01 100644 --- a/sdk-core/src/main/java/com/kieronquinn/app/smartspacer/sdk/model/SmartspaceTarget.kt +++ b/sdk-core/src/main/java/com/kieronquinn/app/smartspacer/sdk/model/SmartspaceTarget.kt @@ -7,13 +7,20 @@ import android.os.Bundle import android.os.Parcelable import android.os.Process import android.os.UserHandle +import android.widget.RemoteViews import androidx.annotation.RestrictTo import androidx.core.os.bundleOf import com.kieronquinn.app.smartspacer.sdk.annotations.LimitedNativeSupport import com.kieronquinn.app.smartspacer.sdk.model.SmartspaceTarget.Companion.FEATURE_UNDEFINED import com.kieronquinn.app.smartspacer.sdk.model.SmartspaceTarget.Companion.FEATURE_UPCOMING_ALARM import com.kieronquinn.app.smartspacer.sdk.model.expanded.ExpandedState -import com.kieronquinn.app.smartspacer.sdk.model.uitemplatedata.* +import com.kieronquinn.app.smartspacer.sdk.model.uitemplatedata.BaseTemplateData +import com.kieronquinn.app.smartspacer.sdk.model.uitemplatedata.CarouselTemplateData +import com.kieronquinn.app.smartspacer.sdk.model.uitemplatedata.CombinedCardsTemplateData +import com.kieronquinn.app.smartspacer.sdk.model.uitemplatedata.HeadToHeadTemplateData +import com.kieronquinn.app.smartspacer.sdk.model.uitemplatedata.SubCardTemplateData +import com.kieronquinn.app.smartspacer.sdk.model.uitemplatedata.SubImageTemplateData +import com.kieronquinn.app.smartspacer.sdk.model.uitemplatedata.SubListTemplateData import com.kieronquinn.app.smartspacer.sdk.utils.getEnumList import com.kieronquinn.app.smartspacer.sdk.utils.getParcelableArrayListCompat import com.kieronquinn.app.smartspacer.sdk.utils.getParcelableCompat @@ -115,6 +122,11 @@ data class SmartspaceTarget( * [AppWidgetProviderInfo] if this target is a widget (not currently used) */ var widget: AppWidgetProviderInfo? = null, + /** + * [RemoteViews] if this Target has RemoteViews + */ + @set:RestrictTo(RestrictTo.Scope.LIBRARY) + var remoteViews: RemoteViews? = null, /** * [BaseTemplateData] for the layout and format on Android 13+ (ignored on 12/12L) */ @@ -218,6 +230,7 @@ data class SmartspaceTarget( private const val KEY_ASSOCIATED_SMARTSPACE_TARGET_ID = "associated_smartspace_target_id" private const val KEY_SLICE_URI = "slice_uri" private const val KEY_WIDGET = "widget" + private const val KEY_REMOTE_VIEWS = "remote_views" private const val KEY_TEMPLATE_DATA = "template_data" private const val KEY_TEMPLATE_DATA_TYPE = "template_data_type" private const val KEY_EXPANDED_STATE = "expanded_state" @@ -312,6 +325,7 @@ data class SmartspaceTarget( associatedSmartspaceTargetId = bundle.getString(KEY_ASSOCIATED_SMARTSPACE_TARGET_ID), sliceUri = bundle.getParcelableCompat(KEY_SLICE_URI, Uri::class.java), widget = bundle.getParcelableCompat(KEY_WIDGET, AppWidgetProviderInfo::class.java), + remoteViews = bundle.getParcelableCompat(KEY_REMOTE_VIEWS, RemoteViews::class.java), templateData = bundle.getTemplateData(), expandedState = bundle.getBundle(KEY_EXPANDED_STATE)?.let { ExpandedState(it) }, canBeDismissed = bundle.getBoolean(KEY_CAN_BE_DISMISSED), @@ -338,6 +352,7 @@ data class SmartspaceTarget( target.associatedSmartspaceTargetId, target.sliceUri, target.widget, + target.remoteViews, target.templateData, target.expandedState, target.canBeDismissed, @@ -422,6 +437,7 @@ data class SmartspaceTarget( KEY_ASSOCIATED_SMARTSPACE_TARGET_ID to associatedSmartspaceTargetId, KEY_SLICE_URI to sliceUri, KEY_WIDGET to widget, + KEY_REMOTE_VIEWS to remoteViews, KEY_EXPANDED_STATE to expandedState?.toBundle(), KEY_CAN_BE_DISMISSED to canBeDismissed, KEY_CAN_TAKE_TWO_COMPLICATIONS to canTakeTwoComplications, diff --git a/sdk-core/src/main/java/com/kieronquinn/app/smartspacer/sdk/utils/TargetTemplate.kt b/sdk-core/src/main/java/com/kieronquinn/app/smartspacer/sdk/utils/TargetTemplate.kt index 9c08f3b2..a09a0780 100644 --- a/sdk-core/src/main/java/com/kieronquinn/app/smartspacer/sdk/utils/TargetTemplate.kt +++ b/sdk-core/src/main/java/com/kieronquinn/app/smartspacer/sdk/utils/TargetTemplate.kt @@ -990,6 +990,29 @@ sealed class TargetTemplate { } + + + /** + * Fully customisable Target layout, supported on Smartspacer and Native Smartspace on + * Android 15 QPR2 or Android 16+. + * + * You must provide a fallback template, which will be shown when RemoteViews are not + * supported. The Smartspacer ID from this fallback will be used all the time. + */ + data class RemoteViews( + val remoteViews: android.widget.RemoteViews, + val fallback: TargetTemplate + ): TargetTemplate() { + override fun create(): SmartspaceTarget { + if(fallback is RemoteViews) { + throw IllegalArgumentException("Fallback cannot also be RemoteViews template") + } + return fallback.create().apply { + remoteViews = this@RemoteViews.remoteViews + } + } + } + protected fun Icon.toBitmap(context: Context): Bitmap? { return icon.loadDrawable(context)?.toBitmap() } diff --git a/sdk-plugin/build.gradle b/sdk-plugin/build.gradle index 41ba622d..060e01fd 100644 --- a/sdk-plugin/build.gradle +++ b/sdk-plugin/build.gradle @@ -7,7 +7,7 @@ plugins { android { namespace 'com.kieronquinn.app.smartspacer.sdk.plugin' - compileSdk 34 + compileSdk 35 defaultConfig { minSdk 29 @@ -36,12 +36,12 @@ android { } dependencies { - implementation 'androidx.core:core-ktx:1.13.1' - api('com.kieronquinn.smartspacer:sdk-core:1.0.5') + implementation 'androidx.core:core-ktx:1.15.0' + api('com.kieronquinn.smartspacer:sdk-core:1.1.1') } mavenPublishing { - coordinates("com.kieronquinn.smartspacer", "sdk-plugin", "1.0.4") + coordinates("com.kieronquinn.smartspacer", "sdk-plugin", "1.1") pom { name = "Smartspacer Plugin SDK" diff --git a/sdk-sample/build.gradle b/sdk-sample/build.gradle index b61a6000..6533d814 100644 --- a/sdk-sample/build.gradle +++ b/sdk-sample/build.gradle @@ -5,7 +5,7 @@ plugins { android { namespace 'com.kieronquinn.app.smartspacer.sdksample' - compileSdk 34 + compileSdk 35 defaultConfig { applicationId "com.kieronquinn.app.smartspacer.sdksample" @@ -34,10 +34,10 @@ android { dependencies { implementation 'androidx.appcompat:appcompat:1.7.0' - implementation "androidx.fragment:fragment-ktx:1.8.2" - implementation "androidx.lifecycle:lifecycle-service:2.8.4" - implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.8.4" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3" + implementation "androidx.fragment:fragment-ktx:1.8.5" + implementation "androidx.lifecycle:lifecycle-service:2.8.7" + implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.8.7" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1" implementation project(path: ':sdk-plugin') implementation project(path: ':sdk-client') } \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index c039450a..d61d5734 100644 --- a/settings.gradle +++ b/settings.gradle @@ -11,6 +11,7 @@ dependencyResolutionManagement { google() mavenCentral() mavenLocal() + maven { url "https://api.xposed.info/" } maven { url 'https://jitpack.io' } jcenter() } diff --git a/systemstubs/src/main/java/android/widget/RemoteViewsHidden.java b/systemstubs/src/main/java/android/widget/RemoteViewsHidden.java index dcc09e0c..38d67284 100644 --- a/systemstubs/src/main/java/android/widget/RemoteViewsHidden.java +++ b/systemstubs/src/main/java/android/widget/RemoteViewsHidden.java @@ -20,6 +20,14 @@ public RemoteViewsHidden(RemoteViews landscape, RemoteViews portrait) { super(landscape, portrait); } + public RemoteViewsHidden(RemoteViews source) { + super(source); + } + + public RemoteViewsHidden(String packageName, int layout) { + super(packageName, layout); + } + /** * Handler for view interactions (such as clicks) within a RemoteViews. */ @@ -74,4 +82,8 @@ public void addFlags(int flags) { throw new RuntimeException("Stub!"); } + public boolean hasFlags(int flag) { + throw new RuntimeException("Stub!"); + } + }