Pierre Barbier de Reuille | c4b2dd8 | 2021-08-26 00:08:28 +0100 | [diff] [blame] | 1 | /* |
| 2 | * Copyright 2021 The Android Open Source Project |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | |
| 17 | package androidx.glance.appwidget |
| 18 | |
| 19 | import android.app.Activity |
| 20 | import android.appwidget.AppWidgetHost |
| 21 | import android.appwidget.AppWidgetHostView |
| 22 | import android.appwidget.AppWidgetManager |
| 23 | import android.appwidget.AppWidgetProviderInfo |
| 24 | import android.content.ComponentName |
| 25 | import android.content.Context |
Pierre Barbier de Reuille | 1248632 | 2021-09-02 01:06:51 +0100 | [diff] [blame] | 26 | import android.content.res.Configuration |
Pierre Barbier de Reuille | c4b2dd8 | 2021-08-26 00:08:28 +0100 | [diff] [blame] | 27 | import android.graphics.Color |
| 28 | import android.os.Bundle |
Pierre Barbier de Reuille | d7119aa | 2021-10-01 00:16:04 +0100 | [diff] [blame] | 29 | import android.os.LocaleList |
Pierre Barbier de Reuille | c4b2dd8 | 2021-08-26 00:08:28 +0100 | [diff] [blame] | 30 | import android.util.Log |
| 31 | import android.view.Gravity |
Pierre Barbier de Reuille | 1a8dde9 | 2021-10-01 11:11:53 +0100 | [diff] [blame] | 32 | import android.view.View |
Pierre Barbier de Reuille | c4b2dd8 | 2021-08-26 00:08:28 +0100 | [diff] [blame] | 33 | import android.view.WindowManager |
| 34 | import android.widget.FrameLayout |
| 35 | import android.widget.RemoteViews |
Pierre Barbier de Reuille | 1248632 | 2021-09-02 01:06:51 +0100 | [diff] [blame] | 36 | import androidx.annotation.RequiresApi |
Pierre Barbier de Reuille | c4b2dd8 | 2021-08-26 00:08:28 +0100 | [diff] [blame] | 37 | import androidx.glance.appwidget.test.R |
Jamie Garside | 0d543aa | 2021-10-19 17:35:32 +0100 | [diff] [blame] | 38 | import androidx.compose.ui.unit.DpSize |
| 39 | import androidx.compose.ui.unit.dp |
Pierre Barbier de Reuille | ea54927 | 2021-10-06 23:43:40 +0100 | [diff] [blame] | 40 | import org.junit.Assert.fail |
Pierre Barbier de Reuille | d7119aa | 2021-10-01 00:16:04 +0100 | [diff] [blame] | 41 | import java.util.Locale |
Pierre Barbier de Reuille | ea54927 | 2021-10-06 23:43:40 +0100 | [diff] [blame] | 42 | import java.util.concurrent.CountDownLatch |
| 43 | import java.util.concurrent.TimeUnit |
Pierre Barbier de Reuille | c4b2dd8 | 2021-08-26 00:08:28 +0100 | [diff] [blame] | 44 | |
Pierre Barbier de Reuille | 1248632 | 2021-09-02 01:06:51 +0100 | [diff] [blame] | 45 | @RequiresApi(26) |
Pierre Barbier de Reuille | c4b2dd8 | 2021-08-26 00:08:28 +0100 | [diff] [blame] | 46 | class AppWidgetHostTestActivity : Activity() { |
| 47 | private var mHost: AppWidgetHost? = null |
Pierre Barbier de Reuille | 1248632 | 2021-09-02 01:06:51 +0100 | [diff] [blame] | 48 | private val mHostViews = mutableListOf<TestAppWidgetHostView>() |
Willie Koomson | dadafb5 | 2022-08-16 12:31:10 -0700 | [diff] [blame^] | 49 | private var mConfigurationChanged: CountDownLatch? = null |
| 50 | private var mLastConfiguration: Configuration? = null |
| 51 | val lastConfiguration: Configuration |
| 52 | get() = synchronized(this) { mLastConfiguration!! } |
Pierre Barbier de Reuille | c4b2dd8 | 2021-08-26 00:08:28 +0100 | [diff] [blame] | 53 | |
| 54 | override fun onCreate(savedInstanceState: Bundle?) { |
| 55 | super.onCreate(savedInstanceState) |
| 56 | window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) |
| 57 | setContentView(R.layout.app_widget_host_activity) |
| 58 | |
| 59 | mHost = TestAppWidgetHost(this, 1025).also { |
| 60 | it.startListening() |
| 61 | } |
| 62 | } |
| 63 | |
| 64 | override fun onDestroy() { |
| 65 | try { |
| 66 | mHost?.stopListening() |
Pierre Barbier de Reuille | c4b2dd8 | 2021-08-26 00:08:28 +0100 | [diff] [blame] | 67 | } catch (ex: Throwable) { |
Marcel Pintó Biescas | 74fbd76 | 2022-09-05 12:02:25 +0200 | [diff] [blame] | 68 | Log.w("AppWidgetHostTestActivity", "Error stopping listening", ex) |
| 69 | } |
| 70 | try { |
| 71 | mHost?.deleteHost() |
| 72 | } catch (t: Throwable) { |
| 73 | Log.w("AppWidgetHostTestActivity", "Error deleting Host", t) |
Pierre Barbier de Reuille | c4b2dd8 | 2021-08-26 00:08:28 +0100 | [diff] [blame] | 74 | } |
| 75 | mHost = null |
| 76 | super.onDestroy() |
| 77 | } |
| 78 | |
Pierre Barbier de Reuille | 1248632 | 2021-09-02 01:06:51 +0100 | [diff] [blame] | 79 | fun bindAppWidget(portraitSize: DpSize, landscapeSize: DpSize): TestAppWidgetHostView { |
Pierre Barbier de Reuille | c4b2dd8 | 2021-08-26 00:08:28 +0100 | [diff] [blame] | 80 | val host = mHost ?: error("App widgets can only be bound while the activity is created") |
| 81 | |
| 82 | val appWidgetManager = AppWidgetManager.getInstance(this) |
| 83 | val appWidgetId = host.allocateAppWidgetId() |
| 84 | val componentName = ComponentName(this, TestGlanceAppWidgetReceiver::class.java) |
| 85 | |
Pierre Barbier de Reuille | 1248632 | 2021-09-02 01:06:51 +0100 | [diff] [blame] | 86 | val wasBound = appWidgetManager.bindAppWidgetIdIfAllowed( |
| 87 | appWidgetId, |
| 88 | componentName, |
Jamie Garside | 0d543aa | 2021-10-19 17:35:32 +0100 | [diff] [blame] | 89 | optionsBundleOf(listOf(portraitSize, landscapeSize)) |
Pierre Barbier de Reuille | 1248632 | 2021-09-02 01:06:51 +0100 | [diff] [blame] | 90 | ) |
Pierre Barbier de Reuille | c4b2dd8 | 2021-08-26 00:08:28 +0100 | [diff] [blame] | 91 | if (!wasBound) { |
| 92 | fail("Failed to bind the app widget") |
| 93 | } |
| 94 | |
| 95 | val info = appWidgetManager.getAppWidgetInfo(appWidgetId) |
Pierre Barbier de Reuille | 1a8dde9 | 2021-10-01 11:11:53 +0100 | [diff] [blame] | 96 | val locale = Locale.getDefault() |
| 97 | val config = resources.configuration |
| 98 | config.setLocales(LocaleList(locale)) |
| 99 | config.setLayoutDirection(locale) |
| 100 | val context = this.createConfigurationContext(config) |
| 101 | |
Pierre Barbier de Reuille | d7119aa | 2021-10-01 00:16:04 +0100 | [diff] [blame] | 102 | val hostView = host.createView(context, appWidgetId, info) as TestAppWidgetHostView |
Pierre Barbier de Reuille | 7584fe8 | 2021-09-03 21:58:58 +0100 | [diff] [blame] | 103 | hostView.setPadding(0, 0, 0, 0) |
Pierre Barbier de Reuille | c4b2dd8 | 2021-08-26 00:08:28 +0100 | [diff] [blame] | 104 | val contentFrame = findViewById<FrameLayout>(R.id.content) |
Pierre Barbier de Reuille | 1248632 | 2021-09-02 01:06:51 +0100 | [diff] [blame] | 105 | contentFrame.addView(hostView) |
| 106 | hostView.setSizes(portraitSize, landscapeSize) |
Pierre Barbier de Reuille | c4b2dd8 | 2021-08-26 00:08:28 +0100 | [diff] [blame] | 107 | hostView.setBackgroundColor(Color.WHITE) |
Pierre Barbier de Reuille | 1248632 | 2021-09-02 01:06:51 +0100 | [diff] [blame] | 108 | mHostViews += hostView |
Pierre Barbier de Reuille | c4b2dd8 | 2021-08-26 00:08:28 +0100 | [diff] [blame] | 109 | return hostView |
| 110 | } |
Pierre Barbier de Reuille | 1248632 | 2021-09-02 01:06:51 +0100 | [diff] [blame] | 111 | |
Pierre Barbier de Reuille | 4be2171 | 2021-11-18 10:27:03 +0000 | [diff] [blame] | 112 | fun deleteAppWidget(hostView: TestAppWidgetHostView) { |
| 113 | val appWidgetId = hostView.appWidgetId |
| 114 | mHost?.deleteAppWidgetId(appWidgetId) |
| 115 | mHostViews.remove(hostView) |
| 116 | val contentFrame = findViewById<FrameLayout>(R.id.content) |
| 117 | contentFrame.removeView(hostView) |
| 118 | } |
| 119 | |
Pierre Barbier de Reuille | 1248632 | 2021-09-02 01:06:51 +0100 | [diff] [blame] | 120 | override fun onConfigurationChanged(newConfig: Configuration) { |
| 121 | super.onConfigurationChanged(newConfig) |
Willie Koomson | dadafb5 | 2022-08-16 12:31:10 -0700 | [diff] [blame^] | 122 | mHostViews.forEach { |
| 123 | it.updateSize(newConfig.orientation) |
| 124 | it.reapplyRemoteViews() |
| 125 | } |
| 126 | synchronized(this) { |
| 127 | mLastConfiguration = newConfig |
| 128 | mConfigurationChanged?.countDown() |
| 129 | } |
Pierre Barbier de Reuille | 1248632 | 2021-09-02 01:06:51 +0100 | [diff] [blame] | 130 | } |
| 131 | |
Willie Koomson | dadafb5 | 2022-08-16 12:31:10 -0700 | [diff] [blame^] | 132 | fun resetConfigurationChangedLatch() { |
| 133 | synchronized(this) { |
| 134 | mConfigurationChanged = CountDownLatch(1) |
| 135 | mLastConfiguration = null |
| 136 | } |
Pierre Barbier de Reuille | 1248632 | 2021-09-02 01:06:51 +0100 | [diff] [blame] | 137 | } |
| 138 | |
Willie Koomson | dadafb5 | 2022-08-16 12:31:10 -0700 | [diff] [blame^] | 139 | // This should not be called from the main thread, so that it does not block |
| 140 | // onConfigurationChanged from being called. |
| 141 | fun waitForConfigurationChange() { |
| 142 | val result = mConfigurationChanged?.await(5, TimeUnit.SECONDS)!! |
| 143 | require(result) { "Timeout before getting configuration" } |
Pierre Barbier de Reuille | 1248632 | 2021-09-02 01:06:51 +0100 | [diff] [blame] | 144 | } |
Pierre Barbier de Reuille | c4b2dd8 | 2021-08-26 00:08:28 +0100 | [diff] [blame] | 145 | } |
| 146 | |
Pierre Barbier de Reuille | 1248632 | 2021-09-02 01:06:51 +0100 | [diff] [blame] | 147 | @RequiresApi(26) |
Pierre Barbier de Reuille | c4b2dd8 | 2021-08-26 00:08:28 +0100 | [diff] [blame] | 148 | class TestAppWidgetHost(context: Context, hostId: Int) : AppWidgetHost(context, hostId) { |
| 149 | override fun onCreateView( |
| 150 | context: Context, |
| 151 | appWidgetId: Int, |
| 152 | appWidget: AppWidgetProviderInfo? |
| 153 | ): AppWidgetHostView = TestAppWidgetHostView(context) |
| 154 | } |
| 155 | |
Pierre Barbier de Reuille | 1248632 | 2021-09-02 01:06:51 +0100 | [diff] [blame] | 156 | @RequiresApi(26) |
Pierre Barbier de Reuille | c4b2dd8 | 2021-08-26 00:08:28 +0100 | [diff] [blame] | 157 | class TestAppWidgetHostView(context: Context) : AppWidgetHostView(context) { |
| 158 | |
Pierre Barbier de Reuille | 1248632 | 2021-09-02 01:06:51 +0100 | [diff] [blame] | 159 | init { |
| 160 | // Prevent asynchronous inflation of the App Widget |
| 161 | setExecutor(null) |
Pierre Barbier de Reuille | 1a8dde9 | 2021-10-01 11:11:53 +0100 | [diff] [blame] | 162 | layoutDirection = View.LAYOUT_DIRECTION_LOCALE |
Pierre Barbier de Reuille | 1248632 | 2021-09-02 01:06:51 +0100 | [diff] [blame] | 163 | } |
Pierre Barbier de Reuille | c4b2dd8 | 2021-08-26 00:08:28 +0100 | [diff] [blame] | 164 | |
Pierre Barbier de Reuille | 1248632 | 2021-09-02 01:06:51 +0100 | [diff] [blame] | 165 | private var mLatch: CountDownLatch? = null |
| 166 | private var mRemoteViews: RemoteViews? = null |
Jamie Garside | 0d543aa | 2021-10-19 17:35:32 +0100 | [diff] [blame] | 167 | private var mPortraitSize: DpSize = DpSize(0.dp, 0.dp) |
| 168 | private var mLandscapeSize: DpSize = DpSize(0.dp, 0.dp) |
Pierre Barbier de Reuille | 1248632 | 2021-09-02 01:06:51 +0100 | [diff] [blame] | 169 | |
| 170 | /** |
| 171 | * Wait for the new remote views to be received. If a remote views was already received, return |
| 172 | * immediately. |
| 173 | */ |
Pierre Barbier de Reuille | c4b2dd8 | 2021-08-26 00:08:28 +0100 | [diff] [blame] | 174 | fun waitForRemoteViews() { |
Pierre Barbier de Reuille | 1248632 | 2021-09-02 01:06:51 +0100 | [diff] [blame] | 175 | synchronized(this) { |
| 176 | mRemoteViews?.let { return } |
| 177 | mLatch = CountDownLatch(1) |
| 178 | } |
| 179 | val result = mLatch?.await(5, TimeUnit.SECONDS)!! |
Pierre Barbier de Reuille | c4b2dd8 | 2021-08-26 00:08:28 +0100 | [diff] [blame] | 180 | require(result) { "Timeout before getting RemoteViews" } |
| 181 | } |
| 182 | |
| 183 | override fun updateAppWidget(remoteViews: RemoteViews?) { |
| 184 | super.updateAppWidget(remoteViews) |
Pierre Barbier de Reuille | 1248632 | 2021-09-02 01:06:51 +0100 | [diff] [blame] | 185 | synchronized(this) { |
| 186 | mRemoteViews = remoteViews |
| 187 | if (remoteViews != null) { |
| 188 | mLatch?.countDown() |
| 189 | } |
Pierre Barbier de Reuille | c4b2dd8 | 2021-08-26 00:08:28 +0100 | [diff] [blame] | 190 | } |
| 191 | } |
Pierre Barbier de Reuille | 1248632 | 2021-09-02 01:06:51 +0100 | [diff] [blame] | 192 | |
| 193 | /** Reset the latch used to detect the arrival of a new RemoteViews. */ |
| 194 | fun resetRemoteViewsLatch() { |
| 195 | synchronized(this) { |
| 196 | mRemoteViews = null |
| 197 | mLatch = null |
| 198 | } |
| 199 | } |
| 200 | |
| 201 | fun setSizes(portraitSize: DpSize, landscapeSize: DpSize) { |
| 202 | mPortraitSize = portraitSize |
| 203 | mLandscapeSize = landscapeSize |
Pierre Barbier de Reuille | ea54927 | 2021-10-06 23:43:40 +0100 | [diff] [blame] | 204 | updateSize(resources.configuration.orientation) |
Pierre Barbier de Reuille | 1248632 | 2021-09-02 01:06:51 +0100 | [diff] [blame] | 205 | } |
| 206 | |
Pierre Barbier de Reuille | ea54927 | 2021-10-06 23:43:40 +0100 | [diff] [blame] | 207 | fun updateSize(orientation: Int) { |
| 208 | val size = when (orientation) { |
Pierre Barbier de Reuille | 1248632 | 2021-09-02 01:06:51 +0100 | [diff] [blame] | 209 | Configuration.ORIENTATION_LANDSCAPE -> mLandscapeSize |
| 210 | Configuration.ORIENTATION_PORTRAIT -> mPortraitSize |
| 211 | else -> error("Unknown orientation ${context.resources.configuration.orientation}") |
| 212 | } |
| 213 | val displayMetrics = resources.displayMetrics |
| 214 | val width = size.width.toPixels(displayMetrics) |
| 215 | val height = size.height.toPixels(displayMetrics) |
| 216 | layoutParams = LayoutParams(width, height, Gravity.CENTER) |
| 217 | requestLayout() |
| 218 | } |
| 219 | |
| 220 | fun reapplyRemoteViews() { |
| 221 | mRemoteViews?.let { super.updateAppWidget(it) } |
| 222 | } |
Pierre Barbier de Reuille | c4b2dd8 | 2021-08-26 00:08:28 +0100 | [diff] [blame] | 223 | } |