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