blob: ba2887936cb796704953d0cecab6d9eaf84cf8f1 [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.NotificationContentInflater.FLAG_CONTENT_VIEW_ALL;
import static com.android.systemui.statusbar.notification.row.NotificationContentInflater.FLAG_CONTENT_VIEW_EXPANDED;
import static com.android.systemui.statusbar.notification.row.NotificationContentInflater.FLAG_CONTENT_VIEW_HEADS_UP;
import static com.android.systemui.statusbar.notification.row.NotificationContentInflater.FLAG_CONTENT_VIEW_PUBLIC;
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.service.notification.StatusBarNotification;
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 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.NotificationContentInflater.InflationCallback;
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)
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(mRow);
mNotificationInflater.setInflationCallback(new InflationCallback() {
@Override
public void handleInflationException(StatusBarNotification notification,
Exception e) {
}
@Override
public void onAsyncInflationFinished(NotificationEntry entry,
@NotificationContentInflater.InflationFlag int inflatedFlags) {
}
});
}
@Test
public void testIncreasedHeadsUpBeingUsed() {
mNotificationInflater.setUsesIncreasedHeadsUpHeight(true);
Notification.Builder builder = spy(mBuilder);
mNotificationInflater.inflateNotificationViews(
true /* inflateSynchronously */,
FLAG_CONTENT_VIEW_ALL,
builder,
mContext);
verify(builder).createHeadsUpContentView(true);
}
@Test
public void testIncreasedHeightBeingUsed() {
mNotificationInflater.setUsesIncreasedHeight(true);
Notification.Builder builder = spy(mBuilder);
mNotificationInflater.inflateNotificationViews(
true /* inflateSynchronously */,
FLAG_CONTENT_VIEW_ALL,
builder,
mContext);
verify(builder).createContentView(true);
}
@Test
public void testInflationCallsUpdated() throws Exception {
runThenWaitForInflation(() -> mNotificationInflater.inflateNotificationViews(),
mNotificationInflater);
verify(mRow).onNotificationUpdated();
}
@Test
public void testInflationOnlyInflatesSetFlags() throws Exception {
mNotificationInflater.updateInflationFlag(FLAG_CONTENT_VIEW_HEADS_UP,
true /* shouldInflate */);
runThenWaitForInflation(() -> mNotificationInflater.inflateNotificationViews(),
mNotificationInflater);
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);
runThenWaitForInflation(() -> mNotificationInflater.inflateNotificationViews(),
true /* expectingException */, mNotificationInflater);
assertTrue(mRow.getPrivateLayout().getChildCount() == 0);
verify(mRow, times(0)).onNotificationUpdated();
}
@Test
public void testAsyncTaskRemoved() throws Exception {
mRow.getEntry().abortTask();
runThenWaitForInflation(() -> mNotificationInflater.inflateNotificationViews(),
mNotificationInflater);
verify(mRow).onNotificationUpdated();
}
@Test
public void testRemovedNotInflated() throws Exception {
mRow.setRemoved();
mNotificationInflater.setInflateSynchronously(true);
mNotificationInflater.inflateNotificationViews();
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(StatusBarNotification notification,
Exception e) {
countDownLatch.countDown();
throw new RuntimeException("No Exception expected");
}
@Override
public void onAsyncInflationFinished(NotificationEntry entry,
@NotificationContentInflater.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));
}
@Test
public void testUpdateNeedsRedactionReinflatesChangedContentViews() {
mNotificationInflater.updateInflationFlag(FLAG_CONTENT_VIEW_PUBLIC, true);
mNotificationInflater.updateNeedsRedaction(true);
NotificationContentInflater.AsyncInflationTask asyncInflationTask =
(NotificationContentInflater.AsyncInflationTask) mRow.getEntry().getRunningTask();
assertEquals(FLAG_CONTENT_VIEW_PUBLIC, asyncInflationTask.getReInflateFlags());
asyncInflationTask.abort();
}
/* Cancelling requires us to be on the UI thread otherwise we might have a race */
@Test
public void testSupersedesExistingTask() {
mNotificationInflater.addInflationFlags(FLAG_CONTENT_VIEW_ALL);
mNotificationInflater.inflateNotificationViews();
// Trigger inflation of content and expanded only.
mNotificationInflater.setIsLowPriority(true);
mNotificationInflater.setIsChildInGroup(true);
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));
}
public static void runThenWaitForInflation(Runnable block,
NotificationContentInflater inflater) throws Exception {
runThenWaitForInflation(block, false /* expectingException */, inflater);
}
private static void runThenWaitForInflation(Runnable block, boolean expectingException,
NotificationContentInflater inflater) throws Exception {
CountDownLatch countDownLatch = new CountDownLatch(1);
final ExceptionHolder exceptionHolder = new ExceptionHolder();
inflater.setInflateSynchronously(true);
inflater.setInflationCallback(new InflationCallback() {
@Override
public void handleInflationException(StatusBarNotification notification,
Exception e) {
if (!expectingException) {
exceptionHolder.setException(e);
}
countDownLatch.countDown();
}
@Override
public void onAsyncInflationFinished(NotificationEntry entry,
@NotificationContentInflater.InflationFlag int inflatedFlags) {
if (expectingException) {
exceptionHolder.setException(new RuntimeException(
"Inflation finished even though there should be an error"));
}
countDownLatch.countDown();
}
});
block.run();
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);
}
}
}