blob: f916fe5ad7fb1944bfe57393769bbe6a1c557b88 [file] [log] [blame]
/*
* Copyright (C) 2017 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.statusbar.notification.row;
import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL;
import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED;
import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED;
import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_HEADS_UP;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.app.Notification;
import android.content.Context;
import android.os.CancellationSignal;
import android.os.Handler;
import android.os.Looper;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper.RunWithLooper;
import android.util.ArrayMap;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RemoteViews;
import androidx.test.filters.SmallTest;
import androidx.test.filters.Suppress;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.statusbar.InflationTask;
import com.android.systemui.statusbar.NotificationTestHelper;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.BindParams;
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationCallback;
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag;
import com.android.systemui.tests.R;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.HashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@RunWithLooper(setAsMainLooper = true)
@Suppress
public class NotificationContentInflaterTest extends SysuiTestCase {
private NotificationContentInflater mNotificationInflater;
private Notification.Builder mBuilder;
private ExpandableNotificationRow mRow;
@Before
public void setUp() throws Exception {
mBuilder = new Notification.Builder(mContext).setSmallIcon(
R.drawable.ic_person)
.setContentTitle("Title")
.setContentText("Text")
.setStyle(new Notification.BigTextStyle().bigText("big text"));
ExpandableNotificationRow row = new NotificationTestHelper(mContext, mDependency).createRow(
mBuilder.build());
mRow = spy(row);
mNotificationInflater = new NotificationContentInflater();
}
@Test
public void testIncreasedHeadsUpBeingUsed() {
BindParams params = new BindParams();
params.usesIncreasedHeadsUpHeight = true;
Notification.Builder builder = spy(mBuilder);
mNotificationInflater.inflateNotificationViews(mRow.getEntry(),
mRow,
params,
true /* inflateSynchronously */,
FLAG_CONTENT_VIEW_ALL,
builder,
mContext);
verify(builder).createHeadsUpContentView(true);
}
@Test
public void testIncreasedHeightBeingUsed() {
BindParams params = new BindParams();
params.usesIncreasedHeight = true;
Notification.Builder builder = spy(mBuilder);
mNotificationInflater.inflateNotificationViews(mRow.getEntry(),
mRow,
params,
true /* inflateSynchronously */,
FLAG_CONTENT_VIEW_ALL,
builder,
mContext);
verify(builder).createContentView(true);
}
@Test
public void testInflationCallsUpdated() throws Exception {
inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow);
verify(mRow).onNotificationUpdated();
}
@Test
public void testInflationOnlyInflatesSetFlags() throws Exception {
inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_HEADS_UP, mRow);
assertNotNull(mRow.getPrivateLayout().getHeadsUpChild());
verify(mRow).onNotificationUpdated();
}
@Test
public void testInflationThrowsErrorDoesntCallUpdated() throws Exception {
mRow.getPrivateLayout().removeAllViews();
mRow.getEntry().getSbn().getNotification().contentView
= new RemoteViews(mContext.getPackageName(), R.layout.status_bar);
inflateAndWait(true /* expectingException */, mNotificationInflater, FLAG_CONTENT_VIEW_ALL,
mRow);
assertTrue(mRow.getPrivateLayout().getChildCount() == 0);
verify(mRow, times(0)).onNotificationUpdated();
}
@Test
public void testAsyncTaskRemoved() throws Exception {
mRow.getEntry().abortTask();
inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow);
verify(mRow).onNotificationUpdated();
}
@Test
public void testRemovedNotInflated() throws Exception {
mRow.setRemoved();
mNotificationInflater.setInflateSynchronously(true);
mNotificationInflater.bindContent(
mRow.getEntry(),
mRow,
FLAG_CONTENT_VIEW_ALL,
new BindParams(),
false /* forceInflate */,
null /* callback */);
Assert.assertNull(mRow.getEntry().getRunningTask());
}
@Test
@Ignore
public void testInflationIsRetriedIfAsyncFails() throws Exception {
NotificationContentInflater.InflationProgress result =
new NotificationContentInflater.InflationProgress();
result.packageContext = mContext;
CountDownLatch countDownLatch = new CountDownLatch(1);
NotificationContentInflater.applyRemoteView(
false /* inflateSynchronously */,
result,
FLAG_CONTENT_VIEW_EXPANDED,
0,
new ArrayMap() /* cachedContentViews */, mRow,
true /* isNewView */, (v, p, r) -> true,
new InflationCallback() {
@Override
public void handleInflationException(NotificationEntry entry,
Exception e) {
countDownLatch.countDown();
throw new RuntimeException("No Exception expected");
}
@Override
public void onAsyncInflationFinished(NotificationEntry entry,
@InflationFlag int inflatedFlags) {
countDownLatch.countDown();
}
}, mRow.getPrivateLayout(), null, null, new HashMap<>(),
new NotificationContentInflater.ApplyCallback() {
@Override
public void setResultView(View v) {
}
@Override
public RemoteViews getRemoteView() {
return new AsyncFailRemoteView(mContext.getPackageName(),
R.layout.custom_view_dark);
}
});
assertTrue(countDownLatch.await(500, TimeUnit.MILLISECONDS));
}
/* Cancelling requires us to be on the UI thread otherwise we might have a race */
@Test
public void testSupersedesExistingTask() {
mNotificationInflater.bindContent(
mRow.getEntry(),
mRow,
FLAG_CONTENT_VIEW_ALL,
new BindParams(),
false /* forceInflate */,
null /* callback */);
// Trigger inflation of contracted only.
mNotificationInflater.bindContent(
mRow.getEntry(),
mRow,
FLAG_CONTENT_VIEW_CONTRACTED,
new BindParams(),
false /* forceInflate */,
null /* callback */);
InflationTask runningTask = mRow.getEntry().getRunningTask();
NotificationContentInflater.AsyncInflationTask asyncInflationTask =
(NotificationContentInflater.AsyncInflationTask) runningTask;
assertEquals("Successive inflations don't inherit the previous flags!",
FLAG_CONTENT_VIEW_ALL, asyncInflationTask.getReInflateFlags());
runningTask.abort();
}
@Test
public void doesntReapplyDisallowedRemoteView() throws Exception {
mBuilder.setStyle(new Notification.MediaStyle());
RemoteViews mediaView = mBuilder.createContentView();
mBuilder.setStyle(new Notification.DecoratedCustomViewStyle());
mBuilder.setCustomContentView(new RemoteViews(getContext().getPackageName(),
R.layout.custom_view_dark));
RemoteViews decoratedMediaView = mBuilder.createContentView();
Assert.assertFalse("The decorated media style doesn't allow a view to be reapplied!",
NotificationContentInflater.canReapplyRemoteView(mediaView, decoratedMediaView));
}
private static void inflateAndWait(NotificationContentInflater inflater,
@InflationFlag int contentToInflate,
ExpandableNotificationRow row)
throws Exception {
inflateAndWait(false /* expectingException */, inflater, contentToInflate, row);
}
private static void inflateAndWait(boolean expectingException,
NotificationContentInflater inflater,
@InflationFlag int contentToInflate,
ExpandableNotificationRow row) throws Exception {
CountDownLatch countDownLatch = new CountDownLatch(1);
final ExceptionHolder exceptionHolder = new ExceptionHolder();
inflater.setInflateSynchronously(true);
InflationCallback callback = new InflationCallback() {
@Override
public void handleInflationException(NotificationEntry entry,
Exception e) {
if (!expectingException) {
exceptionHolder.setException(e);
}
countDownLatch.countDown();
}
@Override
public void onAsyncInflationFinished(NotificationEntry entry,
@InflationFlag int inflatedFlags) {
if (expectingException) {
exceptionHolder.setException(new RuntimeException(
"Inflation finished even though there should be an error"));
}
countDownLatch.countDown();
}
};
inflater.bindContent(
row.getEntry(),
row,
contentToInflate,
new BindParams(),
false /* forceInflate */,
callback /* callback */);
assertTrue(countDownLatch.await(500, TimeUnit.MILLISECONDS));
if (exceptionHolder.mException != null) {
throw exceptionHolder.mException;
}
}
private static class ExceptionHolder {
private Exception mException;
public void setException(Exception exception) {
mException = exception;
}
}
private class AsyncFailRemoteView extends RemoteViews {
Handler mHandler = Handler.createAsync(Looper.getMainLooper());
public AsyncFailRemoteView(String packageName, int layoutId) {
super(packageName, layoutId);
}
@Override
public View apply(Context context, ViewGroup parent) {
return super.apply(context, parent);
}
@Override
public CancellationSignal applyAsync(Context context, ViewGroup parent, Executor executor,
OnViewAppliedListener listener, OnClickHandler handler) {
mHandler.post(() -> listener.onError(new RuntimeException("Failed to inflate async")));
return new CancellationSignal();
}
@Override
public CancellationSignal applyAsync(Context context, ViewGroup parent, Executor executor,
OnViewAppliedListener listener) {
return applyAsync(context, parent, executor, listener, null);
}
}
}