Add Support for App Widgets as Overlays.

This changelist adds support for loading App Widgets
from a ComponentName. This functionality is wrapped in
an OverlayProvider for integration with
DreamOverlayService.

Bug: 201675932
Test: atest AppWidgetOverlayProviderTest
Change-Id: Iddc6297ee0a9eb6c945a70368ba1f1b7ef0b1a0b
This commit is contained in:
Bryce Lee
2021-09-30 17:07:30 -07:00
parent 814a2d60ef
commit 54b8f33ec7
7 changed files with 393 additions and 0 deletions

View File

@@ -294,6 +294,11 @@
<uses-permission android:name="android.permission.READ_PEOPLE_DATA" />
<!-- Permission for dream overlay. -->
<uses-permission android:name="android.permission.BIND_DREAM_SERVICE" />
<uses-permission android:name="android.permission.BIND_APPWIDGET" />
<protected-broadcast android:name="com.android.settingslib.action.REGISTER_SLICE_RECEIVER" />
<protected-broadcast android:name="com.android.settingslib.action.UNREGISTER_SLICE_RECEIVER" />
<protected-broadcast android:name="com.android.settings.flashlight.action.FLASHLIGHT_CHANGED" />

View File

@@ -36,6 +36,7 @@ import com.android.systemui.controls.dagger.ControlsModule;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.demomode.dagger.DemoModeModule;
import com.android.systemui.doze.dagger.DozeComponent;
import com.android.systemui.dreams.dagger.DreamModule;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FeatureFlagManager;
import com.android.systemui.flags.FeatureFlags;
@@ -101,6 +102,7 @@ import dagger.Provides;
AssistModule.class,
ClockModule.class,
CommunalModule.class,
DreamModule.class,
ControlsModule.class,
DemoModeModule.class,
FalsingModule.class,

View File

@@ -0,0 +1,82 @@
/*
* Copyright (C) 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 com.android.systemui.dreams.appwidgets;
import android.appwidget.AppWidgetHostView;
import android.content.ComponentName;
import android.content.Context;
import android.util.Log;
import android.widget.RemoteViews;
import com.android.systemui.dreams.OverlayHost;
import com.android.systemui.dreams.OverlayHostView;
import com.android.systemui.dreams.OverlayProvider;
import com.android.systemui.plugins.ActivityStarter;
import javax.inject.Inject;
/**
* {@link AppWidgetOverlayProvider} is an implementation of {@link OverlayProvider} for providing
* app widget-based overlays.
*/
public class AppWidgetOverlayProvider implements OverlayProvider {
private static final String TAG = "AppWdgtOverlayProvider";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private final ActivityStarter mActivityStarter;
private final AppWidgetProvider mAppWidgetProvider;
private final ComponentName mComponentName;
private final OverlayHostView.LayoutParams mLayoutParams;
@Inject
public AppWidgetOverlayProvider(ActivityStarter activityStarter,
ComponentName componentName, AppWidgetProvider widgetProvider,
OverlayHostView.LayoutParams layoutParams) {
mActivityStarter = activityStarter;
mComponentName = componentName;
mAppWidgetProvider = widgetProvider;
mLayoutParams = layoutParams;
}
@Override
public void onCreateOverlay(Context context, OverlayHost.CreationCallback creationCallback,
OverlayHost.InteractionCallback interactionCallback) {
final AppWidgetHostView widget = mAppWidgetProvider.getWidget(mComponentName);
if (widget == null) {
Log.e(TAG, "could not create widget");
return;
}
widget.setInteractionHandler((view, pendingIntent, response) -> {
if (pendingIntent.isActivity()) {
if (DEBUG) {
Log.d(TAG, "launching pending intent from app widget:" + mComponentName);
}
interactionCallback.onExit();
mActivityStarter.startPendingIntentDismissingKeyguard(pendingIntent,
null /*intentSentUiThreadCallback*/, view);
return true;
} else {
return RemoteViews.startPendingIntent(view, pendingIntent,
response.getLaunchOptions(view));
}
});
creationCallback.onCreated(widget, mLayoutParams);
}
}

View File

@@ -0,0 +1,101 @@
/*
* Copyright (C) 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 com.android.systemui.dreams.appwidgets;
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.Resources;
import android.util.Log;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import java.util.List;
import javax.inject.Inject;
/**
* {@link AppWidgetProvider} is a singleton for accessing app widgets within SystemUI. This
* consolidates resources such as the App Widget Host across potentially multiple
* {@link AppWidgetOverlayProvider} instances and other usages.
*/
@SysUISingleton
public class AppWidgetProvider {
private static final String TAG = "AppWidgetProvider";
public static final int APP_WIDGET_HOST_ID = 1025;
private final Context mContext;
private final AppWidgetManager mAppWidgetManager;
private final AppWidgetHost mAppWidgetHost;
private final Resources mResources;
@Inject
public AppWidgetProvider(Context context, @Main Resources resources) {
mContext = context;
mResources = resources;
mAppWidgetManager = android.appwidget.AppWidgetManager.getInstance(context);
mAppWidgetHost = new AppWidgetHost(context, APP_WIDGET_HOST_ID);
mAppWidgetHost.startListening();
}
/**
* Returns an {@link AppWidgetHostView} associated with a given {@link ComponentName}.
* @param component The {@link ComponentName} of the target {@link AppWidgetHostView}.
* @return The {@link AppWidgetHostView} or {@code null} on error.
*/
public AppWidgetHostView getWidget(ComponentName component) {
final List<AppWidgetProviderInfo> appWidgetInfos =
mAppWidgetManager.getInstalledProviders();
for (AppWidgetProviderInfo widgetInfo : appWidgetInfos) {
if (widgetInfo.provider.equals(component)) {
final int widgetId = mAppWidgetHost.allocateAppWidgetId();
boolean success = mAppWidgetManager.bindAppWidgetIdIfAllowed(widgetId,
widgetInfo.provider);
if (!success) {
Log.e(TAG, "could not bind to app widget:" + component);
break;
}
final AppWidgetHostView appWidgetView =
mAppWidgetHost.createView(mContext, widgetId, widgetInfo);
if (appWidgetView != null) {
// Register a layout change listener to update the widget on any sizing changes.
appWidgetView.addOnLayoutChangeListener(
(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
final float density = mResources.getDisplayMetrics().density;
final int height = Math.round((bottom - top) / density);
final int width = Math.round((right - left) / density);
appWidgetView.updateAppWidgetSize(null, width, height, width,
height);
});
}
return appWidgetView;
}
}
return null;
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (C) 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 com.android.systemui.dreams.dagger;
import android.content.ComponentName;
import com.android.systemui.dreams.OverlayHostView;
import com.android.systemui.dreams.appwidgets.AppWidgetOverlayProvider;
import dagger.BindsInstance;
import dagger.Subcomponent;
/** */
@Subcomponent
public interface AppWidgetOverlayComponent {
/** */
@Subcomponent.Factory
interface Factory {
AppWidgetOverlayComponent build(@BindsInstance ComponentName component,
@BindsInstance OverlayHostView.LayoutParams layoutParams);
}
/** Builds a {@link AppWidgetOverlayProvider}. */
AppWidgetOverlayProvider getAppWidgetOverlayProvider();
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright (C) 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 com.android.systemui.dreams.dagger;
import dagger.Module;
/**
* Dagger Module providing Communal-related functionality.
*/
@Module(subcomponents = {
AppWidgetOverlayComponent.class,
})
public interface DreamModule {
}

View File

@@ -0,0 +1,136 @@
/*
* Copyright (C) 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 com.android.systemui.dreams;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.isNull;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.PendingIntent;
import android.appwidget.AppWidgetHostView;
import android.content.ComponentName;
import android.testing.AndroidTestingRunner;
import android.widget.RemoteViews;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.SysuiTestableContext;
import com.android.systemui.dreams.appwidgets.AppWidgetOverlayProvider;
import com.android.systemui.dreams.appwidgets.AppWidgetProvider;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.utils.leaks.LeakCheckedTest;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@SmallTest
@RunWith(AndroidTestingRunner.class)
public class AppWidgetOverlayProviderTest extends SysuiTestCase {
@Mock
ActivityStarter mActivityStarter;
@Mock
ComponentName mComponentName;
@Mock
AppWidgetProvider mAppWidgetProvider;
@Mock
AppWidgetHostView mAppWidgetHostView;
@Mock
OverlayHost.CreationCallback mCreationCallback;
@Mock
OverlayHost.InteractionCallback mInteractionCallback;
@Mock
PendingIntent mPendingIntent;
@Mock
RemoteViews.RemoteResponse mRemoteResponse;
AppWidgetOverlayProvider mOverlayProvider;
RemoteViews.InteractionHandler mInteractionHandler;
@Rule
public final LeakCheckedTest.SysuiLeakCheck mLeakCheck = new LeakCheckedTest.SysuiLeakCheck();
@Rule
public SysuiTestableContext mContext = new SysuiTestableContext(
InstrumentationRegistry.getContext(), mLeakCheck);
OverlayHostView.LayoutParams mLayoutParams = new OverlayHostView.LayoutParams(
OverlayHostView.LayoutParams.MATCH_PARENT, OverlayHostView.LayoutParams.MATCH_PARENT);
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
when(mPendingIntent.isActivity()).thenReturn(true);
when(mAppWidgetProvider.getWidget(mComponentName)).thenReturn(mAppWidgetHostView);
mOverlayProvider = new AppWidgetOverlayProvider(
mActivityStarter,
mComponentName,
mAppWidgetProvider,
mLayoutParams
);
final ArgumentCaptor<RemoteViews.InteractionHandler> creationCallbackCapture =
ArgumentCaptor.forClass(RemoteViews.InteractionHandler.class);
mOverlayProvider.onCreateOverlay(mContext, mCreationCallback, mInteractionCallback);
verify(mAppWidgetHostView, times(1))
.setInteractionHandler(creationCallbackCapture.capture());
mInteractionHandler = creationCallbackCapture.getValue();
}
@Test
public void testWidgetBringup() {
// Make sure widget was requested.
verify(mAppWidgetProvider, times(1)).getWidget(eq(mComponentName));
// Make sure widget was returned to callback.
verify(mCreationCallback, times(1)).onCreated(eq(mAppWidgetHostView),
eq(mLayoutParams));
}
@Test
public void testWidgetInteraction() {
// Trigger interaction.
mInteractionHandler.onInteraction(mAppWidgetHostView, mPendingIntent,
mRemoteResponse);
// Ensure activity is started.
verify(mActivityStarter, times(1))
.startPendingIntentDismissingKeyguard(eq(mPendingIntent), isNull(),
eq(mAppWidgetHostView));
// Verify exit is requested.
verify(mInteractionCallback, times(1)).onExit();
}
}