• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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 android.server.wm;
18 
19 import static android.server.wm.ActivityManagerTestBase.executeShellCommand;
20 import static android.server.wm.WindowInsetsAnimationUtils.requestControlThenTransitionToVisibility;
21 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
22 import static android.view.WindowInsets.Type.ime;
23 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN;
24 
25 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
26 
27 import android.app.Activity;
28 import android.content.ComponentName;
29 import android.content.Context;
30 import android.graphics.Bitmap;
31 import android.graphics.Canvas;
32 import android.graphics.Insets;
33 import android.graphics.Paint;
34 import android.graphics.Rect;
35 import android.inputmethodservice.InputMethodService;
36 import android.os.Bundle;
37 import android.platform.test.annotations.LargeTest;
38 import android.provider.Settings;
39 import android.server.wm.WindowInsetsAnimationControllerTests.LimitedErrorCollector;
40 import android.view.View;
41 import android.view.ViewGroup;
42 import android.view.Window;
43 import android.view.WindowInsets;
44 import android.view.WindowInsetsAnimation;
45 import android.view.WindowInsetsAnimation.Callback;
46 import android.view.WindowInsetsController;
47 import android.view.inputmethod.EditorInfo;
48 import android.view.inputmethod.InputMethodManager;
49 import android.widget.EditText;
50 import android.widget.FrameLayout;
51 
52 import androidx.annotation.NonNull;
53 import androidx.annotation.Nullable;
54 import androidx.test.platform.app.InstrumentationRegistry;
55 import androidx.test.rule.ActivityTestRule;
56 
57 import com.android.compatibility.common.util.PollingCheck;
58 
59 import org.junit.Rule;
60 import org.junit.Test;
61 
62 import java.util.List;
63 import java.util.Locale;
64 import java.util.function.Supplier;
65 
66 @LargeTest
67 public class WindowInsetsAnimationSynchronicityTests {
68     private static final int APP_COLOR = 0xff01fe10; // green
69     private static final int BEHIND_IME_COLOR = 0xfffeef00; // yellow
70     private static final int IME_COLOR = 0xfffe01fd; // pink
71 
72     @Rule
73     public LimitedErrorCollector mErrorCollector = new LimitedErrorCollector();
74 
75     @Rule
76     public ActivityTestRule<TestActivity> mActivityRule = new ActivityTestRule<>(
77             TestActivity.class, false, false);
78 
79     private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
80 
81     @Test
testShowAndHide_renderSynchronouslyBetweenImeWindowAndAppContent()82     public void testShowAndHide_renderSynchronouslyBetweenImeWindowAndAppContent() throws Throwable {
83         runTest(false /* useControlApi */);
84     }
85 
86     @Test
testControl_rendersSynchronouslyBetweenImeWindowAndAppContent()87     public void testControl_rendersSynchronouslyBetweenImeWindowAndAppContent() throws Throwable {
88         runTest(true /* useControlApi */);
89     }
90 
runTest(boolean useControlApi)91     private void runTest(boolean useControlApi) throws Exception {
92         try (ImeSession imeSession = new ImeSession(SimpleIme.getName(mContext))) {
93             TestActivity activity = mActivityRule.launchActivity(null);
94             activity.setUseControlApi(useControlApi);
95             PollingCheck.waitFor(activity::hasWindowFocus);
96             activity.setEvaluator(() -> {
97                 // This runs from time to time on the UI thread.
98                 Bitmap screenshot = getInstrumentation().getUiAutomation().takeScreenshot();
99                 final int center = screenshot.getWidth() / 2;
100                 int imePositionApp = lowestPixelWithColor(APP_COLOR, 1, screenshot);
101                 int contentBottomMiddle = lowestPixelWithColor(APP_COLOR, center, screenshot);
102                 int behindImeBottomMiddle =
103                         lowestPixelWithColor(BEHIND_IME_COLOR, center, screenshot);
104                 int imePositionIme = Math.max(contentBottomMiddle, behindImeBottomMiddle);
105                 if (imePositionApp != imePositionIme) {
106                     mErrorCollector.addError(new AssertionError(String.format(Locale.US,
107                             "IME is positioned at %d (max of %d, %d),"
108                                     + " app thinks it is positioned at %d",
109                             imePositionIme, contentBottomMiddle, behindImeBottomMiddle,
110                             imePositionApp)));
111                 }
112             });
113             Thread.sleep(2000);
114         }
115     }
116 
lowestPixelWithColor(int color, int x, Bitmap bitmap)117     private static int lowestPixelWithColor(int color, int x, Bitmap bitmap) {
118         int[] pixels = new int[bitmap.getHeight()];
119         bitmap.getPixels(pixels, 0, 1, x, 0, 1, bitmap.getHeight());
120         for (int y = pixels.length - 1; y >= 0; y--) {
121             if (pixels[y] == color) {
122                 return y;
123             }
124         }
125         return -1;
126     }
127 
128     public static class TestActivity extends Activity implements
129             WindowInsetsController.OnControllableInsetsChangedListener {
130 
131         private TestView mTestView;
132         private EditText mEditText;
133         private Runnable mEvaluator;
134         private boolean mUseControlApi;
135 
136         @Override
onCreate(@ullable Bundle savedInstanceState)137         protected void onCreate(@Nullable Bundle savedInstanceState) {
138             super.onCreate(savedInstanceState);
139             getWindow().requestFeature(Window.FEATURE_NO_TITLE);
140             getWindow().setDecorFitsSystemWindows(false);
141             getWindow().setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_HIDDEN);
142             mTestView = new TestView(this);
143             mEditText = new EditText(this);
144             mEditText.setImeOptions(EditorInfo.IME_FLAG_NO_FULLSCREEN);
145             mTestView.addView(mEditText);
146             mTestView.mEvaluator = () -> {
147                 if (mEvaluator != null) {
148                     mEvaluator.run();
149                 }
150             };
151             mEditText.requestFocus();
152             setContentView(mTestView);
153             mEditText.getWindowInsetsController().addOnControllableInsetsChangedListener(this);
154         }
155 
setEvaluator(Runnable evaluator)156         void setEvaluator(Runnable evaluator) {
157             mEvaluator = evaluator;
158         }
159 
setUseControlApi(boolean useControlApi)160         void setUseControlApi(boolean useControlApi) {
161             mUseControlApi = useControlApi;
162         }
163 
164         @Override
onControllableInsetsChanged(@onNull WindowInsetsController controller, int typeMask)165         public void onControllableInsetsChanged(@NonNull WindowInsetsController controller,
166                 int typeMask) {
167             if ((typeMask & ime()) != 0) {
168                 mEditText.getWindowInsetsController().removeOnControllableInsetsChangedListener(
169                         this);
170                 showIme();
171             }
172         }
173 
showIme()174         private void showIme() {
175             if (mUseControlApi) {
176                 requestControlThenTransitionToVisibility(mTestView.getWindowInsetsController(),
177                         ime(), true);
178             } else {
179                 mTestView.getWindowInsetsController().show(ime());
180             }
181         }
182 
hideIme()183         private void hideIme() {
184             if (mUseControlApi) {
185                 requestControlThenTransitionToVisibility(mTestView.getWindowInsetsController(),
186                         ime(), false);
187             } else {
188                 mTestView.getWindowInsetsController().hide(ime());
189             }
190         }
191 
192         private static class TestView extends FrameLayout {
193             private WindowInsets mLayoutInsets;
194             private WindowInsets mAnimationInsets;
195             private final Rect mTmpRect = new Rect();
196             private final Paint mContentPaint = new Paint();
197 
198             private final Callback mInsetsCallback = new Callback(Callback.DISPATCH_MODE_STOP) {
199                 @NonNull
200                 @Override
201                 public WindowInsets onProgress(@NonNull WindowInsets insets,
202                         @NonNull List<WindowInsetsAnimation> runningAnimations) {
203                     if (runningAnimations.stream().anyMatch(TestView::isImeAnimation)) {
204                         mAnimationInsets = insets;
205                         invalidate();
206                     }
207                     return WindowInsets.CONSUMED;
208                 }
209 
210                 @Override
211                 public void onEnd(@NonNull WindowInsetsAnimation animation) {
212                     if (isImeAnimation(animation)) {
213                         mAnimationInsets = null;
214                         post(() -> {
215                             if (mLayoutInsets.isVisible(ime())) {
216                                 ((TestActivity) getContext()).hideIme();
217                             } else {
218                                 ((TestActivity) getContext()).showIme();
219                             }
220                         });
221 
222                     }
223                 }
224             };
225             private final Runnable mRunEvaluator;
226             private Runnable mEvaluator;
227 
TestView(Context context)228             TestView(Context context) {
229                 super(context);
230                 setWindowInsetsAnimationCallback(mInsetsCallback);
231                 mContentPaint.setColor(APP_COLOR);
232                 mContentPaint.setStyle(Paint.Style.FILL);
233                 setWillNotDraw(false);
234                 mRunEvaluator = () -> {
235                     if (mEvaluator != null) {
236                         mEvaluator.run();
237                     }
238                 };
239             }
240 
241             @Override
onApplyWindowInsets(WindowInsets insets)242             public WindowInsets onApplyWindowInsets(WindowInsets insets) {
243                 mLayoutInsets = insets;
244                 return WindowInsets.CONSUMED;
245             }
246 
getEffectiveInsets()247             private WindowInsets getEffectiveInsets() {
248                 return mAnimationInsets != null ? mAnimationInsets : mLayoutInsets;
249             }
250 
251             @Override
onDraw(Canvas canvas)252             protected void onDraw(Canvas canvas) {
253                 canvas.drawColor(BEHIND_IME_COLOR);
254                 mTmpRect.set(0, 0, getWidth(), getHeight());
255                 Insets insets = getEffectiveInsets().getInsets(ime());
256                 insetRect(mTmpRect, insets);
257                 canvas.drawRect(mTmpRect, mContentPaint);
258                 removeCallbacks(mRunEvaluator);
259                 post(mRunEvaluator);
260             }
261 
isImeAnimation(WindowInsetsAnimation animation)262             private static boolean isImeAnimation(WindowInsetsAnimation animation) {
263                 return (animation.getTypeMask() & ime()) != 0;
264             }
265 
insetRect(Rect rect, Insets insets)266             private static void insetRect(Rect rect, Insets insets) {
267                 rect.left += insets.left;
268                 rect.top += insets.top;
269                 rect.right -= insets.right;
270                 rect.bottom -= insets.bottom;
271             }
272         }
273     }
274 
275     private static class ImeSession implements AutoCloseable {
276 
277         private static final long TIMEOUT = 2000;
278         private final ComponentName mImeName;
279         private Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
280 
ImeSession(ComponentName ime)281         ImeSession(ComponentName ime) throws Exception {
282             mImeName = ime;
283             executeShellCommand("ime reset");
284             executeShellCommand("ime enable " + ime.flattenToShortString());
285             executeShellCommand("ime set " + ime.flattenToShortString());
286             PollingCheck.check("Make sure that MockIME becomes available", TIMEOUT,
287                     () -> ime.equals(getCurrentInputMethodId()));
288         }
289 
290         @Override
close()291         public void close() throws Exception {
292             executeShellCommand("ime reset");
293             PollingCheck.check("Make sure that MockIME becomes unavailable", TIMEOUT, () ->
294                     mContext.getSystemService(InputMethodManager.class)
295                             .getEnabledInputMethodList()
296                             .stream()
297                             .noneMatch(info -> mImeName.equals(info.getComponent())));
298         }
299 
300         @Nullable
getCurrentInputMethodId()301         private ComponentName getCurrentInputMethodId() {
302             // TODO: Replace this with IMM#getCurrentInputMethodIdForTesting()
303             return ComponentName.unflattenFromString(
304                     Settings.Secure.getString(mContext.getContentResolver(),
305                     Settings.Secure.DEFAULT_INPUT_METHOD));
306         }
307     }
308 
309     public static class SimpleIme extends InputMethodService {
310 
311         public static final int HEIGHT_DP = 200;
312         public static final int SIDE_PADDING_DP = 50;
313 
314         @Override
onCreateInputView()315         public View onCreateInputView() {
316             final ViewGroup view = new FrameLayout(this);
317             final View inner = new View(this);
318             final float density = getResources().getDisplayMetrics().density;
319             final int height = (int) (HEIGHT_DP * density);
320             final int sidePadding = (int) (SIDE_PADDING_DP * density);
321             view.setPadding(sidePadding, 0, sidePadding, 0);
322             view.addView(inner, new FrameLayout.LayoutParams(MATCH_PARENT,
323                     height));
324             inner.setBackgroundColor(IME_COLOR);
325             return view;
326         }
327 
getName(Context context)328         static ComponentName getName(Context context) {
329             return new ComponentName(context, SimpleIme.class);
330         }
331     }
332 }
333