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