1 /* 2 * Copyright (C) 2008 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.widget.cts; 18 19 import static org.junit.Assert.assertEquals; 20 import static org.junit.Assert.assertNotNull; 21 import static org.junit.Assert.assertNull; 22 import static org.junit.Assert.assertSame; 23 import static org.junit.Assert.assertTrue; 24 import static org.mockito.Mockito.mock; 25 import static org.mockito.Mockito.times; 26 import static org.mockito.Mockito.verify; 27 import static org.mockito.Mockito.verifyNoMoreInteractions; 28 29 import android.app.Activity; 30 import android.app.ActivityGroup; 31 import android.app.Instrumentation; 32 import android.content.Intent; 33 import android.view.KeyEvent; 34 import android.view.View; 35 import android.widget.ListView; 36 import android.widget.TabHost; 37 import android.widget.TabHost.OnTabChangeListener; 38 import android.widget.TabHost.TabSpec; 39 import android.widget.TextView; 40 41 import androidx.test.InstrumentationRegistry; 42 import androidx.test.annotation.UiThreadTest; 43 import androidx.test.filters.SmallTest; 44 import androidx.test.rule.ActivityTestRule; 45 import androidx.test.runner.AndroidJUnit4; 46 47 import com.android.compatibility.common.util.WidgetTestUtils; 48 49 import org.junit.Before; 50 import org.junit.Rule; 51 import org.junit.Test; 52 import org.junit.runner.RunWith; 53 54 /** 55 * Test {@link TabHost}. 56 */ 57 @SmallTest 58 @RunWith(AndroidJUnit4.class) 59 public class TabHostTest { 60 private static final String TAG_TAB1 = "tab 1"; 61 private static final String TAG_TAB2 = "tab 2"; 62 private static final int TAB_HOST_ID = android.R.id.tabhost; 63 64 private Instrumentation mInstrumentation; 65 private TabHostCtsActivity mActivity; 66 67 @Rule 68 public ActivityTestRule<TabHostCtsActivity> mActivityRule = 69 new ActivityTestRule<>(TabHostCtsActivity.class); 70 71 @Before 72 public void setup() { 73 mInstrumentation = InstrumentationRegistry.getInstrumentation(); 74 mActivity = mActivityRule.getActivity(); 75 } 76 77 @Test 78 public void testConstructor() { 79 new TabHost(mActivity); 80 81 new TabHost(mActivity, null); 82 } 83 84 @Test 85 public void testNewTabSpec() { 86 TabHost tabHost = new TabHost(mActivity); 87 88 assertNotNull(tabHost.newTabSpec(TAG_TAB2)); 89 } 90 91 @Test(expected=IllegalArgumentException.class) 92 public void testNewTabSpecWithNullTag() { 93 TabHost tabHost = new TabHost(mActivity); 94 95 tabHost.newTabSpec(null); 96 } 97 98 /* 99 * Check points: 100 * 1. the tabWidget view and tabContent view associated with tabHost are created. 101 * 2. no exception occurs when doing normal operation after setup(). 102 */ 103 @Test 104 public void testSetup1() throws Throwable { 105 final Intent launchIntent = new Intent(Intent.ACTION_MAIN); 106 launchIntent.setClassName("android.widget.cts", CtsActivity.class.getName()); 107 launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 108 final Activity activity = mInstrumentation.startActivitySync(launchIntent); 109 mInstrumentation.waitForIdleSync(); 110 111 mActivityRule.runOnUiThread(() -> { 112 activity.setContentView(R.layout.tabhost_layout); 113 114 TabHost tabHost = (TabHost) activity.findViewById(TAB_HOST_ID); 115 assertNull(tabHost.getTabWidget()); 116 assertNull(tabHost.getTabContentView()); 117 tabHost.setup(); 118 assertNotNull(tabHost.getTabWidget()); 119 assertNotNull(tabHost.getTabContentView()); 120 121 TabSpec tabSpec = tabHost.newTabSpec(TAG_TAB1); 122 tabSpec.setIndicator(TAG_TAB1); 123 tabSpec.setContent(new MyTabContentFactoryList()); 124 tabHost.addTab(tabSpec); 125 tabHost.setCurrentTab(0); 126 }); 127 mInstrumentation.waitForIdleSync(); 128 129 activity.finish(); 130 } 131 132 /* 133 * Check points: 134 * 1. the tabWidget view and tabContent view associated with tabHost are created. 135 * 2. no exception occurs when uses TabSpec.setContent(android.content.Intent) after setup(). 136 */ 137 @Test 138 public void testSetup2() throws Throwable { 139 final Intent launchIntent = new Intent(Intent.ACTION_MAIN); 140 launchIntent.setClassName("android.widget.cts", ActivityGroup.class.getName()); 141 launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 142 final ActivityGroup activity = 143 (ActivityGroup) mInstrumentation.startActivitySync(launchIntent); 144 mInstrumentation.waitForIdleSync(); 145 146 mActivityRule.runOnUiThread(() -> { 147 activity.setContentView(R.layout.tabhost_layout); 148 149 TabHost tabHost = (TabHost) activity.findViewById(TAB_HOST_ID); 150 assertNull(tabHost.getTabWidget()); 151 assertNull(tabHost.getTabContentView()); 152 tabHost.setup(activity.getLocalActivityManager()); 153 assertNotNull(tabHost.getTabWidget()); 154 assertNotNull(tabHost.getTabContentView()); 155 156 TabSpec tabSpec = tabHost.newTabSpec(TAG_TAB1); 157 tabSpec.setIndicator(TAG_TAB1); 158 Intent intent = new Intent(Intent.ACTION_VIEW, null, 159 mActivity, CtsActivity.class); 160 tabSpec.setContent(intent); 161 tabHost.addTab(tabSpec); 162 tabHost.setCurrentTab(0); 163 }); 164 mInstrumentation.waitForIdleSync(); 165 166 activity.finish(); 167 } 168 169 @UiThreadTest 170 @Test 171 public void testAddTab() { 172 TabHost tabHost = mActivity.getTabHost(); 173 // there is a initial tab 174 assertEquals(1, tabHost.getTabWidget().getChildCount()); 175 176 TabSpec tabSpec = tabHost.newTabSpec(TAG_TAB2); 177 tabSpec.setIndicator(TAG_TAB2); 178 tabSpec.setContent(new MyTabContentFactoryList()); 179 tabHost.addTab(tabSpec); 180 assertEquals(2, tabHost.getTabWidget().getChildCount()); 181 tabHost.setCurrentTab(1); 182 assertTrue(tabHost.getCurrentView() instanceof ListView); 183 assertEquals(TAG_TAB2, tabHost.getCurrentTabTag()); 184 } 185 186 @UiThreadTest 187 @Test(expected=IllegalArgumentException.class) 188 public void testAddTabNoIndicatorNoContent() { 189 TabHost tabHost = mActivity.getTabHost(); 190 tabHost.addTab(tabHost.newTabSpec("tab 3")); 191 } 192 193 @UiThreadTest 194 @Test(expected=IllegalArgumentException.class) 195 public void testAddTabNoContent() { 196 TabHost tabHost = mActivity.getTabHost(); 197 tabHost.addTab(tabHost.newTabSpec("tab 3").setIndicator("tab 3")); 198 } 199 200 @UiThreadTest 201 @Test(expected=IllegalArgumentException.class) 202 public void testAddTabNoIndicator() { 203 TabHost tabHost = mActivity.getTabHost(); 204 tabHost.addTab(tabHost.newTabSpec("tab 3").setContent(new MyTabContentFactoryText())); 205 } 206 207 @UiThreadTest 208 @Test(expected=NullPointerException.class) 209 public void testAddTabNull() { 210 TabHost tabHost = mActivity.getTabHost(); 211 tabHost.addTab(null); 212 } 213 214 @UiThreadTest 215 @Test 216 public void testClearAllTabs() { 217 TabHost tabHost = mActivity.getTabHost(); 218 MyTabContentFactoryText tcf = new MyTabContentFactoryText(); 219 // add two additional tabs 220 tabHost.addTab(tabHost.newTabSpec(TAG_TAB1).setIndicator(TAG_TAB1).setContent(tcf)); 221 tabHost.addTab(tabHost.newTabSpec(TAG_TAB2).setIndicator(TAG_TAB2).setContent(tcf)); 222 assertEquals(3, tabHost.getTabWidget().getChildCount()); 223 assertEquals(3, tabHost.getTabContentView().getChildCount()); 224 assertEquals(0, tabHost.getCurrentTab()); 225 assertNotNull(tabHost.getCurrentView()); 226 227 tabHost.clearAllTabs(); 228 229 assertEquals(0, tabHost.getTabWidget().getChildCount()); 230 assertEquals(0, tabHost.getTabContentView().getChildCount()); 231 assertEquals(-1, tabHost.getCurrentTab()); 232 assertNull(tabHost.getCurrentView()); 233 } 234 235 @Test 236 public void testGetTabWidget() { 237 TabHost tabHost = mActivity.getTabHost(); 238 239 // The attributes defined in tabhost_layout.xml 240 assertEquals(android.R.id.tabs, tabHost.getTabWidget().getId()); 241 WidgetTestUtils.assertScaledPixels(1, tabHost.getTabWidget().getPaddingLeft(), mActivity); 242 WidgetTestUtils.assertScaledPixels(1, tabHost.getTabWidget().getPaddingRight(), mActivity); 243 WidgetTestUtils.assertScaledPixels(4, tabHost.getTabWidget().getPaddingTop(), mActivity); 244 } 245 246 @UiThreadTest 247 @Test 248 public void testAccessCurrentTab() { 249 TabHost tabHost = mActivity.getTabHost(); 250 assertEquals(0, tabHost.getCurrentTab()); 251 252 // normal value 253 TabSpec tabSpec = tabHost.newTabSpec(TAG_TAB2); 254 tabSpec.setIndicator(TAG_TAB2); 255 tabSpec.setContent(new MyTabContentFactoryText()); 256 tabHost.addTab(tabSpec); 257 tabHost.setCurrentTab(1); 258 assertEquals(1, tabHost.getCurrentTab()); 259 tabHost.setCurrentTab(0); 260 assertEquals(0, tabHost.getCurrentTab()); 261 262 // exceptional value 263 tabHost.setCurrentTab(tabHost.getTabWidget().getChildCount() + 1); 264 assertEquals(0, tabHost.getCurrentTab()); 265 tabHost.setCurrentTab(-1); 266 assertEquals(0, tabHost.getCurrentTab()); 267 } 268 269 @UiThreadTest 270 @Test 271 public void testGetCurrentTabView() { 272 TabHost tabHost = mActivity.getTabHost(); 273 // current tab view is the first child of tabWidget. 274 assertSame(tabHost.getTabWidget().getChildAt(0), tabHost.getCurrentTabView()); 275 276 TabSpec tabSpec = tabHost.newTabSpec(TAG_TAB2); 277 tabSpec.setIndicator(TAG_TAB2); 278 tabSpec.setContent(new MyTabContentFactoryText()); 279 tabHost.addTab(tabSpec); 280 tabHost.setCurrentTab(1); 281 // current tab view is the second child of tabWidget. 282 assertSame(tabHost.getTabWidget().getChildAt(1), tabHost.getCurrentTabView()); 283 } 284 285 @UiThreadTest 286 @Test 287 public void testGetCurrentView() { 288 TabHost tabHost = mActivity.getTabHost(); 289 TextView textView = (TextView) tabHost.getCurrentView(); 290 assertEquals(TabHostCtsActivity.INITIAL_VIEW_TEXT, textView.getText().toString()); 291 292 TabSpec tabSpec = tabHost.newTabSpec(TAG_TAB2); 293 tabSpec.setIndicator(TAG_TAB2); 294 tabSpec.setContent(new MyTabContentFactoryList()); 295 tabHost.addTab(tabSpec); 296 tabHost.setCurrentTab(1); 297 assertTrue(tabHost.getCurrentView() instanceof ListView); 298 } 299 300 @UiThreadTest 301 @Test 302 public void testSetCurrentTabByTag() { 303 TabHost tabHost = mActivity.getTabHost(); 304 305 // set CurrentTab 306 TabSpec tabSpec = tabHost.newTabSpec(TAG_TAB2); 307 tabSpec.setIndicator(TAG_TAB2); 308 tabSpec.setContent(new MyTabContentFactoryText()); 309 tabHost.addTab(tabSpec); 310 311 tabHost.setCurrentTabByTag(TAG_TAB2); 312 assertEquals(1, tabHost.getCurrentTab()); 313 314 tabHost.setCurrentTabByTag(TabHostCtsActivity.INITIAL_TAB_TAG); 315 assertEquals(0, tabHost.getCurrentTab()); 316 317 // exceptional value 318 tabHost.setCurrentTabByTag(null); 319 assertEquals(0, tabHost.getCurrentTab()); 320 321 tabHost.setCurrentTabByTag("unknown tag"); 322 assertEquals(0, tabHost.getCurrentTab()); 323 } 324 325 @UiThreadTest 326 @Test 327 public void testGetTabContentView() { 328 TabHost tabHost = mActivity.getTabHost(); 329 assertEquals(3, tabHost.getTabContentView().getChildCount()); 330 331 TextView child0 = (TextView) tabHost.getTabContentView().getChildAt(0); 332 assertEquals(mActivity.getResources().getString(R.string.hello_world), 333 child0.getText().toString()); 334 assertTrue(tabHost.getTabContentView().getChildAt(1) instanceof ListView); 335 TextView child2 = (TextView) tabHost.getTabContentView().getChildAt(2); 336 tabHost.setCurrentTab(0); 337 assertEquals(TabHostCtsActivity.INITIAL_VIEW_TEXT, child2.getText().toString()); 338 339 TabSpec tabSpec = tabHost.newTabSpec(TAG_TAB2); 340 tabSpec.setIndicator(TAG_TAB2); 341 tabSpec.setContent(new MyTabContentFactoryList()); 342 tabHost.addTab(tabSpec); 343 assertEquals(3, tabHost.getTabContentView().getChildCount()); 344 tabHost.setCurrentTab(1); 345 assertEquals(4, tabHost.getTabContentView().getChildCount()); 346 347 child0 = (TextView) tabHost.getTabContentView().getChildAt(0); 348 assertEquals(mActivity.getResources().getString(R.string.hello_world), 349 child0.getText().toString()); 350 assertTrue(tabHost.getTabContentView().getChildAt(1) instanceof ListView); 351 child2 = (TextView) tabHost.getTabContentView().getChildAt(2); 352 tabHost.setCurrentTab(0); 353 assertEquals(TabHostCtsActivity.INITIAL_VIEW_TEXT, child2.getText().toString()); 354 } 355 356 /** 357 * Check points: 358 * 1. the specified callback should be invoked when the selected state of any of the items 359 * in this list changes 360 */ 361 @UiThreadTest 362 @Test 363 public void testSetOnTabChangedListener() { 364 TabHost tabHost = mActivity.getTabHost(); 365 366 // add a tab, and change current tab to the new tab 367 OnTabChangeListener mockTabChangeListener = mock(OnTabChangeListener.class); 368 tabHost.setOnTabChangedListener(mockTabChangeListener); 369 370 TabSpec tabSpec = tabHost.newTabSpec(TAG_TAB2); 371 tabSpec.setIndicator(TAG_TAB2); 372 tabSpec.setContent(new MyTabContentFactoryList()); 373 tabHost.addTab(tabSpec); 374 tabHost.setCurrentTab(1); 375 verify(mockTabChangeListener, times(1)).onTabChanged(TAG_TAB2); 376 377 // change current tab to the first one 378 tabHost.setCurrentTab(0); 379 verify(mockTabChangeListener, times(1)).onTabChanged(TabHostCtsActivity.INITIAL_TAB_TAG); 380 381 // set the same tab 382 tabHost.setCurrentTab(0); 383 verifyNoMoreInteractions(mockTabChangeListener); 384 } 385 386 @UiThreadTest 387 @Test 388 public void testGetCurrentTabTag() { 389 TabHost tabHost = mActivity.getTabHost(); 390 assertEquals(TabHostCtsActivity.INITIAL_TAB_TAG, tabHost.getCurrentTabTag()); 391 392 TabSpec tabSpec = tabHost.newTabSpec(TAG_TAB2); 393 tabSpec.setIndicator(TAG_TAB2); 394 tabSpec.setContent(new MyTabContentFactoryList()); 395 tabHost.addTab(tabSpec); 396 tabHost.setCurrentTab(1); 397 assertEquals(TAG_TAB2, tabHost.getCurrentTabTag()); 398 } 399 400 @Test 401 public void testKeyboardNavigation() throws Throwable { 402 mActivityRule.runOnUiThread(() -> { 403 mActivity.setContentView(R.layout.tabhost_focus); 404 TabHost tabHost = mActivity.findViewById(android.R.id.tabhost); 405 tabHost.setup(); 406 TabSpec spec = tabHost.newTabSpec("Tab 1"); 407 spec.setContent(R.id.tab1); 408 spec.setIndicator("Tab 1"); 409 tabHost.addTab(spec); 410 spec = tabHost.newTabSpec("Tab 2"); 411 spec.setContent(R.id.tab2); 412 spec.setIndicator("Tab 2"); 413 tabHost.addTab(spec); 414 View topBut = mActivity.findViewById(R.id.before_button); 415 topBut.requestFocus(); 416 assertTrue(topBut.isFocused()); 417 }); 418 mInstrumentation.waitForIdleSync(); 419 mInstrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_TAB); 420 View tabs = mActivity.findViewById(android.R.id.tabs); 421 assertTrue(tabs.hasFocus()); 422 View firstTab = tabs.findFocus(); 423 mInstrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_TAB); 424 assertTrue(tabs.hasFocus()); 425 int[] shiftKey = new int[]{KeyEvent.KEYCODE_SHIFT_LEFT}; 426 sendKeyComboSync(KeyEvent.KEYCODE_TAB, shiftKey); 427 assertTrue(tabs.hasFocus()); 428 mInstrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_TAB); 429 assertTrue(tabs.hasFocus()); 430 431 // non-navigation sends focus to content 432 mInstrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_E); 433 assertTrue(mActivity.findViewById(R.id.tab1_button).isFocused()); 434 sendKeyComboSync(KeyEvent.KEYCODE_TAB, shiftKey); 435 assertTrue(tabs.hasFocus()); 436 437 mActivityRule.runOnUiThread(() -> firstTab.requestFocus()); 438 mInstrumentation.waitForIdleSync(); 439 sendKeyComboSync(KeyEvent.KEYCODE_TAB, shiftKey); 440 assertTrue(mActivity.findViewById(R.id.before_button).isFocused()); 441 } 442 443 private class MyTabContentFactoryText implements TabHost.TabContentFactory { 444 public View createTabContent(String tag) { 445 final TextView tv = new TextView(mActivity); 446 tv.setText(tag); 447 return tv; 448 } 449 } 450 451 private class MyTabContentFactoryList implements TabHost.TabContentFactory { 452 public View createTabContent(String tag) { 453 final ListView lv = new ListView(mActivity); 454 return lv; 455 } 456 } 457 458 private static int metaFromKey(int keyCode) { 459 switch(keyCode) { 460 case KeyEvent.KEYCODE_ALT_LEFT: return KeyEvent.META_ALT_LEFT_ON; 461 case KeyEvent.KEYCODE_ALT_RIGHT: return KeyEvent.META_ALT_RIGHT_ON; 462 case KeyEvent.KEYCODE_SHIFT_LEFT: return KeyEvent.META_SHIFT_LEFT_ON; 463 case KeyEvent.KEYCODE_SHIFT_RIGHT: return KeyEvent.META_SHIFT_RIGHT_ON; 464 case KeyEvent.KEYCODE_CTRL_LEFT: return KeyEvent.META_CTRL_LEFT_ON; 465 case KeyEvent.KEYCODE_CTRL_RIGHT: return KeyEvent.META_CTRL_RIGHT_ON; 466 case KeyEvent.KEYCODE_META_LEFT: return KeyEvent.META_META_LEFT_ON; 467 case KeyEvent.KEYCODE_META_RIGHT: return KeyEvent.META_META_RIGHT_ON; 468 } 469 return 0; 470 } 471 472 /** 473 * High-level method for sending a chorded key-combo (modifiers + key). This will send all the 474 * down and up key events as a user would press them (ie. all the modifiers get their own 475 * down and up events). 476 * 477 * @param keyCode The keycode to send while all meta keys are pressed. 478 * @param metaKeys An array of meta key *keycodes* (not modifiers). 479 */ 480 private void sendKeyComboSync(int keyCode, int[] metaKeys) { 481 int metaState = 0; 482 if (metaKeys != null) { 483 for (int mk = 0; mk < metaKeys.length; ++mk) { 484 metaState |= metaFromKey(metaKeys[mk]); 485 mInstrumentation.sendKeySync(new KeyEvent(0, 0, KeyEvent.ACTION_DOWN, metaKeys[mk], 486 0, KeyEvent.normalizeMetaState(metaState))); 487 } 488 } 489 mInstrumentation.sendKeySync(new KeyEvent(0, 0, KeyEvent.ACTION_DOWN, keyCode, 0, 490 KeyEvent.normalizeMetaState(metaState))); 491 mInstrumentation.sendKeySync(new KeyEvent(0, 0, KeyEvent.ACTION_UP, keyCode, 0, 492 KeyEvent.normalizeMetaState(metaState))); 493 if (metaKeys != null) { 494 for (int mk = 0; mk < metaKeys.length; ++mk) { 495 metaState &= ~metaFromKey(metaKeys[mk]); 496 mInstrumentation.sendKeySync(new KeyEvent(0, 0, KeyEvent.ACTION_UP, metaKeys[mk], 0, 497 KeyEvent.normalizeMetaState(metaState))); 498 } 499 } 500 } 501 } 502