| /* |
| * Copyright 2021 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package androidx.glance.appwidget |
| |
| import android.app.Activity |
| import android.appwidget.AppWidgetHost |
| import android.appwidget.AppWidgetHostView |
| import android.appwidget.AppWidgetManager |
| import android.appwidget.AppWidgetProviderInfo |
| import android.content.ComponentName |
| import android.content.Context |
| import android.content.res.Configuration |
| import android.graphics.Color |
| import android.os.Bundle |
| import android.os.LocaleList |
| import android.util.Log |
| import android.view.Gravity |
| import android.view.View |
| import android.view.WindowManager |
| import android.widget.FrameLayout |
| import android.widget.RemoteViews |
| import androidx.annotation.RequiresApi |
| import androidx.glance.appwidget.test.R |
| import androidx.compose.ui.unit.DpSize |
| import androidx.compose.ui.unit.dp |
| import org.junit.Assert.fail |
| import java.util.Locale |
| import java.util.concurrent.CountDownLatch |
| import java.util.concurrent.TimeUnit |
| |
| @RequiresApi(26) |
| class AppWidgetHostTestActivity : Activity() { |
| private var mHost: AppWidgetHost? = null |
| private val mHostViews = mutableListOf<TestAppWidgetHostView>() |
| private var mConfigurationChanged: CountDownLatch? = null |
| private var mLastConfiguration: Configuration? = null |
| val lastConfiguration: Configuration |
| get() = synchronized(this) { mLastConfiguration!! } |
| |
| override fun onCreate(savedInstanceState: Bundle?) { |
| super.onCreate(savedInstanceState) |
| window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) |
| setContentView(R.layout.app_widget_host_activity) |
| |
| mHost = TestAppWidgetHost(this, 1025).also { |
| it.startListening() |
| } |
| } |
| |
| override fun onDestroy() { |
| try { |
| mHost?.stopListening() |
| } catch (ex: Throwable) { |
| Log.w("AppWidgetHostTestActivity", "Error stopping listening", ex) |
| } |
| try { |
| mHost?.deleteHost() |
| } catch (t: Throwable) { |
| Log.w("AppWidgetHostTestActivity", "Error deleting Host", t) |
| } |
| mHost = null |
| super.onDestroy() |
| } |
| |
| fun bindAppWidget(portraitSize: DpSize, landscapeSize: DpSize): TestAppWidgetHostView { |
| val host = mHost ?: error("App widgets can only be bound while the activity is created") |
| |
| val appWidgetManager = AppWidgetManager.getInstance(this) |
| val appWidgetId = host.allocateAppWidgetId() |
| val componentName = ComponentName(this, TestGlanceAppWidgetReceiver::class.java) |
| |
| val wasBound = appWidgetManager.bindAppWidgetIdIfAllowed( |
| appWidgetId, |
| componentName, |
| optionsBundleOf(listOf(portraitSize, landscapeSize)) |
| ) |
| if (!wasBound) { |
| fail("Failed to bind the app widget") |
| } |
| |
| val info = appWidgetManager.getAppWidgetInfo(appWidgetId) |
| val locale = Locale.getDefault() |
| val config = resources.configuration |
| config.setLocales(LocaleList(locale)) |
| config.setLayoutDirection(locale) |
| val context = this.createConfigurationContext(config) |
| |
| val hostView = host.createView(context, appWidgetId, info) as TestAppWidgetHostView |
| hostView.setPadding(0, 0, 0, 0) |
| val contentFrame = findViewById<FrameLayout>(R.id.content) |
| contentFrame.addView(hostView) |
| hostView.setSizes(portraitSize, landscapeSize) |
| hostView.setBackgroundColor(Color.WHITE) |
| mHostViews += hostView |
| return hostView |
| } |
| |
| fun deleteAppWidget(hostView: TestAppWidgetHostView) { |
| val appWidgetId = hostView.appWidgetId |
| mHost?.deleteAppWidgetId(appWidgetId) |
| mHostViews.remove(hostView) |
| val contentFrame = findViewById<FrameLayout>(R.id.content) |
| contentFrame.removeView(hostView) |
| } |
| |
| override fun onConfigurationChanged(newConfig: Configuration) { |
| super.onConfigurationChanged(newConfig) |
| mHostViews.forEach { |
| it.updateSize(newConfig.orientation) |
| it.reapplyRemoteViews() |
| } |
| synchronized(this) { |
| mLastConfiguration = newConfig |
| mConfigurationChanged?.countDown() |
| } |
| } |
| |
| fun resetConfigurationChangedLatch() { |
| synchronized(this) { |
| mConfigurationChanged = CountDownLatch(1) |
| mLastConfiguration = null |
| } |
| } |
| |
| // This should not be called from the main thread, so that it does not block |
| // onConfigurationChanged from being called. |
| fun waitForConfigurationChange() { |
| val result = mConfigurationChanged?.await(5, TimeUnit.SECONDS)!! |
| require(result) { "Timeout before getting configuration" } |
| } |
| } |
| |
| @RequiresApi(26) |
| class TestAppWidgetHost(context: Context, hostId: Int) : AppWidgetHost(context, hostId) { |
| override fun onCreateView( |
| context: Context, |
| appWidgetId: Int, |
| appWidget: AppWidgetProviderInfo? |
| ): AppWidgetHostView = TestAppWidgetHostView(context) |
| } |
| |
| @RequiresApi(26) |
| class TestAppWidgetHostView(context: Context) : AppWidgetHostView(context) { |
| |
| init { |
| // Prevent asynchronous inflation of the App Widget |
| setExecutor(null) |
| layoutDirection = View.LAYOUT_DIRECTION_LOCALE |
| } |
| |
| private var mLatch: CountDownLatch? = null |
| private var mRemoteViews: RemoteViews? = null |
| private var mPortraitSize: DpSize = DpSize(0.dp, 0.dp) |
| private var mLandscapeSize: DpSize = DpSize(0.dp, 0.dp) |
| |
| /** |
| * Wait for the new remote views to be received. If a remote views was already received, return |
| * immediately. |
| */ |
| fun waitForRemoteViews() { |
| synchronized(this) { |
| mRemoteViews?.let { return } |
| mLatch = CountDownLatch(1) |
| } |
| val result = mLatch?.await(5, TimeUnit.SECONDS)!! |
| require(result) { "Timeout before getting RemoteViews" } |
| } |
| |
| override fun updateAppWidget(remoteViews: RemoteViews?) { |
| super.updateAppWidget(remoteViews) |
| synchronized(this) { |
| mRemoteViews = remoteViews |
| if (remoteViews != null) { |
| mLatch?.countDown() |
| } |
| } |
| } |
| |
| /** Reset the latch used to detect the arrival of a new RemoteViews. */ |
| fun resetRemoteViewsLatch() { |
| synchronized(this) { |
| mRemoteViews = null |
| mLatch = null |
| } |
| } |
| |
| fun setSizes(portraitSize: DpSize, landscapeSize: DpSize) { |
| mPortraitSize = portraitSize |
| mLandscapeSize = landscapeSize |
| updateSize(resources.configuration.orientation) |
| } |
| |
| fun updateSize(orientation: Int) { |
| val size = when (orientation) { |
| Configuration.ORIENTATION_LANDSCAPE -> mLandscapeSize |
| Configuration.ORIENTATION_PORTRAIT -> mPortraitSize |
| else -> error("Unknown orientation ${context.resources.configuration.orientation}") |
| } |
| val displayMetrics = resources.displayMetrics |
| val width = size.width.toPixels(displayMetrics) |
| val height = size.height.toPixels(displayMetrics) |
| layoutParams = LayoutParams(width, height, Gravity.CENTER) |
| requestLayout() |
| } |
| |
| fun reapplyRemoteViews() { |
| mRemoteViews?.let { super.updateAppWidget(it) } |
| } |
| } |