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.InstrumentationRegistry; 32 import android.support.test.annotation.UiThreadTest; 33 import android.support.test.filters.FlakyTest; 34 import android.support.test.filters.SmallTest; 35 import android.support.test.runner.AndroidJUnit4; 36 import android.view.View; 37 import android.view.ViewGroup; 38 import android.widget.RemoteViews; 39 40 import com.android.systemui.R; 41 import com.android.systemui.SysuiTestCase; 42 import com.android.systemui.statusbar.ExpandableNotificationRow; 43 import com.android.systemui.statusbar.NotificationData; 44 import com.android.systemui.statusbar.NotificationTestHelper; 45 import com.android.systemui.statusbar.InflationTask; 46 47 import org.junit.Assert; 48 import org.junit.Before; 49 import org.junit.Ignore; 50 import org.junit.Test; 51 import org.junit.runner.RunWith; 52 53 import java.util.HashMap; 54 import java.util.concurrent.CountDownLatch; 55 import java.util.concurrent.Executor; 56 57 @SmallTest 58 @RunWith(AndroidJUnit4.class) 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 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 countDownLatch.await(); 185 } 186 187 @Test 188 public void testSupersedesExistingTask() throws Exception { 189 mNotificationInflater.inflateNotificationViews(); 190 mNotificationInflater.setIsLowPriority(true); 191 mNotificationInflater.setIsChildInGroup(true); 192 InflationTask runningTask = mRow.getEntry().getRunningTask(); 193 NotificationInflater.AsyncInflationTask asyncInflationTask = 194 (NotificationInflater.AsyncInflationTask) runningTask; 195 Assert.assertSame("Successive inflations don't inherit the previous flags!", 196 asyncInflationTask.getReInflateFlags(), 197 NotificationInflater.FLAG_REINFLATE_ALL); 198 runningTask.abort(); 199 } 200 201 public static void runThenWaitForInflation(Runnable block, 202 NotificationInflater inflater) throws Exception { 203 runThenWaitForInflation(block, false /* expectingException */, inflater); 204 } 205 206 private static void runThenWaitForInflation(Runnable block, boolean expectingException, 207 NotificationInflater inflater) throws Exception { 208 com.android.systemui.util.Assert.isNotMainThread(); 209 CountDownLatch countDownLatch = new CountDownLatch(1); 210 final ExceptionHolder exceptionHolder = new ExceptionHolder(); 211 inflater.setInflationCallback(new NotificationInflater.InflationCallback() { 212 @Override 213 public void handleInflationException(StatusBarNotification notification, 214 Exception e) { 215 if (!expectingException) { 216 exceptionHolder.setException(e); 217 } 218 countDownLatch.countDown(); 219 } 220 221 @Override 222 public void onAsyncInflationFinished(NotificationData.Entry entry) { 223 if (expectingException) { 224 exceptionHolder.setException(new RuntimeException( 225 "Inflation finished even though there should be an error")); 226 } 227 countDownLatch.countDown(); 228 } 229 }); 230 block.run(); 231 countDownLatch.await(); 232 if (exceptionHolder.mException != null) { 233 throw exceptionHolder.mException; 234 } 235 } 236 237 private static class ExceptionHolder { 238 private Exception mException; 239 240 public void setException(Exception exception) { 241 mException = exception; 242 } 243 } 244 245 private class AsyncFailRemoteView extends RemoteViews { 246 Handler mHandler = new Handler(Looper.getMainLooper()); 247 248 public AsyncFailRemoteView(String packageName, int layoutId) { 249 super(packageName, layoutId); 250 } 251 252 @Override 253 public View apply(Context context, ViewGroup parent) { 254 return super.apply(context, parent); 255 } 256 257 @Override 258 public CancellationSignal applyAsync(Context context, ViewGroup parent, Executor executor, 259 OnViewAppliedListener listener, OnClickHandler handler) { 260 mHandler.post(() -> listener.onError(new RuntimeException("Failed to inflate async"))); 261 return new CancellationSignal(); 262 } 263 264 @Override 265 public CancellationSignal applyAsync(Context context, ViewGroup parent, Executor executor, 266 OnViewAppliedListener listener) { 267 return applyAsync(context, parent, executor, listener, null); 268 } 269 } 270 } 271