1 /* 2 * Copyright (C) 2017 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 com.android.systemui.statusbar.notification.row; 18 19 import static com.android.systemui.statusbar.notification.row.NotificationContentInflater.FLAG_CONTENT_VIEW_ALL; 20 import static com.android.systemui.statusbar.notification.row.NotificationContentInflater.FLAG_CONTENT_VIEW_AMBIENT; 21 import static com.android.systemui.statusbar.notification.row.NotificationContentInflater.FLAG_CONTENT_VIEW_EXPANDED; 22 import static com.android.systemui.statusbar.notification.row.NotificationContentInflater.FLAG_CONTENT_VIEW_HEADS_UP; 23 import static com.android.systemui.statusbar.notification.row.NotificationContentInflater.FLAG_CONTENT_VIEW_PUBLIC; 24 25 import static org.junit.Assert.assertEquals; 26 import static org.junit.Assert.assertNotNull; 27 import static org.junit.Assert.assertNull; 28 import static org.junit.Assert.assertTrue; 29 import static org.mockito.Mockito.spy; 30 import static org.mockito.Mockito.times; 31 import static org.mockito.Mockito.verify; 32 33 import android.app.Notification; 34 import android.content.Context; 35 import android.os.CancellationSignal; 36 import android.os.Handler; 37 import android.os.Looper; 38 import android.service.notification.StatusBarNotification; 39 import android.testing.AndroidTestingRunner; 40 import android.testing.TestableLooper.RunWithLooper; 41 import android.util.ArrayMap; 42 import android.view.View; 43 import android.view.ViewGroup; 44 import android.widget.RemoteViews; 45 46 import androidx.test.filters.SmallTest; 47 48 import com.android.systemui.R; 49 import com.android.systemui.SysuiTestCase; 50 import com.android.systemui.statusbar.InflationTask; 51 import com.android.systemui.statusbar.NotificationTestHelper; 52 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 53 import com.android.systemui.statusbar.notification.row.NotificationContentInflater.InflationCallback; 54 55 import org.junit.Assert; 56 import org.junit.Before; 57 import org.junit.Ignore; 58 import org.junit.Test; 59 import org.junit.runner.RunWith; 60 61 import java.util.HashMap; 62 import java.util.concurrent.CountDownLatch; 63 import java.util.concurrent.Executor; 64 import java.util.concurrent.TimeUnit; 65 66 @SmallTest 67 @RunWith(AndroidTestingRunner.class) 68 @RunWithLooper(setAsMainLooper = true) 69 public class NotificationContentInflaterTest extends SysuiTestCase { 70 71 private NotificationContentInflater mNotificationInflater; 72 private Notification.Builder mBuilder; 73 private ExpandableNotificationRow mRow; 74 75 @Before setUp()76 public void setUp() throws Exception { 77 mBuilder = new Notification.Builder(mContext).setSmallIcon( 78 R.drawable.ic_person) 79 .setContentTitle("Title") 80 .setContentText("Text") 81 .setStyle(new Notification.BigTextStyle().bigText("big text")); 82 ExpandableNotificationRow row = new NotificationTestHelper(mContext).createRow( 83 mBuilder.build()); 84 mRow = spy(row); 85 mNotificationInflater = new NotificationContentInflater(mRow); 86 mNotificationInflater.setInflationCallback(new InflationCallback() { 87 @Override 88 public void handleInflationException(StatusBarNotification notification, 89 Exception e) { 90 } 91 92 @Override 93 public void onAsyncInflationFinished(NotificationEntry entry, 94 @NotificationContentInflater.InflationFlag int inflatedFlags) { 95 } 96 }); 97 } 98 99 @Test testIncreasedHeadsUpBeingUsed()100 public void testIncreasedHeadsUpBeingUsed() { 101 mNotificationInflater.setUsesIncreasedHeadsUpHeight(true); 102 Notification.Builder builder = spy(mBuilder); 103 mNotificationInflater.inflateNotificationViews( 104 true /* inflateSynchronously */, 105 FLAG_CONTENT_VIEW_ALL, 106 builder, 107 mContext); 108 verify(builder).createHeadsUpContentView(true); 109 } 110 111 @Test testIncreasedHeightBeingUsed()112 public void testIncreasedHeightBeingUsed() { 113 mNotificationInflater.setUsesIncreasedHeight(true); 114 Notification.Builder builder = spy(mBuilder); 115 mNotificationInflater.inflateNotificationViews( 116 true /* inflateSynchronously */, 117 FLAG_CONTENT_VIEW_ALL, 118 builder, 119 mContext); 120 verify(builder).createContentView(true); 121 } 122 123 @Test testInflationCallsUpdated()124 public void testInflationCallsUpdated() throws Exception { 125 runThenWaitForInflation(() -> mNotificationInflater.inflateNotificationViews(), 126 mNotificationInflater); 127 verify(mRow).onNotificationUpdated(); 128 } 129 130 @Test testInflationOnlyInflatesSetFlags()131 public void testInflationOnlyInflatesSetFlags() throws Exception { 132 mNotificationInflater.updateInflationFlag(FLAG_CONTENT_VIEW_HEADS_UP, 133 true /* shouldInflate */); 134 runThenWaitForInflation(() -> mNotificationInflater.inflateNotificationViews(), 135 mNotificationInflater); 136 137 assertNotNull(mRow.getPrivateLayout().getHeadsUpChild()); 138 assertNull(mRow.getShowingLayout().getAmbientChild()); 139 verify(mRow).onNotificationUpdated(); 140 } 141 142 @Test testInflationThrowsErrorDoesntCallUpdated()143 public void testInflationThrowsErrorDoesntCallUpdated() throws Exception { 144 mRow.getPrivateLayout().removeAllViews(); 145 mRow.getStatusBarNotification().getNotification().contentView 146 = new RemoteViews(mContext.getPackageName(), R.layout.status_bar); 147 runThenWaitForInflation(() -> mNotificationInflater.inflateNotificationViews(), 148 true /* expectingException */, mNotificationInflater); 149 assertTrue(mRow.getPrivateLayout().getChildCount() == 0); 150 verify(mRow, times(0)).onNotificationUpdated(); 151 } 152 153 @Test testAsyncTaskRemoved()154 public void testAsyncTaskRemoved() throws Exception { 155 mRow.getEntry().abortTask(); 156 runThenWaitForInflation(() -> mNotificationInflater.inflateNotificationViews(), 157 mNotificationInflater); 158 verify(mRow).onNotificationUpdated(); 159 } 160 161 @Test testRemovedNotInflated()162 public void testRemovedNotInflated() throws Exception { 163 mRow.setRemoved(); 164 mNotificationInflater.setInflateSynchronously(true); 165 mNotificationInflater.inflateNotificationViews(); 166 Assert.assertNull(mRow.getEntry().getRunningTask()); 167 } 168 169 @Test 170 @Ignore testInflationIsRetriedIfAsyncFails()171 public void testInflationIsRetriedIfAsyncFails() throws Exception { 172 NotificationContentInflater.InflationProgress result = 173 new NotificationContentInflater.InflationProgress(); 174 result.packageContext = mContext; 175 CountDownLatch countDownLatch = new CountDownLatch(1); 176 NotificationContentInflater.applyRemoteView( 177 false /* inflateSynchronously */, 178 result, 179 FLAG_CONTENT_VIEW_EXPANDED, 180 0, 181 new ArrayMap() /* cachedContentViews */, mRow, false /* redactAmbient */, 182 true /* isNewView */, (v, p, r) -> true, 183 new InflationCallback() { 184 @Override 185 public void handleInflationException(StatusBarNotification notification, 186 Exception e) { 187 countDownLatch.countDown(); 188 throw new RuntimeException("No Exception expected"); 189 } 190 191 @Override 192 public void onAsyncInflationFinished(NotificationEntry entry, 193 @NotificationContentInflater.InflationFlag int inflatedFlags) { 194 countDownLatch.countDown(); 195 } 196 }, mRow.getPrivateLayout(), null, null, new HashMap<>(), 197 new NotificationContentInflater.ApplyCallback() { 198 @Override 199 public void setResultView(View v) { 200 } 201 202 @Override 203 public RemoteViews getRemoteView() { 204 return new AsyncFailRemoteView(mContext.getPackageName(), 205 R.layout.custom_view_dark); 206 } 207 }); 208 assertTrue(countDownLatch.await(500, TimeUnit.MILLISECONDS)); 209 } 210 211 @Test testUpdateNeedsRedactionReinflatesChangedContentViews()212 public void testUpdateNeedsRedactionReinflatesChangedContentViews() { 213 mNotificationInflater.updateInflationFlag(FLAG_CONTENT_VIEW_AMBIENT, true); 214 mNotificationInflater.updateInflationFlag(FLAG_CONTENT_VIEW_PUBLIC, true); 215 mNotificationInflater.updateNeedsRedaction(true); 216 217 NotificationContentInflater.AsyncInflationTask asyncInflationTask = 218 (NotificationContentInflater.AsyncInflationTask) mRow.getEntry().getRunningTask(); 219 assertEquals(FLAG_CONTENT_VIEW_AMBIENT | FLAG_CONTENT_VIEW_PUBLIC, 220 asyncInflationTask.getReInflateFlags()); 221 asyncInflationTask.abort(); 222 } 223 224 /* Cancelling requires us to be on the UI thread otherwise we might have a race */ 225 @Test testSupersedesExistingTask()226 public void testSupersedesExistingTask() { 227 mNotificationInflater.addInflationFlags(FLAG_CONTENT_VIEW_ALL); 228 mNotificationInflater.inflateNotificationViews(); 229 230 // Trigger inflation of content and expanded only. 231 mNotificationInflater.setIsLowPriority(true); 232 mNotificationInflater.setIsChildInGroup(true); 233 234 InflationTask runningTask = mRow.getEntry().getRunningTask(); 235 NotificationContentInflater.AsyncInflationTask asyncInflationTask = 236 (NotificationContentInflater.AsyncInflationTask) runningTask; 237 assertEquals("Successive inflations don't inherit the previous flags!", 238 FLAG_CONTENT_VIEW_ALL, asyncInflationTask.getReInflateFlags()); 239 runningTask.abort(); 240 } 241 242 @Test doesntReapplyDisallowedRemoteView()243 public void doesntReapplyDisallowedRemoteView() throws Exception { 244 mBuilder.setStyle(new Notification.MediaStyle()); 245 RemoteViews mediaView = mBuilder.createContentView(); 246 mBuilder.setStyle(new Notification.DecoratedCustomViewStyle()); 247 mBuilder.setCustomContentView(new RemoteViews(getContext().getPackageName(), 248 R.layout.custom_view_dark)); 249 RemoteViews decoratedMediaView = mBuilder.createContentView(); 250 Assert.assertFalse("The decorated media style doesn't allow a view to be reapplied!", 251 NotificationContentInflater.canReapplyRemoteView(mediaView, decoratedMediaView)); 252 } 253 runThenWaitForInflation(Runnable block, NotificationContentInflater inflater)254 public static void runThenWaitForInflation(Runnable block, 255 NotificationContentInflater inflater) throws Exception { 256 runThenWaitForInflation(block, false /* expectingException */, inflater); 257 } 258 runThenWaitForInflation(Runnable block, boolean expectingException, NotificationContentInflater inflater)259 private static void runThenWaitForInflation(Runnable block, boolean expectingException, 260 NotificationContentInflater inflater) throws Exception { 261 CountDownLatch countDownLatch = new CountDownLatch(1); 262 final ExceptionHolder exceptionHolder = new ExceptionHolder(); 263 inflater.setInflateSynchronously(true); 264 inflater.setInflationCallback(new InflationCallback() { 265 @Override 266 public void handleInflationException(StatusBarNotification notification, 267 Exception e) { 268 if (!expectingException) { 269 exceptionHolder.setException(e); 270 } 271 countDownLatch.countDown(); 272 } 273 274 @Override 275 public void onAsyncInflationFinished(NotificationEntry entry, 276 @NotificationContentInflater.InflationFlag int inflatedFlags) { 277 if (expectingException) { 278 exceptionHolder.setException(new RuntimeException( 279 "Inflation finished even though there should be an error")); 280 } 281 countDownLatch.countDown(); 282 } 283 }); 284 block.run(); 285 assertTrue(countDownLatch.await(500, TimeUnit.MILLISECONDS)); 286 if (exceptionHolder.mException != null) { 287 throw exceptionHolder.mException; 288 } 289 } 290 291 private static class ExceptionHolder { 292 private Exception mException; 293 setException(Exception exception)294 public void setException(Exception exception) { 295 mException = exception; 296 } 297 } 298 299 private class AsyncFailRemoteView extends RemoteViews { 300 Handler mHandler = Handler.createAsync(Looper.getMainLooper()); 301 AsyncFailRemoteView(String packageName, int layoutId)302 public AsyncFailRemoteView(String packageName, int layoutId) { 303 super(packageName, layoutId); 304 } 305 306 @Override apply(Context context, ViewGroup parent)307 public View apply(Context context, ViewGroup parent) { 308 return super.apply(context, parent); 309 } 310 311 @Override applyAsync(Context context, ViewGroup parent, Executor executor, OnViewAppliedListener listener, OnClickHandler handler)312 public CancellationSignal applyAsync(Context context, ViewGroup parent, Executor executor, 313 OnViewAppliedListener listener, OnClickHandler handler) { 314 mHandler.post(() -> listener.onError(new RuntimeException("Failed to inflate async"))); 315 return new CancellationSignal(); 316 } 317 318 @Override applyAsync(Context context, ViewGroup parent, Executor executor, OnViewAppliedListener listener)319 public CancellationSignal applyAsync(Context context, ViewGroup parent, Executor executor, 320 OnViewAppliedListener listener) { 321 return applyAsync(context, parent, executor, listener, null); 322 } 323 } 324 } 325